刷题时学到的TIPS

本文记录了一些在刷题过程中遇到的一些TIPS

边界条件

  • $10^{9}$需要额外注意,通常会出现int溢出的问题
  • 256MB内存大约可以存储 8000 * 8000 的32位数据
  • 如果内存允许,有时可以使用数组替代unordered_map,因为unordered_map需要编写额外的哈希函数来计算pair等复杂类型键的哈希值
  • 回溯问题通常可以使用空间换时间,记录各个分支的解,减少重复计算

二叉树

完全二叉树

数组表示的完全二叉树中,第i个节点与其左右子节点的关系为(i从0开始):
$$
\begin{align}
& i_{left}=2i + 1 \\
& i_{right} = 2i + 2
\end{align}
$$

二叉搜索树

二叉搜索树有一个很好的性质,即其中序遍历的结果严格升序。

二叉树遍历

前序、后序、中序遍历可以用栈来进行实现,且前中序的迭代代码类似,如下:

C++
vector<int> traversal(TreeNode* root) {
    stack<TreeNode*> stk;
    vector<int> res;

    while (root || !stk.empty()) {
        while (root) {
            stk.push(root);
            // 前序:res.push_back(root->val);
            root = root->left;
        }
        root = stk.top();
        stk.pop();
        // 中序:res.push_back(root->val);
        root = root->right;
    }

    return res;
}

而后序的代码较为复杂,代码如下(修改自本文提供的代码):

C++
vector<int> postorderTraversal(TreeNode* root) {  
    stack<TreeNode*> stk;
    vector<int> res;
    TreeNode* cur = root;
    TreeNode* pre = nullptr;//用来记录上一个已经结点
    while(!s.empty() || cur){
        while(cur){
            s.push(cur);
            cur = cur->left;
        }       
        cur = s.top();
        //下面的判断是说明,右子树当前的cur的右子树已经遍历完了,可以进行遍历当前的结点了
        if(!cur->right || cur->right==pre){
            res.push_back(cur->val);
            s.pop();
            pre = cur;
            cur = nullptr;//这一步很重要,这一步能够使得在遍历完当前的cur以及其左右子树之后能够进一步往树的上方回溯
        }else{
            cur = cur->right;
        }
    }
    return res;
}

数组

子序列

异位词的判别方法

通常将字符串转化为统计各个字母出现次数的array,比较时只需要比较两个array是否相等。($O(n)$)
或者利用排序的方式,将字符串进行排序后比较。($O(nlongn)$)

更进一步的,还可以对上述两者求散列值进一步的加速相关操作。

回文串的处理技巧

破坏偶数回文串只需要搜索相邻两个相同的字符,删去其中一个即可。

链表

删除/插入

由于删除和插入都有可能改变头指针,因此通常会创建一个假头,将真头作为其后继,从而便于统一处理。

通常利用快慢指针的方式对环进行相关处理。

排序

常见排序汇总表

名称最好时间复杂度最坏时间复杂度平均时间复杂度空间复杂度稳定性说明
冒泡$O(n)$$O(n^2)$$O(n^2)$$O(1)$T有序情况下最好,倒序情况下最坏。
插入$O(n)$$O(n^2)$$O(n^2)$$O(1)$T有序情况下最好,倒序情况下最坏。
适用于数据量少、基本有序的情况。
选择$O(n^2)$$O(n^2)$$O(n^2)$$O(1)$F虽然时间复杂度高,但是交换次数少。因此比冒泡效率要高。
找最值进行交换导致不稳定。
希尔$O(n^{1.3~2})$$O(n^{1.3~2})$$O(n^{1.3~2})$$O(1)$F缩小增量插入排序。
增量可以自行选择,会对时间复杂度产生影响。
跨元素交换导致不稳定。
$O(nlogn)$$O(nlogn)$$O(nlogn)$$O(1)$F建堆$O(n)$,排序$O(nlong)$。
相对于快排的交换次数多、访存不连续
归并$O(nlogn)$$O(nlogn)$$O(nlogn)$$O(n)$T每次都先放左侧的数,即可保证稳定。
快速$O(nlogn)$$O(n^2)$$O(nlogn)$$O(logn ~ n)$F正序、倒序情况最坏(时间空间都是)。
可以利用栈实现。
中心点的选取非常重要,可以利用三数取中、随机选取的方法进行优化。
也可以在元素少于一定数值时做插入排序。
还可以利用三向切分的方法,跳过相同元素。
$O(n)$$O(n^2)$$O(n)$$O(n)$T桶内使用链表插入排序
参考文章

快速排序

快速排序的不同实现方法

本科上课且个人一直觉得比较好理解的是空穴法,即下述这种实现方法:

C++
int quickSelect(vector<int>& nums, int i, int j, int k) {
    int left = i, right = j;

    int pivot = nums[right];
    while (left < right) {
        while (left < right && nums[left] >= pivot) left++;
        if (left < right) nums[right] = nums[left];
        while (left < right && nums[right] <= pivot) right--;
        if (left < right) nums[left] = nums[right];
    }
    nums[left] = pivot;

    if (left == k) {
        return nums[left];
    }
    else if (left < k) {
        return quickSelect(nums, left + 1, j, k);
    }
    else {
        return quickSelect(nums, i, left - 1, k);
    }
}

这种方法存在一个问题,即如果数组中存在大量的相同元素,例如考虑一个全为0的数列,那么在右移左指针时,就会直接将左指针移动到最右边,使得快排的时间复杂度退化到$O(n^2)$。

相反的,如果使用交换法,就可以解决上述情况:

C++
int quickSelect(vector<int>& nums, int i, int j, int k) {
    if (i == j) return nums[k];

    int pivot = nums[i];
    int left = i - 1, right = j + 1;
    while (left < right) {
        do left++; while(nums[left] > pivot);
        do right--; while (nums[right] < pivot);
        if (left < right) swap(nums[left], nums[right]);
    }

    if (right >= k) {
        return quickSelect(nums, i, right, k);
    }
    else {
        return quickSelect(nums, right + 1, j, k);
    }
}

int findKthLargest(vector<int>& nums, int k) {
    return quickSelect(nums, 0, nums.size() - 1, k - 1);
}

可以看到,如果使用这种实现方式,全零的数列最终的中心点位置将会位于数组中央,使得时间复杂度保持在$O(nlogn)$。
不过需要注意,这种方法无法得到每次划分时中心点的最终位置,该方法只能保证区间$[i, right]$和区间$[right + 1, j]$中的元素分别小于等于/大于等于中心点。只有在i==j时,中心点位置才能确定。

参考:

[1] 快速排序 Wiki

图论

图遍历时已访问的记录位置

在进行图遍历时,应当注意,更新已访问数组应该发生在入栈/队时,而不是出栈/队,后者虽然不会导致死循环,但是会导致一个节点被多次访问。

数学

最大公约数和最小公倍数满足:$ab=lcm(a.b)gcm(a,b)$

约瑟夫环的递推公式:$x^\prime = (x + k) % n$,其中$k = m % n$。该递归式把子问题的获胜者编号映射到上层。

发表评论

您的邮箱地址不会被公开。 必填项已用 * 标注

滚动至顶部