LeetCode整理笔记

目录

    • 双指针滑动窗口
    • 分治策略
    • 动态规划
    • 链表
    • 数组
    • 哈希
    • 深度优先搜索
    • 广度优先搜索
    • 数学问题
    • 位运算
    • 变换打印顺序
    • 高精度计算
    • 特殊数字顺序

整理进度:1-201,518-581,1017-1024

待归档:

记忆化广搜:
#576 Out of Boundary Paths

排序问题:
#581 Shortest Unsorted Continuous Subarray

凸包:
#587 Erect the Fence

哈希,重复模式:
#594 Longest Harmonious Subsequence

滑动窗口:
#611 Valid Triangle Number

找规律:
#621 Task Scheduler

动规/记忆化搜索/贪心:
#630 Course Schedule III

双指针滑动窗口

  • #3 Longest Substring Without Repeating Characters

    • 思路1:
      后指针向后滑动以添加新的元素,前指针向后滑动保证滑动窗口内无重复字符
    • 思路2:
      维护每个字符最后一次出现的位置,后指针发现一个前面出现过的字符时,前指针可以直接跳到该字符最后出现的位置后面,i = max(i, lastpos[s[j]]),效率更高一些
  • #11 Container With Most Water
    前指针、后指针中间的区域为一个桶,桶的面积为高乘以宽,当i、j为两端时,桶宽最大,当i、j向中间滑动时,只有桶高增加,桶面积才有可能增大。若移动长板,桶高不可能增大,因此只能移动短板对应的指针。

  • #1 2Sum
    当前后指针和大于k,前移后指针(和变小),否则后移前指针(和变大)

  • #15 3Sum
    遍历并固定第一个数,从而把问题转换为2Sum
    同理 #16 3Sum Closest、#18 4Sum

  • #76 Minimum Window Substring
    滑动窗口前后指针都从起始位置开始,先移动后指针使得窗口内数组符合要求,然后移动前指针直到窗口内数组不再符合要求,随后再次移动后指针,依此类推。

分治策略

  • #4 Median of Two Sorted Arrays

    • 思路1:双约束二分
      寻找A数组中的指针i、B数组中的指针j,使得两指针之前的元素的并集构成两数组的前半部分,这样就会产生两个约束条件,也即:
      ①前后两部分元素个数相等:i + j = m - i + n - j
      ②前半部分的最大值小于后半部分最小值:A[i - 1] <= B[j] && B[j - 1] <= A[i]
      由于i、j满足约束条件①,因此只要确定了i,就可以确定j,如此一来,只需要寻找i的合适位置
      如此一来可以利用二分搜索维护 i 的区间 [i_min, i_max],下次搜索区间可以利用约束条件②来确定
    • 思路2:归并
      简单把两个数组归并后找中位数,但需要用到额外的空间
  • #33 Search in Rotated Sorted Array
    首先用二分搜索找到数组中的最小元素,之后就可以构造出旋转前的数组元素对应在旋转后的数组中的位置索引,根据这一索引进行全局的二分搜索即可。#153 Find Minimum in Rotated Sorted Array 和这道题是一样的
    若考虑数组中存在重复元素,则需要在查找数组最小元素的二分搜索步骤中添加一个预处理步骤,保证二分区间的左右端点值不同,见 #81 Search in Rotated Sorted Array II#154 Find Minimum in Rotated Sorted Array II

  • #50 Pow(x, n)
    二分求快速幂,注意考虑幂的正负

  • #69 Sqrt(x)
    二分确定x的范围,比较x^2与a的大小

  • #74 Search a 2D Matrix
    其实不能算是分治策略,但形式有点像二维的二分搜索,所以干脆放在这里
    从右上角开始进行搜索,若小于目标值,则向下搜索,若大于目标值,则向左搜索,时间复杂度O(m + n)

  • #162 Find Peak Element
    很像之前遇到过的一个面试题目
    这道题如果用O(n)的算法是非常简单的,我们需要找到一个O(logn)的算法
    二分策略的一般流程是,先找到区间的中点元素,将这个元素与某一个标准进行对比,从而决定接下来的区间选择哪一侧
    对比标准常见的有:1、与某个目标值作对比,比如二分查找;2、与左右端点值作对比,比如上面的 #33 Search in Rotated Sorted Array;3、与中点元素两侧的元素作对比,相当于判断中点元素处的单调性,比如这道题就是。
    我们判断nums[mid]与左右两侧元素的大小,如果这一点正好是peak,就直接输出,如果某一侧单调递增,则在这一侧必存在一个peak元素(若递增后开始递减,则存在peak,若一直递增到数组末尾,因为设定nums[nums.size()] = -∞,所以此时末尾元素是一个peak),进而可以将搜索区间决定到这一侧

动态规划

  • #5 Longest Palindromic Substring

    • 思路1:与反向字符串计算最长公共子串
      回文串的问题,一般都可以试试对比原string与反向string的方法
    • 思路2:区间动规
      dp[i][j]表示子串 [i, j] 是否为回文子串
    • 思路3:中心扩展判断回文串
      遍历所有的回文串中心(考虑奇数、偶数长度),从中心向外扩展可以用O(n)的时间找到该中心位置对应的最长回文串,这个思路其实相当直接,但时空复杂度相对于上面两个动规反而更好
  • #10 Regular Expression Matching
    算是经典题目,.表示任意匹配单字符,*表示前一字符可以出现任意次数
    dp[i][j]表示模式串前 i 位是否能与字符串前 j 位匹配

  • #32 Longest Valid Parentheses
    dp[i]表示以i为结尾的最长合法括号子串长度
    s[i] == '('时,dp[i] = 0
    s[i] == ')'时,
    ①若s[i - 1] == '(',则dp[i] = dp[i - 2] + 2
    ②若s[i - 1] == ')' && s[i - dp[i - 1] - 1] == '(',则dp[i] = dp[i - 1] + 2 + dp[i - dp[i - 1] - 2]

  • #44 Wildcard Matching
    和#10类似的字符串匹配问题,这里*不再代表前一字符出现任意次数,而是代表任意序列(可为空)

    • 思路1:动规,可以考虑用滚动数组优化空间
      dp[i][j]表示字符串前前 i 位与模式串前 j 位匹配
      dp[i][j] = dp[i - 1][j - 1] && (s[i - 1] == p[j - 1] || p[j - 1] == '?'), if p[j - 1] != '*'
      dp[i][j] = dp[i][j - 1] || dp[i - 1][j], if p[j - 1] == '*'
    • 思路2:贪心
      若发现模式串出现*,则记录当前模式串/字符串遍历位置pi与si,继续往下匹配,若匹配失败,则返回记录的位置,字符串每次匹配失败后重新开始匹配的位置需要前移一位
  • #53 Maximum Subarray
    dp[i]表示以i为结尾的子数组最大值,则dp[i] = max(dp[i - 1] + arr[i], arr[i]),由于dp[i]仅与dp[i - 1]相关,所以可以降维到0维。
    同理**#121 Best Time to Buy and Sell Stock**

  • #188 Best Time to Buy and Sell Stock IV
    同系列的还有 #121 Best Time to Buy and Sell Stock#122 Best Time to Buy and Sell Stock II#123 Best Time to Buy and Sell Stock III
    第k次购入仅和第k-1次卖出时的收益相关,第k次卖出仅和第k次购入时的收益相关
    rele[i][j]表示 i 时刻进行第 j 次卖出的最大收益,以hold[i][j]表示 i 时刻进行第 j 次买入的最大收益

    for(int i=0; i<prices.size(); i++){
        for(int j=k; j>=1; j--){
            rele[j] = max(rele[j], prices[i]-hold[j]);
            hold[j] = min(hold[j], prices[i]-rele[j-1]);
            maxProfit=max(maxProfit, rele[j]);
        }
    }
    
  • #198 House Robber
    同系列的还有 #213 House Robber II#337 House Robber III
    个人感觉和前面的Best Time to Buy and Sell Stock是同一类型的题目,都是在每个时间步有两种不同的状态,区别在于Stock题目两种状态之间必须相互转移,而Robber这道题状态转移不一定发生在两种状态之间。
    这类题目可以推广到多个不同状态。

  • #72 Edit Distance
    类似正则匹配的方程,dp[i][j]表示a串0…i - 1位与b串0…j - 1位的编辑距离

    for (int i = 1; i <= m; i++) {
        for (int j = 1; j <= n; j++) {
            if (word1[i - 1] == word2[j - 1]) 
                dp[i][j] = dp[i - 1][j - 1];
            else dp[i][j] = min(dp[i - 1][j - 1] + 1, min(dp[i][j - 1] + 1, dp[i - 1][j] + 1));
        }
    }
    
  • #518 Coin Change 2
    dp[i]表示 i 有多少种组合方法,这里需要注意,因为找零是一个组合问题,要忽略顺序,所以选用类似带数量限制的背包问题的方法求解,amount放在内层循环,并倒序遍历从而防止重复选用同一种面额。

    for (int j = 0; j < coins.size(); ++j) {
        for (int i = amount; i >= 1; --i) {
            for (int k = i / coins[j]; k >= 1; --k) {
                dp[i] += dp[i - coins[j] * k];
            }
        }
    }
    
  • #96 Unique Binary Search Trees
    树形动规,遍历所有根节点的位置,那么以这一点为根的不同BST数量为其不同左子树的数量乘以不同右子树的数量。

  • #97 Interleaving String
    dp[i][j]表示a数组的前 i 位与b数组的前 j 位是否能够交错构成c数组的前 i + j 位
    dp[i][j] = dp[i - 1][j] && a[i - 1] == c[i + j - 1] || dp[i][j - 1] && b[j] == c[i + j - 1]

  • #115 Distinct Subsequences
    dp[i][j]表示模式串0…i - 1与字符串0…j - 1位有多少distinct subsequences
    dp[i][j] = dp[i][j - 1] + (t[i - 1] == s[j - 1] ? dp[i - 1][j - 1] : 0)

  • #132 Palindrome Partitioning II
    很容易能想到一个O(n^3)的动规,用dp[i][j]表示i…j段的最小切数,那么dp[i][j] = min(dp[i][k] + dp[k][j] + 1),相当于在k处切一刀时,i…j的最小切数,但这种方法会超时。
    一个更好的办法是用一个O(n^2)的动规,以dp[i]表示i…n - 1段的最小切数,那么若i…j段为回文串时,则dp[i] = max(dp[i], dp[j + 1] + 1),相当于在j后面切一刀

  • #152 Maximum Product Subarray
    因为存在负数,所以需要同时考虑最大乘积和最小乘积
    dp_max[i]表示以第 i 个数字结尾的子数组的最大乘积
    dp_min[i]表示以第 i 个数字结尾的子数组的最小乘积
    遍历一遍数组,当第 i 个数字大于0时
    dp_max[i] = max(nums[i], nums[i] * dp_max[i - 1])
    dp_min[i] = min(nums[i], nums[i] * dp_min[i - 1])
    当第 i 个数字小于0时
    dp_max[i] = max(nums[i], nums[i] * dp_min[i - 1])
    dp_min[i] = min(nums[i], nums[i] * dp_max[i - 1])
    因为动规只与上一个状态相关,所以可以用滚动数组降维到0维,空间复杂度O(1),时间复杂度O(n)

  • #174 Dungeon Game
    需要计算的是到达终点时所需要的最小初始生命值
    这类题目应该倒推,从终点开始往回计算从某个位置开始走到终点所需要的到达这一点最小初始生命值
    dp[i][j]表示到达(i, j)坐标前最小需要多少生命值,从而能够支撑骑士从这一点开始走到终点

    vector<vector<int>> f(m + 1, vector<int> (n + 1, INT_MAX));
    f[m - 1][n] = 1, f[m][n - 1] = 1;
        
    for (int i = m - 1; i >= 0; --i) {
        for (int j = n - 1; j >= 0; --j) {
        	int need = min(f[i][j + 1], f[i + 1][j]) - dungeon[i][j];
        	f[i][j] = need <= 0 ? 1 : need;
        }
    }
    return f[0][0];
    
  • #542 01 Matrix
    二维矩阵动规,也可以用广搜做
    有四个方向需要更新状态,单向更新会缺失状态,所以需要做两遍
    从左上角到右下角做一遍,再从右下角到左上角做一遍
    感觉和 #135 Candy 是一类

  • #546 Remove Boxes
    一个数组里,去除中间连续的一段得到一个分数,去除后两端接起来产生一个新的状态
    这一类题没有办法用二维动规表示全部的状态,因此增加一维
    在这道题的设定下,增加第三维表示在[i, j]段左方有多少个与i相同的连续元素

    class Solution {
    public:
        int removeBoxes(vector<int>& boxes)
        {
            int dp[100][100][100] = {0};    // number of boxes would not exceed 100
            int n = boxes.size();
            return operate(boxes, 0, n-1, 0, dp);   // in the closed interval [0, n-1]
        }
    
        int operate(vector<int>& boxes, int i, int j, int k, int dp[100][100][100])
        {
            // [i, j] is the operating closed interval
            // k is the number of adjacent boxes on the left of boxes[i] with the same value
            if (i>j) return 0;
            if (dp[i][j][k]>0) return dp[i][j][k];
    
            // start with boxes[i] and interval [i+1, j]
            int res = (k+1)*(k+1) + operate(boxes, i+1, j, 0, dp);
            for (int m=i+1; m<=j; m++)
            {
                // if boxes[i]==boxes[m], we can thus eliminate interval [i+1, m-1]
                // to make boxes[i] and boxes[m] adjacent to have a higher score
                if (boxes[i]==boxes[m])
                    // if boxes[i] and boxes[m] are adjacent, then
                    // there will be k+1 boxes on the left of boxes[m] with the same value
                    res = max( res, operate(boxes, i+1, m-1, 0, dp)+operate(boxes, m, j, k+1, dp) );
            }
            dp[i][j][k] = res;
            return res;
        }
    };
    
  • #552 Student Attendance Record II
    多状态之间的互相转换,找到状态转移方程即可,不难,但很典型

  • #553 Optimal Division
    记忆化搜索,重点是搞明白怎么搜
    除法本质上可以分成两侧,左边除以右边,而其最大值发生在左侧最大右侧最小的时候,因此我们需要做的就是得到每个区间改变计算优先级之后的最大以及最小值
    #152 Maximum Product Subarray 本质上是一个类型,要善于发现题目的本质

  • #629 K Inverse Pairs Array
    求1-n这些数字能排列成的含有k个逆序对的序列的数量,记为dp[n][k],那么当增加一个新的数字n的时候,若要组成k个逆序对,考虑将n放在哪个位置,若放在第n位,则不会引入新的逆序对,这时有dp[n - 1][k]种序列,若放在第i位,则会引入n - i个逆序对,若还想组成k个逆序对,有dp[n - 1][k - (n - i)]种序列。
    总的来说,状态转移方程为dp[n][k] = dp[n - 1][k] + d[n - 1][k - 1] + ... + dp[n - 1][max(0, k - (n - 1))]

  • #630 Course Schedule III
    可以证明,如果一个上课序列成立,那么如果将这个序列按照课程结束时间进行重排列,这个序列依然成立
    因为如果st + dur_b < st + dur_b + dur_a <= ed_a <= ed_b,那么一定有st + dur_a < st + dur_a + dur_b <= ed_a <= ed_b,因此,我们只用考虑在课程结束时间这一维度上有序的上课序列,也就相当于只考虑每节课上或者不上,搜索时间复杂度从全排列的n!降为2^n,同时,这种形式的搜索可以转化为二维DP,以dp[st][k]表示从st时刻开始上课,从第k节课开始选,能够完成的最多课程数
    这个方法时间复杂度上还是略高的,进一步分析,可以把这道题转化为一个贪心问题——尽可能选择每一节课,如果出现了选择了第i节课后会超出其结束时间的情况,就从前i节课中已选的课程中放弃一门持续时间最长的课,这样一来,就可以保证选择的课程数量没有减少,但最优化了上完已选课程所需要的总时间

  • #23 Merge k Sorted Lists
    维护一个由所有list的头结点组成的小根堆,堆里每个元素要同时记录值与所属的list

  • #1019 Next Greater Node In Linked List
    维护一个栈,保持栈里的元素是还没有找到next greater node的元素,那么遍历时见到的第一个大于他的元素就一定是next greater node。
    遇到新元素的时候,让栈顶所有比新元素小的node出栈,并把他们的next greater node标记为新元素,最后将新元素入栈。
    同理 #503 Next Greater Element II

  • #84 Largest Rectangle in Histogram
    只需要遍历所有直方柱,寻找以这个直方柱为高,可向左右扩展出的最大长方形面积,那么我们只需要找到对于每个 i ,其左/右边第一个比他矮的直方柱的下标,相当于就找到了以第 i 个直方柱为高,可向左右扩展的最远距离。
    寻找左右第一个比h[i]小的元素,可以使用上面的#1019的方法,如此一来只需要O(n)的时间复杂度

  • #85 Maximal Rectangle
    相当于#84的扩展形式,依次把每一行当做底,每个位置的直方柱高相当于是从该位置向上延伸最大的连续1的个数。

  • #150 Evaluate Reverse Polish Notation
    遍历数组,若是数字则入栈,若是运算符,则出栈两个数字,计算结果后将结果入栈

  • #155 Min Stack
    在实际的元素栈之外,维护一个min stack,将当前栈内元素的最小值入栈
    当元素栈进行pop操作时,min stack也进行pop,当元素栈push时,min stack也进行push,但push的是当前最小的元素(可以对比本次入栈元素与之前最小的元素)

链表

  • #25 Reverse Nodes in k-Group
    反转链表,记录3个指针即可
    如果复杂度允许,可以递归,或者用栈

  • #138 Copy List with Random Pointer
    因为有一个随机指针的存在,可能在复制时会多次访问同一个节点,所以需要额外用一个hash map记录所有已经复制过的节点,类似记忆化搜索

  • #142 Linked List Cycle II
    判断链表中是否有环:
    使用两个指针fast、slow,分别以2、1的前进速度从链表头开始遍历,若fast先遇到链表尾,则不含环,若两个指针相遇,说明存在环
    证明:
    当slow进入环时,fast必然已经在环的某一位上,此时开始fast开始在环上相对slow以1的速度运动,因此二者必然在slow绕环一周前相遇

    判断链表中环的起点:
    先用上面的方法找到fast与slow的相遇点p,使用两个指针a、b分别从链表头、相遇点p开始以相同速度开始遍历,则a、b相遇的位置为环的起点
    证明:
    假设链表非环段长为x,整个环长为c,当slow刚进入环时,fast已在环内运动距离为x(因为fast速度为slow的二倍,而此时slow移动了x),因此此时fast的位置相对于环起点(也就是slow目前的位置)为x % c,fast在移动方向上距离slow为c - x % c,由于fast相对于slow移动速度为1,因此再经过c - x % c次移动,fast与slow相遇,此时slow位置相对于环起点为c - x % c,也就是fast、slow相遇位置,从这一点再向前进x个位置的话,相对于环起点位置为(c + x - x % c) % c = 0

  • #146 LRU Cache
    因为是Least Recently Used,所以用链表记录最近访问是最有效的,当某一个键值被访问时,将他在链表中的位置更新到表头,cache满时,将表位的键值删除。但由于链表的访问时间复杂度为O(n),因此需要额外用一个hash map记录每个键值在链表中的位置(iterator)

  • #148 Sort List
    链表排序用插入(O(n^2))或者归并(O(nlogn))

数组

  • #41 First Missing Positive
    忽略大于数组长度n的正数,以及全部的非正数,将剩余的数字填入与其值对应的数组位置中,最后只需要遍历一遍数组,返回第一个下标不等于值的数字

  • #88 Merge Sorted Array
    若可以使用额外空间,直接归并就可以,如果要求归并到其中一个长度为m + n的数组,可以从后向前归并

  • #135 Candy
    从前向后扫一遍,保证ratings[i] > ratings[i - 1]的也满足candy[i] > candy[i - 1]
    从后向前扫一遍,保证ratings[i] > ratings[i + 1]的也满足candy[i] > candy[i + 1]

  • #164 Maximum Gap
    题目要求是找到无序数组的有序排列中相邻元素的最大差值
    对于求最大Gap的题目,即使是大数据的题目,也可以使用分桶的方法来做
    桶的数量必须大于元素的数量,这样一来,将所有元素放入对应的桶中,必然存在至少一个桶为空
    最大的gap必然出现在连续的空桶两侧的两个桶内的元素之间,而不会出现在某个同内的元素之间(因为桶内元素间的Gap最大超不过桶宽,而桶间元素的差值可以超过这一值)
    随后我们遍历一遍所有的桶,若存在某一个区间内的桶均为空,则找到该区间左侧第一个非空桶的最大值,以及该区间右侧第一个非空桶的最小值,这两个值的差即是最大Gap的一个候选值

  • #169 Majority Element
    经典题目,要找出出现次数超过 n / k 的元素,记录 k - 1 个候选值(不可能出现超过k个出现次数大于 n / k 的元素),每个候选值对应一个计数器,遍历整个数组,若元素等于某个候选值,则候选值计数器加一,否则减一,若计数器小于0,则将对应的候选值变更为新元素,计数器初始化为 k - 1
    这里的原理是,如果从整个数组中消去1个候选元素以及k - 1个其他元素,那么若候选元素在之前是符合要求的元素,他在消去k个元素后的数组中仍然会符合要求,因为(x - 1) / (n - k) > x / n > 1 / k
    最后只需要确认一下各个候选值是不是真的符合要求即可

  • #189 Rotate Array
    题目要求空间复杂度为O(1)
    可以先将前半段和后半段分别翻转,再整体翻转,这样需要2次遍历

哈希

  • #523 Continuous Subarray Sum
    计算数组0…i - 1的和sum[i],并将sum[i]模k的结果存入哈希表中,之后只要发现新的sum[j] % k的值在哈希表中出现过,就说明(sum[j] - sum[i]) % k == 0,相当于sum(a[i]...a[j]) = n * k
    这道题有非常多的边界条件,需要非常小心

  • #525 Contiguous Array
    和上面的#523属于同一种类型,即:当累加和重复出现某一模式时,说明两次重复之间的区间符合某一条件。这种题都可以考虑用哈希去记录累加和的模式,然后在遍历中用哈希确认模式是否重复出现

    int findMaxLength(vector<int>& nums) {
        unordered_map<int, int> mp;
        int ans = 0;
        mp[0] = -1;
        for (int i = 0, sum = 0; i < nums.size(); ++i) {
            sum += nums[i] == 1 ? 1 : -1;
            if (mp.find(sum) != mp.end()) ans = max(ans, i - mp[sum]);
            else mp[sum] = i;
        }
        return ans;
    }
    
  • #560 Subarray Sum Equals K
    和#523与#525一个类型,如果这道题规定所有数字都是正整数的话,可以用双滑动指针的方法来做,但是这道题涉及到负数,这种方法就不适用了
    要统计有多少连续子数组满足某一个模式,都可以使用类似的方法

  • #128 Longest Consecutive Sequence
    先将所有元素加入哈希表,然后遍历每个元素num,以其为中心向左右扩张连续区间:
    若扩张到的元素存在于哈希表内,说明数组中有这个元素,可以扩张,扩张后将该元素从哈希表中删除,避免重复使用
    若扩张到的元素不在哈希表内,说明数组中没有这个元素,不能扩张,扩张停止
    这样一来,每个元素只会入表一次、出表一次,时间复杂度是O(n)的

    class Solution {
    public:
        int longestConsecutive(vector<int> &num) {
            if (num.size() == 0) return 0;
            unordered_set<int> record(num.begin(),num.end());
            int res = 1;
            for(int n : num){
                if(record.find(n)==record.end()) continue;
                record.erase(n);
                int prev = n-1,next = n+1;
                while(record.find(prev)!=record.end()) record.erase(prev--);
                while(record.find(next)!=record.end()) record.erase(next++);
                res = max(res,next-prev-1);
            }
            return res;
        }
    };
    
  • #149 Max Points on a Line
    用向量表示直线,将同一方向的向量表示成同一形式(比如令(x, y)的两个坐标互质),然后用hash map统计同一向量出现的次数即可
    要注意重复点和垂直坐标轴的直线

  • #94 Binary Tree Inorder Traversal
    递归遍历的话,没有什么难度,主要记一下非递归遍历的方法,用栈来辅助
    另外两道,先序遍历 #144 Binary Tree Preorder Traversal,后序遍历 #145 Binary Tree Postorder Traversal 也是一样

    class Solution {
    public:
    	vector<int> postorderTraversal(TreeNode* root) {
    		stack<TreeNode *> stk;
            vector<int> res;
            if (!root) return res;
            
            TreeNode *ptr = root;
            while (!stk.empty() || ptr != NULL) {
            	if (ptr != NULL) {
            		stk.push(ptr);
            		res.push_back(ptr->val);
            		ptr = ptr->right;
    			}
    			else {
    				ptr = stk.top()->left;
    				stk.pop();
    			}
    		}
    		reverse(res.begin(), res.end());
    		return res;
        }
    	
    	vector<int> inorderTraversal(TreeNode* root) {
    		stack<TreeNode *> stk;
            vector<int> res;
            if (!root) return res;
            
            TreeNode *ptr = root;
            while (!stk.empty() || ptr != NULL) {
            	if (ptr != NULL) {
            		stk.push(ptr);
            		ptr = ptr->left;
    			}
    			else {
    				TreeNode *node = stk.top();
    				stk.pop();
    				res.push_back(node->val);
    				ptr = node->right;
    			}
    		}
    		return res;
    	}
    	
        vector<int> preorderTraversal(TreeNode* root) {
            stack<TreeNode *> stk;
            vector<int> res;
            if (!root) return res;
            
            TreeNode *ptr = root;
            while (!stk.empty() || ptr != NULL) {
            	if (ptr != NULL) {
            		stk.push(ptr);
            		res.push_back(ptr->val);
            		ptr = ptr->left;
    			}
    			else {
    				ptr = stk.top()->right;
    				stk.pop();
    			}
    		}
    		return res;
        }
    };
    
  • #99 Recover Binary Search Tree
    若变换BST中两个元素的位置,则在其中序遍历中,会有一个元素比其前后元素都小,另一个元素比其前后元素都大,这两个元素就是被调换的元素,把他们找出来就好了
    可以在中序遍历时记录连续三个元素的值(实际实现中只需要记录前两个,正在遍历的元素就是第三个),比较中间值和两端值的大小

  • #105 Construct Binary Tree from Preorder and Inorder Traversal
    递归,先序序列中的第一个节点是根节点,在中序序列中找到这个根节点的位置,则根节点左边的序列是左子树,右边的序列是右子树,进一步可以确定左右子树的序列长度,最后把问题划分为左右子树两个子问题。
    类似的还有 #106 Construct Binary Tree from Inorder and Postorder Traversal ,把先序序列换成后序序列,本质方法是一样的

  • #126 Word Ladder II
    把每个word作为一个点,如果word之间可以只改变一个字符就能够相互转换,则两个word之间连接一条边,随后只需要寻找从beginWord到endWord间的最短路径,可用Dijkstra算法。
    若要记录所有最短路径,需要为每一个点记录源节点到他的所有最短路径,在更新某一个节点 i 的最短路时,若发现路径从某个节点 k 到 i 的路径长度等于其最短路dist[i],则将源节点到节点 k 的所有路径(为这些路径添加一个新节点 i )添加到源节点到节点 i 的最短路中。

深度优先搜索

  • #1020 Number of Enclaves
    在矩阵外添加一圈陆地(可访问),然后Flood Fill搜索所有与外围陆地相连的陆地,用总的陆地数量减去floodfill结果,就可以得到孤岛面积

  • #200 Number of Islands
    同上,Flood Fill

广度优先搜索

  • #199 Binary Tree Right Side View
    树的层次遍历,用广搜
    需要顺便记录层数,从而在输出的时候保证只输出没一层第一个/最后一个元素

  • #542 01 Matrix
    矩阵中找到每个1最邻近的0,相当于岛上到海最近到距离,从岛开始搜的话,广搜每次只能搜一个点,因为广搜没办法更新中间节点,而深搜则复杂度不能接受。因此可以从海开始搜(因为海不需要更新,岛需要更新),并且初始加入队列的节点是所有的海面节点,而不是单独的一个。

数学问题

  • #1017 Convert to Base -2
    负基数进制转换问题。仍使用短除法,若余数为正,则和正基数一样处理,若余数为负,将余数修正为 remainder + (-negtiveBase),同时把商加一
    class Solution {
    public:
        string baseNeg2(int n) {
            if (n == 0) return "0"; 
            
            string res; 
            int negBase = -2;
            
            while (n != 0) { 
                int remainder = n % negBase; 
                n /= negBase; 
                if (remainder < 0) 
                { 
                    remainder += (-negBase); 
                    n += 1; 
                } 
                res = to_string(remainder) + res; 
            } 
    
            return res; 
        }
    };
    
  • #172 Factorial Trailing Zeroes
    阶乘结尾的0的数目实际上就是所有乘子的因子里2、5对的数量
    而2出现的次数显然比5要多
    因此只需要统计所有乘子的因子中5的数量即可
    一种比较高效的实现方式
    for (int i = 5; n / i > 0; i *= 5) count += n / i;
    n / i 的结果是 1 - n 中 i 的倍数的个数,而所有5的倍数贡献1个5,所有25的倍数贡献2个5…以此类推

位运算

  • #136 Single Number
    所有数字按位异或,因为异或操作改变顺序不会影响结果,所以每2个相等的数字相互异或得到0,最后0与单一的数字异或得到单一数字本身

  • #137 Single Number II
    如果是出现两次的话,用一个bit就可以
    比如 b,初始为0
    当5第1次出现时, b=5
    当5第2次出现是, b清空为0,表示b可以去处理其他数字了
    所以,最后 如果 b !=0的话,b记录的就是只出现了一次的那个数字

    公式就是 b = b xor i 或者 b = b^i

    那么,如果是三次的话,香浓定理,需要用2bits进行记录

    当5第一次出现的时候,b = 5, a=0, b记录这个数字
    当5第二次出现的时候,b = 0, a=5, a记录了这个数字
    当5第三次出现的时候,b = 0, a=0, 都清空了,可以去处理其他数字了
    所以,如果有某个数字出现了1次,就存在b中,出现了两次,就存在a中,所以返回 a|b

    公式方面 ,上面两次的时候,b清空的公式是 b = b xor i
    而第三次时,b要等于零,而这时a是True,所以再 & 一个a的非就可以,b = b xor & ~a

    b = b xor i & ~a
    a = a xor i & ~b

  • #191 Number of 1 Bits
    消除末尾的1的位运算:n = n & (n - 1)

  • #201 Bitwise AND of Numbers Range
    对m-n之间所有数字按位AND操作会使得m-n之间所有数字都相同的位保持不变,而m-n之间有任何两个数字不同的位变为0,因此我们只需要保持m-n之间所有数字都相同的高位不变,低位全部置为0就可以得到答案。
    由于m和n分别是连续的这一串数字中最大和最小的数,因此m与n在高位相同的数位就是m-n之间所有数字全部一致的位(这部分若有任一位被置1,则会大于n,若有任意一位被置0,则会小于m,所以m-n之间所有数字的这部分数位都是一样的),余下的位置0即可

变换打印顺序

  • #6 ZigZag Conversion
    利用行数限制,判断打印方向

高精度计算

  • #2 Add Two Numbers
    高精度加法

  • #29 Divide Two Integers
    高精度除法

  • #43 Multiply Strings
    高精度乘法

特殊数字顺序

  • #31 Next Permutation
    对于任意若干数字,其排列数的最后一种应该是降序排列的,这一规律是递归出现的。
    若一数列的最低k位不是降序,则目前的排列一定还在这k位内进行;
    若一数列的最后k位是降序,则这k位已经遍历了所有的排列数;
    当数列的最后k位已遍历完所有排列数时,就要开始排列倒数第k-1位,此时的做法是将降序的num[-k:]数列中大于且最接近于num[-(k - 1)]的数字与num[-(k - 1)]交换位置,并将num[-k:]升序排列

  • #60 Permutation Sequence
    依次计算1…n个数的全排列数,若发现A(n, n) > k,且A(n - 1, n - 1) < k,说明第k个排列序列发生在n个数的排列中,之后判断第k个排列序列发生在n个数排列中最高位为多少的排列、再判断次高位,依次类推,确定排列序列中所有的值。

你可能感兴趣的:(算法/数据结构基础)