《剑指offer》题解

文章目录

    • 说明
    • 剑指 Offer 03. 数组中重复的数字
    • 剑指 Offer 04. 二维数组中的查找
    • 剑指 Offer 05. 替换空格
    • 剑指 Offer 06. 从尾到头打印链表
    • 剑指 Offer 07. 重建二叉树
    • 剑指 Offer 09. 用两个栈实现队列
    • 剑指 Offer 10- I. 斐波那契数列【1】
    • 剑指 Offer 10- II. 青蛙跳台阶问题【1】
    • 剑指 Offer 11. 旋转数组的最小数字
    • 剑指 Offer 12. 矩阵中的路径
    • 剑指 Offer 14- I. 剪绳子
    • 剑指 Offer 14- II. 剪绳子 II
    • 剑指 Offer 15. 二进制中1的个数
    • 剑指 Offer 16. 数值的整数次方
    • 剑指 Offer 17. 打印从1到最大的n位数
    • 剑指 Offer 18. 删除链表的节点
    • 剑指 Offer 19. 正则表达式匹配
    • 剑指 Offer 20. 表示数值的字符串【1】
    • 剑指 Offer 21. 调整数组顺序使奇数位于偶数前面
    • 剑指 Offer 22. 链表中倒数第k个节点
    • 剑指 Offer 24. 反转链表
    • 剑指 Offer 25. 合并两个排序的链表
    • 剑指 Offer 26. 树的子结构
    • 剑指 Offer 27. 二叉树的镜像
    • 剑指 Offer 28. 对称的二叉树
    • 剑指 Offer 29. 顺时针打印矩阵
    • 剑指 Offer 30. 包含min函数的栈
    • 剑指 Offer 31. 栈的压入、弹出序列
    • 剑指 Offer 32 - I. 从上到下打印二叉树
    • 剑指 Offer 32 - II. 从上到下打印二叉树 II
    • 剑指 Offer 32 - III. 从上到下打印二叉树 III
    • 剑指 Offer 33. 二叉搜索树的后序遍历序列
    • 剑指 Offer 34. 二叉树中和为某一值的路径
    • 剑指 Offer 35. 复杂链表的复制
    • 剑指 Offer 36. 二叉搜索树与双向链表
    • 剑指 Offer 37. 序列化二叉树【0】
    • 剑指 Offer 38. 字符串的排列
    • 剑指 Offer 39. 数组中出现次数超过一半的数字
    • 剑指 Offer 40. 最小的k个数
    • 剑指 Offer 41. 数据流中的中位数【0】
    • 剑指 Offer 42. 连续子数组的最大和
    • 剑指 Offer 43. 1~n 整数中 1 出现的次数【0】
    • 剑指 Offer 44. 数字序列中某一位的数字【0】
    • 剑指 Offer 46. 把数字翻译成字符串
    • 剑指 Offer 47. 礼物的最大价值
    • 剑指 Offer 48. 最长不含重复字符的子字符串
    • 剑指 Offer 49. 丑数
    • 剑指 Offer 50. 第一个只出现一次的字符
    • 剑指 Offer 51. 数组中的逆序对【1】
    • 剑指 Offer 52. 两个链表的第一个公共节点
    • 剑指 Offer 53 - I. 在排序数组中查找数字 I
    • 剑指 Offer 53 - II. 0~n-1中缺失的数字
    • 剑指 Offer 54. 二叉搜索树的第k大节点
    • 剑指 Offer 55 - I. 二叉树的深度
    • 剑指 Offer 55 - II. 平衡二叉树
    • 剑指 Offer 56 - I. 数组中数字出现的次数
    • 剑指 Offer 56 - II. 数组中数字出现的次数 II【1】
    • 剑指 Offer 57. 和为s的两个数字
    • 剑指 Offer 57 - II. 和为s的连续正数序列
    • 剑指 Offer 58 - I. 翻转单词顺序
    • 剑指 Offer 58 - II. 左旋转字符串
    • 剑指 Offer 59 - I. 滑动窗口的最大值
    • 剑指 Offer 60. n个骰子的点数
    • 剑指 Offer 62. 圆圈中最后剩下的数字
    • 剑指 Offer 63. 股票的最大利润
    • 剑指 Offer 64. 求1+2+…+n
    • 剑指 Offer 65. 不用加减乘除做加法
    • 剑指 Offer 66. 构建乘积数组
    • 剑指 Offer 64. 求1+2+…+n
    • 面试题13. 机器人的运动范围
    • 面试题45. 把数组排成最小的数
    • 面试题59 - II. 队列的最大值
    • 面试题61. 扑克牌中的顺子

说明

0-还未做,先略过

1-待尝试其它方法

剑指 Offer 03. 数组中重复的数字

剑指 Offer 03. 数组中重复的数字

【数组,元素&索引的对应关系】

题干:

给定一个长度为n的数组nums,里面的所有数字都在 [ 0 , n − 1 ] [0,n-1] [0,n1]内,找出数组中任意一个重复数字并返回。

数据范围:2<=n<=1e5

函数签名:

int findRepeatNumber(vector<int>& nums);

解:

  • 方法1:哈希
    • 时间复杂度 O ( n ) O(n) O(n),空间复杂度 O ( 1 ) O(1) O(1)
  • 方法2:元素与索引一一对应
    • 对于这种数组元素在 [ 0 , n − 1 ] [0,n-1] [0,n1]范围的问题,可以将值为i的元素调整到第i个位置上进行求解;在调整过程中,如果第i个位置上已经有一个值为i的元素,就可以知道i值重复了。
    • 原地修改,时间复杂度 O ( n ) O(n) O(n),空间复杂度 O ( 1 ) O(1) O(1)

for循环版:

int findRepeatNumber(vector<int>& nums) {
        for(int i = 0; i< nums.size(); i++){
            while(nums[i]!= i){
                if(nums[nums[i]] == nums[i]){
                    return nums[i];
                }
                swap(nums[i], nums[nums[i]]);
            }
        }
        return -1;
    }

while循环版:

int findRepeatNumber(vector<int>& nums) {
        int i = 0;
        while(i < nums.size()){
            if(nums[i] == i){
                ++i;
                continue;
            }
            if(nums[nums[i]] == nums[i]) return nums[i];
            swap(nums[i], nums[nums[i]]);
        }
        return -1;
    }

剑指 Offer 04. 二维数组中的查找

剑指 Offer 04. 二维数组中的查找

【二分&抽象BST】

题干:给定一个m*n的二维数组,每行按从左到右非递减,每列从上到下非递减的顺序排序。给定一个整数target,判断数组中是否含有该整数。

数据范围:0<=m,n<=1e3

函数签名:

bool findNumberIn2DArray(vector<vector<int>>& matrix, int target)

解:

参考

  • 方法1:二分
    • 遍历每行,对每列二分,判断能否找到target
    • 时间复杂度 O ( m l o g n ) O(mlogn) O(mlogn)(若遍历列二分行则 O ( n l o g m ) O(nlogm) O(nlogm)),空间复杂度 O ( 1 ) O(1) O(1)
bool findNumberIn2DArray(vector<vector<int>>& matrix, int target) {
        if(!matrix.size() || !matrix[0].size()) return false;
        int m = matrix.size(), n = matrix[0].size();
        for(int i = 0; i < m; i++){
            int left = 0, right = n;
            int index = 0;
            while(left < right){
                int mid = left + ((right - left) >> 1);
                if(matrix[i][mid]== target) return true;
                else if(matrix[i][mid] > target) right = mid;
                else left = mid+1;
                
            }    
        }
        return false;
    }
  • 方法2:抽象BST
    • 该题的matrix从左到右递增,从上到下递增,所以左上角是最小元素,右下角是最大元素。若想高效在matrix中搜索一个元素,肯定要从某个角开始(如左上角),每次只能向右或向下移动,不要走回头路。
      • 但左上角无论向右还是向下,元素值都会增加;右下角开始同理
      • 所以从右上角或左下角开始就可以了。如规定从右上角开始,每次只能向左或向下移动,这样就能根据当前位置的元素和target的大小关系判断如何移动,不断接近从而找到target的位置
    • 其实这就是一棵以右上角为根节点的BST
      • 若target<当前节点,搜索当前节点的左子树,即col–;
      • 若target>当前节点,搜索当前节点的右子树,即row++。
    • 时间复杂度 O ( m + n ) O(m+n) O(m+n),空间复杂度 O ( 1 ) O(1) O(1)
bool findNumberIn2DArray(vector<vector<int>>& matrix, int target) {
        if(!matrix.size() || !matrix[0].size()) return false;
        int m = matrix.size(), n = matrix[0].size();
        int i = 0, j = n-1;
        while(i < m && j >= 0){
            if(target == matrix[i][j]) return true;
            else if(target < matrix[i][j]) j--;
            else i++;
        }
        return false;
    }

剑指 Offer 05. 替换空格

剑指 Offer 05. 替换空格

【双指针】

题干:把字符串s的每个空格替换成"%20"

数据范围:0<=s.length()<=1e5

函数签名:

string replaceSpace(string s);

解:

  • 方法1:另开空间
    • 另开一个string变量,遍历s,遇到空格时往里添加"%20",遇到非空格时添加s[i]。
    • 时间复杂度 O ( n ) O(n) O(n),空间复杂度 O ( 1 ) O(1) O(1)
    string replaceSpace(string s) {
        string res;
        int j = 0;
        for(char c: s){
            if(c == ' ') res+="%20";
            else res += c;
        }
        return res;
    }
  • 方法2:双指针,原地修改
    • 每替换一个空格,字符串长度增加2,则可以先统计s中空格的总数,就可计算出替换后的字符串的总长度(s.length()+2
    • 从字符串的末尾开始复制和替换,实现原地修改。
      • 双指针i,j,一个指向原字符串末尾,一个指向替换后的字符串末尾,根据s[i]是否等于空格进行复制或替换操作
      • 若空格都替换完了,则i==j,可退出循环
    • 时间复杂度 O ( n ) O(n) O(n),空间复杂度 O ( 1 ) O(1) O(1)
    string replaceSpace(string s) {
        int cnt = 0;
        for(char c: s){
            if(c == ' ') ++cnt;
        }
        int len = s.size();
        s.resize(len + 2*cnt);
        int i = len-1, j = s.size()-1;
        while(i != j){
            if(s[i] != ' '){
                s[j] = s[i];
                i--, j--;
            }
            else{
                s[j--] = '0';
                s[j--] = '2';
                s[j--] = '%';
                i--;
            }
        }
        return s;
    }

剑指 Offer 06. 从尾到头打印链表

剑指 Offer 06. 从尾到头打印链表

【递归,二叉树后序遍历】

题干:输入一个链表的头节点,从尾到头反过来返回每个节点的值。

数据范围:0<=链表长度<=1e5

函数签名:

vector<int> reversePrint(ListNode* head)

解:

  • 方法:递归
    • 类似二叉树的后序遍历。
    • 对于每个节点,先访问(打印)它的子节点的值,再打印该节点的值,所以是在后序位置写打印语句。
    • 时间复杂度 O ( n ) O(n) O(n),空间复杂度 O ( n ) O(n) O(n)
    vector<int> reversePrint(ListNode* head) {
        vector<int> res;
        printLink(head, res);
        return res;
    }
    void printLink(ListNode* head, vector<int>& res){
        if(!head) return;
        printLink(head->next, res);
        res.push_back(head->val);
    }

剑指 Offer 07. 重建二叉树

剑指 Offer 07. 重建二叉树

【递归】

题干:通过二叉树的前序序列和中序序列重建二叉树

数据范围:0<=节点个数<=5e3

函数签名:

TreeNode* buildTree(vector<int>& preorder, vector<int>& inorder)

解:

  • 方法:递归,前序位置建根节点。见之前的文章。
    • 时间复杂度 O ( n ) O(n) O(n),空间复杂度 O ( n ) O(n) O(n)
      • 时间:初始化hashMap占用 O ( n ) O(n) O(n);递归建立n个节点,每层递归中的节点建立、搜索操作占用 O ( n ) O(n) O(n)。因此使用 O ( n ) O(n) O(n)时间。
      • 空间:hashMap使用 O ( n ) O(n) O(n)额外空间;最差情况下(二叉树为链表时),递归深度 O ( n ) O(n) O(n),占用 O ( n ) O(n) O(n)的栈帧空间。因此总共使用 O ( n ) O(n) O(n)空间
  • 注意条件:不含重复元素。
	unordered_map<int, int> mp;
    TreeNode* buildTree(vector<int>& preorder, vector<int>& inorder) {
        for(int i = 0; i < inorder.size(); i++) mp[inorder[i]] = i;
        int n = preorder.size();
        return build(preorder, 0, n-1, inorder, 0, n-1);
    }
    TreeNode* build(vector<int>& preorder, int preL, int preR, vector<int>& inorder, int inL, int inR){
        if(preL > preR) return nullptr;
        int rootVal = preorder[preL];
        TreeNode* root = new TreeNode(rootVal);
        int inRootIndex = mp[rootVal];
        int leftSize = inRootIndex - inL;
        root->left = build(preorder, preL+1, preL+1 + leftSize-1, inorder, inL, inRootIndex-1);
        root->right = build(preorder, preL+ leftSize + 1, preR, inorder, inRootIndex+1, inR);
        return root;
    }

剑指 Offer 09. 用两个栈实现队列

剑指 Offer 09. 用两个栈实现队列

【模拟】

题干:用两个栈实现队列的push和pop操作(pop不成功返回-1)

数据范围:1<=val<=1e4,最多进行1e4次操作

函数签名:

class CQueue {
public:
    CQueue() {}
    
    void appendTail(int value) {
    }
    int deleteHead() {
    }
};

解:

  • 方法:
    • “in 栈用来处理入栈(push)操作,out 栈用来处理出栈(pop)操作。一个元素进入 in 栈之后,出栈的顺序被反转。当元素要出栈时,需要先进入 out 栈,此时元素出栈顺序再一次被反转,因此出栈顺序就和最开始入栈顺序是相同的,先进入的元素先退出,这就是队列的顺序”(来源)。
class CQueue {
private:
    stack<int> st1, st2;
public:
    CQueue() {}
    
    void appendTail(int value) {
        st1.push(value);
    }
    int deleteHead() {
        if(!st2.empty()){
            int val = st2.top();
            st2.pop();
            return val;
        }
        while(!st1.empty()){
            int val = st1.top();
            st1.pop();
            st2.push(val);
        }
        if(st2.empty()) return -1;
        int val = st2.top();
        st2.pop();
        return val;
    }
};

剑指 Offer 10- I. 斐波那契数列【1】

剑指 Offer 10- I. 斐波那契数列

【递归,递推,矩阵快速幂(待整理)】

题干:求斐波那契数列(f[0]=0,f[1]=1, f[n]=f[n-1]+f[n-2]),并对答案取模(1e9+7)

数据范围:0<=n<=100

函数签名:

int fib(int n);

解:

  • 方法1:递归+记忆化搜索
    const int mod = 1e9+7;
    static const int N = 100;
    int f[N+2];
    int fib(int n){
        if(n == 0) return 0;
        if(n == 1) return 1;
        if(f[n]) return f[n];
        f[n] = (fib(n-1) + fib(n-2)) % mod;
        return f[n];
    }
  • 方法2:递推
    const int mod = 1e9+7;
    static const int N = 100;
    int f[N+2];
    int fib(int n) {
        int f[n+2];
        f[0] = 0;
        f[1] = 1;
        for(int i = 2; i <= n; i++){
            f[i] = (f[i-1] + f[i-2]) % mod;
        }
        return f[n];
    }

剑指 Offer 10- II. 青蛙跳台阶问题【1】

剑指 Offer 10- II. 青蛙跳台阶问题

【递归,递推,矩阵快速幂(待整理)】

题干:青蛙往上跳台阶,一次可以跳1级也可以跳2级,求青蛙跳上一个n级的台阶共有多少种跳法,答案对1e9+7取模。

数据范围:0<=n<=100

函数签名:

int numWays(int n);

解:

  • 方法1:递归+记忆化搜索
    const int mod = 1e9+7;
    static const int N = 100;
    int dp[N+5];
    int numWays(int n) {
        if(n == 0) return 1;
        if(n == 1) return 1;
        if(dp[n]) return dp[n];
        dp[n] = (numWays(n-1) + numWays(n-2)) % mod;
        return dp[n];
    }
  • 方法2:递推
    const int mod = 1e9+7;
    static const int N = 100;
    int dp[N+5];
    int numWays(int n){
        dp[0]=1;
        dp[1]=1;
        for(int i = 2; i<= n; i++){
            dp[i] = (dp[i-1] + dp[i-2]) % mod;
        }
        return dp[n];
    }
  • 优化:空间压缩
    const int mod = 1e9+7;
    int numWays(int n){
        int dp_0 = 1, dp_1 = 1;
        for(int i = 2; i<= n; i++){
            int dp_2 = (dp_0 + dp_1) % mod;
            dp_0 = dp_1;
            dp_1 = dp_2;
        }
        return dp_1;
    }

剑指 Offer 11. 旋转数组的最小数字

剑指 Offer 11. 旋转数组的最小数字

【二分】

题干:给定一个不降序数组numbers(里面可以有相同元素),现将numbers k k k位置( k k k未知)旋转(即将从 k k k开始到末尾的部分移到最前面),要求在numbers中寻找到最小的元素并返回元素值。

数据范围:1<=numbers.length<=5e3, -5e3<=numbers[i]<=5e3

函数签名:

int minArray(vector<int>& numbers);

解:

  • 方法:二分,见之前的文章。
    int minArray(vector<int>& numbers) {
        int left = 0, right = numbers.size()-1;
        if(left == right) return numbers[0]; // boundary
        while(numbers[left] == numbers[right] && (left != right)) left++;
        int target = numbers[right];
        right++;
        int ans=0;
        while(left < right){
            int mid = left + ((right - left)>>1);
            if(numbers[mid]<=target){
                ans = numbers[mid];
                right = mid;
            }
            else left = mid + 1;
        }
        return ans;
    }

剑指 Offer 12. 矩阵中的路径

剑指 Offer 12. 矩阵中的路径

【DFS回溯】

题干:给定一个二维字符网格board和一个字符串word,判断word是否在board中存在,存在返回true,不存在返回false。

数据范围:1<=m,n<=6, 1<=word.length()<=15

函数签名:

bool exist(vector<vector<char>>& board, string word);

解:

  • 方法:DFS递归+回溯
    • 使用vis数组标记,防止重复搜索
    bool exist(vector<vector<char>>& board, string word) {
        m = board.size(), n = board[0].size();
        for(int i = 0; i < m;i ++){
            for(int j = 0; j < n; j++){
                if(board[i][j] == word[0]){
                    if(dfs(board, word, i, j, 0)) return true;
                }
            }
        }
        return false;
    }
private:
    bool vis[10][10];
    int m, n;
    int dx[4] = {-1, 1, 0, 0};
    int dy[4] = {0, 0, -1, 1}; 
    bool dfs(vector<vector<char>>& board, string word, int x, int y, int pos){
        if(pos == word.size()) return true;
        if(x<0 || x >= m || y<0 || y >= n){
            return false;
        }
        if(vis[x][y] || word[pos] != board[x][y]) return false;
        vis[x][y] = 1;
        for(int i = 0; i<= 3; i++){
            int newX = x + dx[i];
            int newY = y + dy[i];
            if(dfs(board, word, newX, newY, pos+1)){
                return true;
            }
        }
        vis[x][y] = 0;
        return false;
    }

剑指 Offer 14- I. 剪绳子

剑指 Offer 14- I. 剪绳子

【DP,贪心+快速幂】

题干:给定一根长度为n的绳子,把它剪成整数长度的m段(m必须大于1),问这m的乘积最大可能是多少?

数据范围:2<=n<=58

函数签名:

int cuttingRope(int n);

解:

  • 方法:dp
    • 设长度为n的绳子cut的最大乘积为dp(n),
  • 方法1:递归+记忆化搜索
    int cuttingRope(int n){
        return dp(n);
    }
private:
    int memo[60];
    int dp(int n){
        if(n==0 || n==1) return 0;
        if(memo[n]) return memo[n];
        for(int i = 1; i < n; i++){
            memo[n] = max(memo[n], max(i*(n-i), i*dp(n-i)));
        }
        return memo[n];
    }
  • 方法2:递推
    int cuttingRope(int n){
        vector<int> dp(n+2);
        for(int i = 2; i <= n; i++){
            for(int j = 1; j < i; j++){
                dp[i] = max(dp[i], max(j*(i-j), j*dp[i-j]));
            }
        }
        return dp[n];
    }

剑指 Offer 14- II. 剪绳子 II

剑指 Offer 14- II. 剪绳子 II

【贪心+快速幂】

解:

  • 方法:贪心
    • 要取模,DP要比较最大值,所以不行
    • 一个整数n切分(至少切分成两段),使得乘积最大:尽可能多的3,不行就换2
      • 数学证明略
    const int p = 1e9+7;
    long long qmod(long long a, long long b){
        long long res = 1;
        while(b){
            if(b&1) res = res * a %p;
            a = a * a %p;
            b>>=1;
        }
        return res;
    }
    int cuttingRope(int n) {
        if(n == 2) return 1;
        if(n == 3) return 2;
        int cnt = n/3;
        int mod = n%3;
        int ans = 1;
        if(!mod){
            ans = qmod(3ll, cnt) %p;
        }
        else if(mod == 1){
            ans = qmod(3ll, cnt-1) * 4ll % p;
        }
        else{
            ans = qmod(3ll, cnt) * 2ll % p;
        }
        return ans;
    }

剑指 Offer 15. 二进制中1的个数

剑指 Offer 15. 二进制中1的个数

【位运算】

解:

  • 方法1:快速幂的思路
    int hammingWeight(uint32_t n) {
        int cnt = 0;
        while(n){
            if(n&1) ++cnt;
            n>>=1;
        }
        return cnt;
    }
  • 方法2:n&(n-1):把最后一个1变为0
    int hammingWeight(uint32_t n) {
        int cnt = 0;
        while(n){
            ++cnt;
            n = n & (n-1);
        }
        return cnt;
    }

剑指 Offer 16. 数值的整数次方

剑指 Offer 16. 数值的整数次方

【快速幂,浮点数】

解:

  • 方法:快速幂
    • 注意对浮点数的处理(指数为负数时)
    double myPow(double x, int n){
        if(!x) return 0;
        long long b = n;
        if(n < 0){
            x = 1.0/x;
            b = -b;
        }

        double res = 1.0;
        while(b){
            if(b & 1) res = res * x;
            x = x * x;
            b >>=1;
        }
        return res;
    }

剑指 Offer 17. 打印从1到最大的n位数

剑指 Offer 17. 打印从1到最大的n位数

【dfs,全排列,元素无重可复选】

解:

  • 方法:递归,DFS全排列

    • 大数,利用string转int。

      • int转string:to_string

        • string s = to_string(x);
          
      • string转int:atoi+c_str

      • 大数,利用string。转int需要去除前导0,所以用atoi+s.c_str()转为int

        • 区别:

          • 使用atoi,不全为字符串转为0
          • 使用stoi,不全为字符串会报错
        • int x = atoi(s.c_str());
          //先将string转为const char*,再转为int
          
    bool flag = 0;
    vector<int> ans;
    vector<int> printNumbers(int n) {
        string s = "0123456789";
        string path = "";
        dfs(s, path, n);
        return ans;
    }
    void dfs(string &s, string &path, int res){
        if(res == 0){
            if(!flag){ //去掉0
                flag = 1;
                return;
            }
            ans.push_back(atoi(path.c_str()));
            return;
        }
        for(int i = 0; i< s.length(); i++){
            path += s[i]; //path.push_back(s[i]);
            dfs(s, path, res-1);
            path.pop_back();
        }
    }

剑指 Offer 18. 删除链表的节点

剑指 Offer 18. 删除链表的节点

【链表删除】

解:

判断的是cur->next是否为要删除的节点,注意判空

    ListNode* deleteNode(ListNode* head, int val) {
        ListNode* dummy = new ListNode(-1);
        dummy->next = head;
        ListNode* cur = dummy;
        while(cur && cur->next){ //while(cur->next)
            if(cur->next->val == val){
                cur->next = cur->next->next;
                break;
            }
            cur = cur->next;
        }
        return dummy->next;
    }

剑指 Offer 19. 正则表达式匹配

剑指 Offer 19. 正则表达式匹配

【DP】

解:

  • 方法:DP,后续会单独整理。
    int m, n;
    int memo[30+5][30+5];
    bool isMatch(string s, string p) {
        m = s.length(), n = p.length();
        memset(memo, -1, sizeof(memo));
        return dp(s, p, 0, 0);
    }
    bool dp(string &s, string &p, int i, int j){
        if(j >= n){
            return i == m;
        }
        if(i >= m){
            if((n-j) & 1) return false;
            for(int k = j; k+1 < n; k+=2){
                if(p[k+1] != '*') return false;
            }
            return true;
        }
        if(memo[i][j]!= -1) return memo[i][j];
        bool res;
        if(s[i] == p[j] || p[j]=='.'){
            if(j < n-1 && p[j+1] == '*'){
                res= dp(s, p, i, j+2) | dp(s, p, i+1, j);
            }
            else{
                res= dp(s, p, i+1, j+1);
            }
        }
        else{
            if(j < n-1 && p[j+1]=='*'){
               res= dp(s, p, i, j+2);
            }
            else res=false;
        }
        memo[i][j] = res;
        return memo[i][j];
    }
};


/*
dp(i,j)
1.s[i]==p[j] || p[j]=='.'
(1)p[j+1] =='*'
i,j+=2
i++,j
(2)others
i++,j++

2.s[i]!=p[j]
(1)p[j+1]=='*'
i,j+=2
(2)others
return false
*/

剑指 Offer 20. 表示数值的字符串【1】

剑指 Offer 20. 表示数值的字符串

【匹配,有限状态自动机(待整理)】

if(a) return true;
else return false;
->
return a;

解:

  • 方法:模拟
    • 按题目要求判断
    bool isNumber(string s){
        int i = 0, j = s.length()-1;
        // 去掉最前方和最后方空格
        while(i < s.length() && s[i]==' ') ++i;
        while(j > 0 && s[j] == ' ') --j;

        bool flagE = 0;
        int k = i;
        for(; k <= j; k++){
            if(!isValid(s[k])) return false;
            if(s[k] == 'e' || s[k] == 'E'){
                flagE = 1;
                break;
            }
        }
        if(flagE){ //有e,e前面可以是整数/小数,e后面只能是整数
            return ((isInt(s, i, k-1) || isDec(s, i, k-1)) && isInt(s, k+1, j));
        }
        
        return (isInt(s, i, j) || isDec(s, i, j)); //没有e,判断整个字符串是否是整数/小数
    }
    
    bool isValid(char c){ //字符是否为合法字符
        if(c == '+' || c == '-' || c == 'e' || c == 'E' || c == '.') return true;
        if(c >= '0' && c <= '9') return true;
        return false;
    }
    bool isSign(char c){ //字符是否为符号
        return (c == '+' || c == '-');
    }
    bool isNum(char c){ //字符是否为数字
        return (c >= '0' && c <= '9');
    }

    bool isInt(string &s, int i, int j){ // 字符串是否为整数
        if(i < 0 || j >= s.length()) return false;
        if(isSign(s[i])) ++i;
        bool flag = 0;
        for(int k  = i; k<= j; k++){
            if(!isNum(s[k])) return false;
            flag = 1;
        }
        return flag;
    }

    bool isIntNoSign(string &s, int i, int j){ //字符串是否为不带符号整数
        if(i < 0 || j >= s.length()) return false;
        bool flag = 0;
        for(int k  = i; k<= j; k++){
            if(!isNum(s[k])) return false;
            flag = 1;
        }
        return flag;
    }

    bool isDec(string &s, int i, int j){ //字符串是否为小数
        if(i < 0 || j >= s.length()) return false;
        if(isSign(s[i])) ++i;
        int k = i;
        for(; k<= j; k++){
            if(s[k] == '.') break;
        }
        if(k == i && isIntNoSign(s, k+1, j)) return true; //.开头,后面必须为不带符号整数
        else if(k == j && isInt(s, i, k-1)) return true; //.结尾,前面必须为整数
        else if(isInt(s, i, k-1) && isIntNoSign(s, k+1, j)) return true; //.在中间,前后必须为整数,其中后面必须为不带符号整数
        return false;
    }

剑指 Offer 21. 调整数组顺序使奇数位于偶数前面

剑指 Offer 21. 调整数组顺序使奇数位于偶数前面

【双指针】

    vector<int> exchange(vector<int>& nums) {
        int i = 0, j = nums.size() - 1;
        while(i < j){
            // cout<
            if(!(nums[i] & 1) && (nums[j]&1)){
                swap(nums[i], nums[j]);
                i++;
                j--;
                continue;
            }
            if(nums[i] & 1) i++;
            if(!(nums[j] & 1)) j--;
        }
        return nums;
    }

剑指 Offer 22. 链表中倒数第k个节点

剑指 Offer 22. 链表中倒数第k个节点

【双指针-快慢指针】

解:

  • 方法:快慢指针
    • 先让快指针先走k步,然后快慢指针同时走,快指针指向null时慢指针指向倒数第k个节点。
    ListNode* getKthFromEnd(ListNode* head, int k) {
        ListNode * fast = head, *slow = head;
        while(k && fast){
            fast = fast->next;
            k--;
        }
        while(fast){
            fast = fast->next;
            slow = slow->next;
        }
        return slow;
    }

剑指 Offer 24. 反转链表

剑指 Offer 24. 反转链表

【迭代&递归】

迭代版:

    ListNode* reverseList(ListNode* head) {
        ListNode* cur = head, *prev = nullptr;
        while(cur){
            ListNode* tmp = cur->next;
            cur->next = prev;
            prev = cur;
            cur = tmp;
        }
        return prev;
    }

递归版:

    ListNode* reverseList(ListNode* head){
        if(!head || !head->next) return head;
        ListNode* last = reverseList(head->next);
        head->next->next = head;
        head->next = nullptr;
        return last;
    }

剑指 Offer 25. 合并两个排序的链表

剑指 Offer 25. 合并两个排序的链表

【迭代&递归】

迭代版:

    ListNode* mergeTwoLists(ListNode* l1, ListNode* l2) {
        ListNode* dummy = new ListNode(-1);
        ListNode* cur = dummy;
        while(l1 && l2){
            if(l1->val <= l2->val){
                cur->next = l1;
                l1 = l1->next;
            }
            else{
                cur->next = l2;
                l2 = l2->next;
            }
            cur = cur->next;
        }
        cur->next = l1? l1: l2;
        return dummy->next;
    }

递归版:

    ListNode* mergeTwoLists(ListNode* l1, ListNode* l2){
        if(!l1) return l2;
        if(!l2) return l1;
        if(l1->val <= l2->val){
            l1->next = mergeTwoLists(l1->next, l2);
            return l1;
        }
        else{
            l2->next = mergeTwoLists(l1, l2->next);
            return l2;
        }
        
    }

剑指 Offer 26. 树的子结构

剑指 Offer 26. 树的子结构

【递归,二叉树】

解:

  • 方法:递归
    • 判断B是不是A的子结构(注意不是子树),B可以是A子树的一部分
    • 先序遍历A的节点,看以该节点为根的子树能否与B匹配,不能就继续往左子树和右子树找。(注意空树不是任意一个树的子结构,特判(base case))
    • 能否匹配:即B是否是P(A的子树)的子结构,如果B空,说明B访问完了,说明B是P的一部分,true;若B不空P空了,说明B还没访问完P已经访问完了,B多出一块,false;若B->val!=P->val,必然不匹配;若B->val==P->val,再看左子树是否满足条件,右子树是否满足条件。
    bool isSubStructure(TreeNode* A, TreeNode* B) {
        if(!A || !B) return false;
        if(isSubStru(A, B)) return true;
        return isSubStructure(A->left, B) || isSubStructure(A->right, B);
    }
    bool isSubStru(TreeNode* A, TreeNode* B){
        if(!B) return true;
        if(!A || A->val != B->val) return false;
        return isSubStru(A->left, B->left) && isSubStru(A->right, B->right);
    }

剑指 Offer 27. 二叉树的镜像

剑指 Offer 27. 二叉树的镜像

【递归,二叉树】

    TreeNode* mirrorTree(TreeNode* root) {
        if(!root) return nullptr;
        TreeNode* left = mirrorTree(root->left);
        TreeNode* right = mirrorTree(root->right);
        root->left = right;
        root->right = left;
        return root;
    }

剑指 Offer 28. 对称的二叉树

剑指 Offer 28. 对称的二叉树

【递归,二叉树】

判断一棵树是否为对称二叉树

解:

  • 方法:递归
    • 思路:构造一个辅助函数判断两棵树是否是镜像对称的,然后题目只要判断两棵这个树是否镜像对称
      • 而比较两棵树是否镜像对称,即一棵树的左子树和另一棵树的右子树,以及一棵树的右子树和另一棵树的左子树是否镜像对称
      • 特殊判断:都是空树满足条件;其中有一棵空树不满足条件
    bool isSymmetric(TreeNode* root) {
        if(!root) return true;
        return isSymmetric(root, root);
    }

    bool isSymmetric(TreeNode* T1, TreeNode* T2){
        if(!T1 && !T2) return true;
        if(!T1 || !T2) return false;
        if(T1->val != T2->val) return false;
        return isSymmetric(T1->left, T2->right) && isSymmetric(T1->right, T2->left); 
    }

剑指 Offer 29. 顺时针打印矩阵

剑指 Offer 29. 顺时针打印矩阵

【模拟,花式遍历数组】

    vector<int> spiralOrder(vector<vector<int>>& matrix) {
        if(!matrix.size() || !matrix[0].size()) return {};
        int left = 0, high = 0, bottom = matrix.size()-1, right = matrix[0].size()-1;
        vector<int> res;
        while(high <= bottom && left <= right){
            for(int i = left; i<= right; i++){
                res.push_back(matrix[high][i]);
            }
            ++high;
            if(high > bottom) break;
            for(int i = high; i<= bottom; i++){
                res.push_back(matrix[i][right]);
            }
            --right;
            if(right < left) break;
            for(int i = right; i>= left; i--){
                res.push_back(matrix[bottom][i]);
            }
            --bottom;
            if(bottom < high) break;
            for(int i = bottom; i>= high; i--){
                res.push_back(matrix[i][left]);
            }
            ++left;
            if(left > right) break;
        }
        return res;
    }

剑指 Offer 30. 包含min函数的栈

剑指 Offer 30. 包含min函数的栈

【辅助栈】

每次调用min函数都需要得到当前栈中的最小值,可借助第二个栈来解决。

解:

  • 方法1:辅助栈
    • 辅助栈:一个非递增栈,每新来一个元素,就看它与该栈栈顶元素的关系;如果小于等于,说明该元素是当前的最小值,入栈;如果大于,说明当前的最小值不是该元素(而是栈顶元素)。
    • 每pop掉一个栈顶元素(第一个栈的),就看这个元素是否是这个非递增栈的栈顶元素,如果是的话,说明当前的最小值是这个元素,但这个元素pop了,所以应当把辅助栈的栈顶元素也pop(即更改当前最小值)。
class MinStack {
public:
    /** initialize your data structure here. */
    stack<int> st1, st2;
    MinStack() {}

    void push(int x) {
        if(st2.empty() || st2.top() >= x) st2.push(x);
        st1.push(x);
    }
    
    void pop() {
        if(st2.top() == st1.top()) st2.pop();
        st1.pop();
    }
    
    int top() {
        return st1.top();
    }
    
    int min() {
        return st2.top();
    }
};
  • 方法2:不需要额外的辅助栈(只需要一个栈)
    • 开个全局变量minn记录当前的最小值
    • 每次push都push两个元素,分别是:当前(新来元素x来之前)最小值,新来的元素x。push完比较minn与x的关系,若x
    • 所以每次pop也pop两个元素。在pop第二个元素(即pop x后的当前最小值)时,记得用它更新minn。
class MinStack {
public:
    /** initialize your data structure here. */
    stack<int> st;
    int minn = INT_MAX;
    MinStack() {}
    
    void push(int x) {
        st.push(minn);
        if(x < minn) minn = x;
        st.push(x);
    }
    
    void pop() {
       st.pop();
       minn = st.top();
       st.pop();
    }
    
    int top() {
        return st.top();
    }
    
    int min() {
        return minn;
    }
};

剑指 Offer 31. 栈的压入、弹出序列

剑指 Offer 31. 栈的压入、弹出序列

【模拟】

解:

  • 方法:模拟
    • pushed数组元素依次入栈(每次入一个),然后判断栈顶元素是否为当前poped[i],若是,弹出栈顶元素并++i,循环直到不相等为止,然后继续将pushed数组元素入栈。。。最后判断栈是否为空。
    bool validateStackSequences(vector<int>& pushed, vector<int>& popped){
        stack<int> st;
        int i = 0;
        for(int num: pushed){
            st.push(num);
            while(!st.empty() && st.top() == popped[i]){
                st.pop();
                i++;
            }
        }
        return st.empty();
    }

剑指 Offer 32 - I. 从上到下打印二叉树

剑指 Offer 32 - I. 从上到下打印二叉树

【BFS,二叉树层序遍历】

    vector<int> levelOrder(TreeNode* root) {
        if(!root) return {};
        vector<int> res;
        queue<TreeNode*> q;
        q.push(root);
        while(!q.empty()){
            int size = q.size();
            for(int i = 0; i< size; i++){
                TreeNode* t = q.front();
                q.pop();
                res.push_back(t->val);
                if(t->left) q.push(t->left);
                if(t->right) q.push(t->right);
            }
            
        }
        return res;
    }

剑指 Offer 32 - II. 从上到下打印二叉树 II

剑指 Offer 32 - II. 从上到下打印二叉树 II

【BFS,二叉树层序遍历】

    vector<vector<int>> levelOrder(TreeNode* root) {
        vector<vector<int>> ans;
        if(!root) return ans;
        queue<TreeNode*> q;
        q.push(root);
        while(!q.empty()){
            int size = q.size();
            vector<int> res;
            for(int i = 0; i< size; i++){
                TreeNode* t = q.front();
                q.pop();
                res.push_back(t->val);
                if(t->left) q.push(t->left);
                if(t->right) q.push(t->right);
            }
            ans.push_back(res);
        }
        return ans;
    }

剑指 Offer 32 - III. 从上到下打印二叉树 III

剑指 Offer 32 - III. 从上到下打印二叉树 III

【BFS,层序遍历二叉树】

解:

  • 方法:BFS
    • 之字形遍历,分奇偶,偶数层从左到右打印,奇数层从右到左打印。(从第0层开始->最终的ans.size()的奇偶可得该层是奇数层还是偶数层)
    • 借助双端队列deque完成reverse。偶数层加到末尾,奇数层加到开头。
    vector<vector<int>> levelOrder(TreeNode* root) {
        vector<vector<int>> ans;
        if(!root) return ans;
        queue<TreeNode*> q;
        q.push(root);
        while(!q.empty()){
            int size = q.size();
            deque<int> dq;
            for(int i = 0; i< size; i++){
                TreeNode* t = q.front();
                q.pop();

                if(ans.size()&1) dq.push_front(t->val);
                else dq.push_back(t->val);

                if(t->left) q.push(t->left);
                if(t->right) q.push(t->right);
            }
            ans.push_back(vector<int>{dq.begin(), dq.end()});
        }
        return ans;
    }

剑指 Offer 33. 二叉搜索树的后序遍历序列

剑指 Offer 33. 二叉搜索树的后序遍历序列

【递归&单调栈】

解:

  • 方法1:递归
    • 二叉树后序序列是否合法:左右根,序列最后一个是根节点。
    • 1.第一个大于根的位置就是左子树和右子树分界线。
    • 2.左子树必须都小于根(步骤1的遍历找位置保证了),右子树必须都大于根(从步骤1的位置开始遍历大于根的位置,看遍历后的指针是否指向最后一个位置(根节点))。
    • 3.判断左子树是否合法&&右子树是否合法。
    bool verifyPostorder(vector<int>& postorder) {
        return isValid(postorder, 0, postorder.size()-1);
    }

    bool isValid(vector<int>& postorder, int i, int j){
        if(i >= j) return true;
        int p = i;
        while(postorder[p] < postorder[j]) p++;
        int k = p;
        while(postorder[p] > postorder[j]) p++;
        if(p != j) return false;
        return isValid(postorder, i, k-1) && isValid(postorder, k, j-1);
    }
  • 方法2:单调栈
    • 为方便,倒序地看后序序列:根右左,就是前序遍历先遍历了右子树。(以下分析的是倒序的后序序列)
    • 对于一个元素(位置i),紧跟其后的(i+1)若大于它,表明后面这个元素是他的直接右子节点;若小于,要么是他直接左子节点,要么是他的父节点的直接左子节点(不好说),判断具体是哪个只需看这个a[i+1]是否大于a[i]的父节点,若大于,则是a[i]的直接左子节点。
    • 又由BST的限制:右子树的元素必须全部大于根节点,左子树的元素必须全部小于根节点,所以需要记录根节点的值,进行左右子树的合法性判断。
    • 还有序列的性质:根右左,访问完右子树(比根大的)再访问左子树,访问左子树时左子树的元素应该都比根小。应该开一个root变量记录当前根元素的值,进行合法性判断。
    • 借助单调栈。维护一个单调递增栈。一开始root设为INF(认为整棵树是值为INF的根节点的左子树),然后依次比较倒序后序序列的每个元素:若小于当前root值则合法;然后看该元素是否大于栈顶元素,若大于(说明是直接右子节点)则入栈;若小于,弹出栈顶元素继续比较直到栈顶元素小于该元素,而最后一个弹出的栈顶元素就是该元素的根节点(也是当前的root值)(该元素就是这个根节点的直接左子节点,也说明倒序后序序列从该元素起后面的所有元素都必须小于这个根节点(即当前的root值)),将该元素入栈。
    • (另外,后面再更新的root值也只会比当前的root值小,因为再找只会找当前root的左子树或它的根节点的左子树(而它是在根节点的右子树中),这样进一步限定了范围)。
    bool verifyPostorder(vector<int>& postorder) {
        stack<int> st;
        int root = INT_MAX;
        for(int i = postorder.size()-1; i >= 0; i--){
            if(postorder[i] > root) return false;
            while(!st.empty() && st.top() > postorder[i]){
                root = st.top();
                st.pop();
            }
            st.push(postorder[i]);
        }
        return true;
    }

剑指 Offer 34. 二叉树中和为某一值的路径

剑指 Offer 34. 二叉树中和为某一值的路径

【递归,二叉树先序遍历】

解:

  • 方法:递归
    • 注意判断左右子树都为空,且target减到0,才能加入ans列表。
    vector<vector<int>> ans;
    vector<vector<int>> pathSum(TreeNode* root, int target) {
        if(!root) return ans;
        vector<int>res;
        dfs(res, root, target);
        return ans;
    }

    void dfs(vector<int> & res,TreeNode* root, int target){
        if(!root) return;

        target -= root->val;
        res.push_back(root->val);
        
        if(!root->left && !root->right){
            if(!target) ans.push_back(res);
        }
        dfs(res, root->left, target);
        dfs(res, root->right, target);
        res.pop_back();
        target += root->val;
        return;
    }

剑指 Offer 35. 复杂链表的复制

剑指 Offer 35. 复杂链表的复制

【哈希&链表的拼接与拆分】

解:

  • 方法1:哈希表
    • 哈希表:unordered_map mp;
    • 第一遍,遍历链表原节点,建立原节点向新节点的映射,复制值;
    • 第二遍,遍历链表原节点,更改对应新节点的next指向和random指向。
    • 返回mp[head];
    Node* copyRandomList(Node* head) {
        unordered_map<Node*, Node*> mp;

        Node* cur = head;
        while(cur){
            mp[cur] = new Node(cur->val);
            cur = cur->next;
        }

        cur = head;
        while(cur){
            Node* tmp = mp[cur];
            tmp->next = mp[cur->next];
            tmp->random = mp[cur->random];

            cur = cur->next;
        }
        return mp[head];
    }
  • 方法2:链表的拼接与拆分
    • 链表拼接(复制):
      • 第一遍:遍历链表原节点,对每个节点,在其后新插入一个节点(复制值)。
      • 第二遍:遍历链表原节点,原节点cur的next(cur->next)即是它的复制,cur->random->next即是cur->random的复制,所以让cur->next->random = cur->random? cur->random->next: nullptr;
    • 链表拆分:
      • prev指向head(为原链表头节点),cur指向head->next(为新链表头节点)
      • 第三遍:同时遍历prev和cur。遍历时让prev->next = prev->next->next; cur->next = cur->next->next,然后迭代:prev = prev->next; cur = cur->next;,直到cur->next为空为止。
      • 注意细节(最后prev->next指向nullptr)。
    Node* copyRandomList(Node* head) {
        if(!head) return nullptr;
        Node* cur = head;
        while(cur){
            Node* tmp = new Node(cur->val);
            tmp->next = cur->next;
            cur->next = tmp;
            cur = tmp->next;
        }
        cur = head;
        while(cur){
            cur->next->random = cur->random? cur->random->next: nullptr;
            cur = cur->next->next;
        }

        Node* prev = head, *newHead = head->next;
        cur = newHead;
        while(cur->next){
            prev->next = prev->next->next;
            cur->next = cur->next->next;

            prev = prev->next;
            cur = cur->next;
        }
        prev->next = nullptr; //carefull!
        return newHead;
    }

剑指 Offer 36. 二叉搜索树与双向链表

剑指 Offer 36. 二叉搜索树与双向链表

【递归,二叉树中序遍历】

解:

  • 方法:递归
    • BST性质:中序遍历可得有序序列,所以转化后的链表顺序即为中序遍历的顺序。
    • 引入外部变量prev,记录当前中序遍历遍历到的节点cur(当前树的根节点)的上一个节点(就是中序遍历的当前节点所在的中序序列的前一个位置的节点),然后在中序位置操作:prev->right = cur; cur->left = prev。因为在中序位置操作,cur的左子树已遍历完,而对于prev,它的左右子树都遍历完了,所以改动cur的left和prev的right很安全。
    • 注意对头节点和尾节点的互相指向操作。
    Node* prev;
    Node* treeToDoublyList(Node* root) {
        if(!root) return nullptr;
        prev = new Node(-1);
        Node* head = prev;
        traverse(root);
        head = head->right;
        prev-> right = head;
        head-> left = prev;
        return head;
    }

    void traverse(Node* root){
        if(!root) return;
        traverse(root->left);
        prev -> right = root;
        root-> left = prev;
        prev = root;
        traverse(root->right);
    }

//另一种对头节点和尾节点的操作方法:
    Node* prev, *head;
    Node* treeToDoublyList(Node* root) {
        if(!root) return nullptr;
        traverse(root);
        prev-> right = head;
        head-> left = prev;
        return head;
    }

    void traverse(Node* root){
        if(!root) return;
        traverse(root->left);
        if(!head) head = root;
        else prev -> right = root;
        root-> left = prev;
        prev = root;
        traverse(root->right);
    }

剑指 Offer 37. 序列化二叉树【0】

剑指 Offer 37. 序列化二叉树
【递归】

剑指 Offer 38. 字符串的排列

剑指 Offer 38. 字符串的排列

【DFS,全排列,元素可重不可复选】

    int vis[10]; //vector vis(s.length(), false);
    vector<string> ans;
    string path;
    vector<string> permutation(string s) {
        sort(s.begin(), s.end());
        dfs(s);
        return ans;
    }
    void dfs(string& s){
        if(path.length() == s.length()){
            ans.push_back(path);
            return;
        }
        char prev = '0';
        for(int i = 0; i < s.length(); i++){
            if(vis[i]) continue;
            if(s[i] == prev) continue;
            prev = s[i];

            vis[i] = 1;
            path += s[i];
            dfs(s);
            vis[i] = 0;
            path.pop_back();
        }
    }

剑指 Offer 39. 数组中出现次数超过一半的数字

剑指 Offer 39. 数组中出现次数超过一半的数字

【哈希表&摩尔投票法&快速选择】

解:

  • 方法2:摩尔投票法
    • 前提:数组中出现次数在一半以上的数字
    • 思想:票数正负抵消
    • 参考
    int majorityElement(vector<int>& nums) {
        int vote = 0;
        int number;
        for(int num: nums){
            if(!vote) number = num;
            vote += (num == number)? 1: -1;
        }
        return number; //该题保证出现次数超过一半的数字一定存在

        // 验证:number是否是超过一半的数字。
        // 因为使用摩尔投票法得到正确结果的前提就是该数字必须在数组中出现的次数在一半以上。
        // int cnt = 0;
        // for(int num: nums){
        //     if(num == number) ++cnt;
        // }
        // return (cnt > nums.size()/2)? number: -1;
    }
  • 方法3:快速选择
    • 找第n/2大的数(但是需要修改原数组)

剑指 Offer 40. 最小的k个数

剑指 Offer 40. 最小的k个数

【快速排序&快速选择】

  • 方法1:升序排序,选择前k个元素
    //快速排序后选取前k个元素
    vector<int> getLeastNumbers(vector<int>& arr, int k){
        if(k >= arr.size()) return arr;
        quickSort(arr, 0, arr.size()-1);
        vector<int> res({arr.begin(), arr.begin()+k});
        return res;
    }
    void quickSort(vector<int> &arr, int l, int r){
        if(l >= r) return;
        int p = partition(arr, l, r);
        quickSort(arr, l, p-1);
        quickSort(arr, p+1, r);
    }
    int partition(vector<int> & arr, int l, int r){
        int pivot = arr[l];
        int i = l+1, j = r; //[l,i), (j, r]
        while(i <= j){
            while(i <= r && arr[i] <= pivot) i++;
            while(j > l && arr[j] > pivot) j--;
            if(i > j) break;
            swap(arr[i], arr[j]);
        }
        swap(arr[l], arr[j]);
        return j;
    }
  • 方法2:快速选择,借助快速排序的“分界点”
    • 分界点前面的元素都小于等于它,分界点后面的元素都大于它。
    • 若分界点位置==k,表明位置k前面的0~k-1这k个元素是最小的k个元素。

迭代:

    // 借助快排,对于一个分界点,它前面的元素都比它小(或等于),它后面的元素都比它大。
    // 所以当分界位置==k时(下标从0开始,所以它是第k+1小元素),它前面的k个元素就是前k小元素。
    vector<int> getLeastNumbers(vector<int>& arr, int k){
        if(k >= arr.size()) return arr;
        return quickSort(arr, 0, arr.size()-1, k); //[l,r]
    }
    vector<int> quickSort(vector<int>& arr, int l, int r, int k){
        vector<int> res;
        while(l <= r){ 
            int p = partition(arr, l, r);
            if(p == k){
                res.assign(arr.begin(), arr.begin()+k);
                break;
            }
            else if(p < k) l = p + 1; //[p+1, r]
            else r = p - 1; //[l, p-1]
        }
        return res;
    }
    int partition(vector<int> &arr, int l, int r){
        int pivot = arr[l];
        int i = l+1, j = r;
        while(i <= j){
            while(i <= r && arr[i] <= pivot) i++;
            while(j > l && arr[j] > pivot) j--;
            if(i > j) break;
            swap(arr[i], arr[j]);
        }
        swap(arr[l], arr[j]);
        return j;
    }

递归:

    // 同上思路,递归写法
    vector<int> res;
    vector<int> getLeastNumbers(vector<int>& arr, int k){
        if(k >= arr.size()) return arr;
        quickSort(arr, 0, arr.size()-1, k);
        return res;
    }
    void quickSort(vector<int>& arr, int l, int r, int k){ //[l,r]
        if(l > r) return; 
        int p = partition(arr, l, r);
        if(p == k){
            res.assign(arr.begin(), arr.begin()+k);
            return;
        }
        else if(p < k) quickSort(arr, p+1, r, k);
        else quickSort(arr, l, p-1, k);
    }
    int partition(vector<int>& arr, int l, int r){
        int pivot = arr[l];
        int i = l+1, j = r;
        while(i <= j){
            while(i <= r && arr[i] <= pivot) i++;
            while(j > l && arr[j] > pivot) j--;
            if(i > j) break;
            swap(arr[i], arr[j]);
        }
        swap(arr[l], arr[j]);
        return j;
    }

剑指 Offer 41. 数据流中的中位数【0】

剑指 Offer 41. 数据流中的中位数

剑指 Offer 42. 连续子数组的最大和

剑指 Offer 42. 连续子数组的最大和

【滑动窗口&线性DP&贪心+前缀和】

解:

  • 方法1:滑动窗口
    int maxSubArray(vector<int>& nums) {
        int left = 0, right = 0;
        int windowSum = 0;
        int maxSum = INT_MIN;

        while(right < nums.size()){
            windowSum += nums[right];
            right++;
            maxSum = max(maxSum, windowSum); //必须写在前面,是在每次扩大窗口的时候更新答案。若写在缩小窗口的后面,有可能把窗口大小缩为0,这样更新的答案就不是正确的
            while(windowSum < 0 && left < right){
                windowSum -= nums[left];
                left++;
            }
        }
        return maxSum;
    }
  • 方法2:线性DP
    int maxSubArray(vector<int>& nums) {
        vector<int> dp(nums.size(), 0);
        dp[0] = nums[0];
        int maxSum = dp[0];
        for(int i = 1; i < nums.size(); i++){
            dp[i] = max(dp[i-1], 0)+nums[i];
            maxSum = max(maxSum, dp[i]);
        }
        return maxSum;
    }

//空间压缩优化后
    int maxSubArray(vector<int>& nums){
        int dp_0 = nums[0];
        int maxSum = dp_0;
        for(int i = 1;i < nums.size(); i++){
            int dp_1 = max(dp_0 + nums[i], nums[i]);
            dp_0 = dp_1;
            maxSum = max(maxSum, dp_0);
        }
        return maxSum;
    }
  • 方法3:贪心,前缀和数组
    int maxSubArray(vector<int>& nums){
        vector<int> preSum(nums.size(), 0);
        preSum[0] = nums[0];
        for(int i = 1; i < nums.size(); i++){
            preSum[i] = preSum[i-1] + nums[i];
        }
        int minPreSum = 0;
        int maxSum = INT_MIN;
        for(int i = 0; i < nums.size(); i++){
            int sum = preSum[i] - minPreSum;
            maxSum = max(maxSum, sum);
            minPreSum = min(minPreSum, preSum[i]);
        }
        return maxSum;
    }

剑指 Offer 43. 1~n 整数中 1 出现的次数【0】

剑指 Offer 43. 1~n 整数中 1 出现的次数

【数学】

剑指 Offer 44. 数字序列中某一位的数字【0】

剑指 Offer 44. 数字序列中某一位的数字

【数学】

剑指 Offer 46. 把数字翻译成字符串

剑指 Offer 46. 把数字翻译成字符串

【DP】

题干:

给定一个非负数,可进行分割,但每次分割出来的数必须 ≤ 25 \le 25 25,问共有多少种不同的分割方式。

如:2163。可分割:[2,1,6,3],[21,6,3],[2,16,3] 3种

数据范围: 0 ≤ n u m ≤ 2 31 0 \le num \le 2^{31} 0num231

函数签名:

int translateNum(int num);

解:dp

方法1:dp,递归

  • 分析:
    • int类型的num转换成字符串,便于分割
    • 状态:从字符串的第0位开始分割(即分割str[0,…]),所以状态就是从字符串的第i位开始分割有多少种不同方式(分割str[i…])
    • 选择:从字符串的第i位开始分割,有两种选择:
      • (1) 第i位和第i+1位一起
      • (2) 第i位单独分
    • 所以状态转移方程dp(i) = dp(i+2) + dp(i+1)
      • 但是第i位和第i+1位一起形成的数字要<=25
      • 而且要>=10,否则说明第i位为0,合在一起不合规
    • base case是i>=str.length()-1时,只有一种分割方法,返回1
    • 要求的是dp(0)
    • 复杂度
      • 时间复杂度 O ( 2 n ) O(2^n) O(2n),n是数字字符个数,也即栈的深度
      • 空间复杂度 O ( n ) O(n) O(n)
  • 优化:又根据状态转移方程,发现出现重叠子问题
    • 如dp(i) = dp(i+2) + dp(i+1),而dp(i+1) = dp(i+3) + dp(i+2),dp(i+2)重复了
    • 使用记忆化搜索
    • 复杂度
      • 时间复杂度 O ( n ) O(n) O(n)
      • 空间复杂度 O ( n ) O(n) O(n)
    int translateNum(int num){
        string str = to_string(num);
        return dp(str, 0);
    }
    int dp(string &str, int i){
        if(i >= str.length()-1) return 1;
        string t = str.substr(i, 2);
        if(t[0] == '1' || t[0] == '2' && t[1] <= '5'){
            return dp(str, i+2) + dp(str, i+1);
        }
        return dp(str, i+1);
    }

使用记忆化搜索:

    int memo[15];
    int translateNum(int num){
        string str = to_string(num);
        memset(memo, -1, sizeof(memo));
        return dp(str, 0);
    }

    int dp(string &str, int i){
        if(i >= str.length()-1) return 1;
        if(memo[i] != -1) return memo[i];
        string t = str.substr(i, 2);
        if(t >= "10" && t <= "25"){
            memo[i]  = dp(str, i+2) + dp(str, i+1);
        }
        else memo[i] = dp(str, i+1);
        return memo[i];
    }

方法2:dp,递推

  • 分析:
    • 由上面递归的分析,可以把备忘录抽取出来,写成递推的形式
    • base case:dp[n-1] = 1, dp[n]=1
    • 或者直接分析:
      • 设dp[i]为分割str[0…i]的方法数
      • 状态转移方程:dp[i] = dp[i-2] + dp[i-1]
      • base case:dp[-1] = 1, dp[0] = 1
      • 要求的:dp[n-1]
      • 正向遍历
    • 实际上,它们是对称的,即:
      • 设dp[i]为分割str[i…]的方法数
      • 状态转移方程:dp[i] = dp[i+2] + dp[i+1]
      • base case:dp[n-1] = 1, dp[n] = 1
      • 要求的:dp[0]
      • 逆向遍历
    • 后面会发现,逆向遍历可以利用整数的取余和除法,省去了转化成字符串的步骤
    • 复杂度:
      • 时间复杂度 O ( n ) O(n) O(n)
      • 空间复杂度 O ( n ) O(n) O(n):字符串 O ( n ) O(n) O(n),dp table O ( n ) O(n) O(n)
  • 优化:
    • 空间压缩:dp[i]只与dp[i+1]和dp[i+2]两个状态有关,所以可以不用dp table,只开两个额外的变量来记录
    • 利用整数取余和除法:整数取余(%100)+整数除法(/10,取整)是从右往左按位缩小数,符合逆向遍历的顺序。所以可不转化成字符串来做。
    • 复杂度:
      • 时间复杂度 O ( n ) O(n) O(n)
      • 空间复杂度 O ( 1 ) O(1) O(1)
    int translateNum(int num){
        string str = to_string(num);
        int n = str.length();
        vector<int> dp(n+5, 0);
        dp[n-1] = 1, dp[n] = 1;
        for(int i = n-2; i >= 0; i--){
            string t = str.substr(i, 2);
            if(t >= "10" && t <= "25") dp[i] = dp[i+2] + dp[i+1];
            else dp[i] = dp[i+1];
        }
        return dp[0];
    }

//正向遍历
    int translateNum(int num){
        string str = to_string(num);
        int n = str.length();
        vector<int> dp(n+5, 0);
        dp[0] = 1, dp[1] = 1;
        for(int i = 1; i < n; i++){
            string t = str.substr(i-1,2);
            if(t >= "10" && t <= "25") {
                if(i-2 < 0) dp[i] = 1 + dp[i-1];
                else dp[i] = dp[i-2] + dp[i-1];
            }
            else dp[i] = dp[i-1]; 
        }
        return dp[n-1];
    }

空间压缩优化:

    int translateNum(int num){
        string str = to_string(num);
        int n = str.length();
        int dp_0 = 1, dp_1 = 1;
        for(int i = n-2; i>= 0; i--){
            string t = str.substr(i, 2);
            int dp_2;
            if(t >= "10" && t <="25") dp_2 = dp_0 + dp_1;
            else dp_2 = dp_0;
            dp_1= dp_0;
            dp_0 = dp_2;
        }
        return dp_0;
    }

利用整数取余和除法:

    int translateNum(int num){
        int dp_0 = 1, dp_1 = 1;
        while(num){
            int t = num%100;
            int dp_2;
            if(t >= 10 && t <= 25){
                dp_2 = dp_0 + dp_1;
            }
            else dp_2 = dp_0;
            dp_1 = dp_0;
            dp_0 = dp_2;
            num /= 10;
        }
        return dp_0;
    }

方法3:递归

  • 直接利用整数取余和整数除法,求出方法数
    int translateNum(int num){
        return dp(num);
    }
    int dp(int num){
        if(!num) return 1;
        // if(num < 10) return 1;
        int t = num%100;
        if(t >= 10 && t <= 25) return dp(num/10) + dp(num/100);
        return dp(num/10);
    }

剑指 Offer 47. 礼物的最大价值

剑指 Offer 47. 礼物的最大价值

【线性DP】

最大路径和

解:

  • 方法1:递归,dp
    int maxValue(vector<vector<int>>& grid) {
        if(!grid.size()) return 0;
        int m = grid.size(), n = grid[0].size();
        return dp(grid, m-1, n-1);
    }
    int memo[200+5][200+5];
    int dp(vector<vector<int>>&grid, int i, int j){
        if(i < 0 || j < 0) return 0;
        if(memo[i][j]) return memo[i][j];
        memo[i][j] = max(dp(grid, i-1, j), dp(grid, i, j-1)) + grid[i][j];
        return memo[i][j];
    }
  • 方法2:递推,dp
    • 优化:空间压缩(滚动数组)
    int maxValue(vector<vector<int>>& grid) {
        if(!grid.size()) return 0;
        int m = grid.size(), n = grid[0].size();
        vector<vector<int>> dp(m+2, vector<int>(n+2, 0));
        dp[0][0] = grid[0][0];
        for(int i = 1; i < m; i++) dp[i][0] = dp[i-1][0] + grid[i][0];
        for(int j = 1; j < n; j++) dp[0][j] = dp[0][j-1] + grid[0][j];
        for(int i = 1; i < m; i++){
            for(int j = 1; j < n; j++){
                dp[i][j] = max(dp[i][j-1], dp[i-1][j]) + grid[i][j];
            }
        }
        return dp[m-1][n-1];
    }

//优化
    int maxValue(vector<vector<int>>& grid){
        if(!grid.size()) return 0;
        int m = grid.size(), n = grid[0].size();
        vector<int> dp(n+2, 0);
        for(int i = 0; i< m; i++){
            dp[0] += grid[i][0];
            for(int j = 1; j < n; j++){
                dp[j] = max(dp[j-1], dp[j]) + grid[i][j];
            }
        }
        return dp[n-1];
    }

剑指 Offer 48. 最长不含重复字符的子字符串

剑指 Offer 48. 最长不含重复字符的子字符串

【滑动窗口(双指针)】

解:

    int lengthOfLongestSubstring(string s) {
        int l = 0, r = 0;
        unordered_map<char, int> mp;
        int maxLen = 0;
        while(r < s.length()){
            char c = s[r];
            r++;
            mp[c]++;
            while(mp[c] > 1 && l < r){
                char t = s[l];
                l++;
                mp[t]--;
            }
            maxLen = max(maxLen, r-l); // 在此更新,保证是没有重复元素
        }
        return maxLen;
    }

剑指 Offer 49. 丑数

剑指 Offer 49. 丑数
【多路归并(三指针) & 优先队列】

  • 求第n个丑数是多少
    • 丑数:质因子只包含2、3、5的数
  • 思路:
    • 若一个数x为丑数,那么2*x3*x5*x都为丑数
      - 就有了三条链表:
      - *2: ugly[1]*2 -> ugly[2]*2 -> ugly[3]*2 -> ugly[4]*2 ...
      - *3: ugly[1]*3 -> ugly[2]*3 -> ugly[3]*3 -> ugly[4]*3 ...
      - *5: ugly[1]*5 -> ugly[2]*5 -> ugly[3]*5 -> ugly[4]*5 ...
    • 合并链表:
      • 将这三条有序链表合并,使得合并后的链表有序
        • 合并链表思想,两条链表合并&k条链表合并
          • 手动合并(三指针) & 最小堆(优先队列)
      • 不同链表间存在重复元素怎么办,会不会被重复计入?
        • 手动合并:不会
          • 每条链表都是升序排列,合并时也是从小到大排列。每次从不同链表中取最小的
          • 所以不同链表中,若存在重复元素,在该元素前面的元素都比它小,被提前拿走了,所以不同链表间的重复元素一定是在同一个循环里被判断的,可当场去重
        • 优先队列:可能会
          • 所以要加判断
          • 同手动合并的规则,因为是从小到大排列,如果存在重复元素,只可能是排列后的最后一个元素,和堆顶元素存在重复,特判即可

手动合并:

    int nthUglyNumber(int n) {
        vector<long> ugly(n+2, 0);
        int k = 1;
        int p2 = 1, p3 = 1, p5 = 1;
        long product2 = 1, product3 = 1, product5 = 1;
        while(k <= n){
            long minn = min(product2, min(product3, product5));
            ugly[k++] = minn;
            if(minn == product2){
                product2 = 2 * ugly[p2++];
            }
            if(minn == product3){
                product3 = 3 * ugly[p3++];
            }
            if(minn == product5){
                product5 = 5 * ugly[p5++];
            }
        }
        return ugly[n];
    }

优先队列(最小堆):

//上面的思路:若X为丑数,则2*x,3*x,5*x也为丑数
    int nthUglyNumber(int n){
        vector<long> ugly(n+2, 0);
        vector<int> factor{2,3,5};
        int k = 1;
        priority_queue<long, vector<long>, greater<long>> q; 
        q.push(1);
        while(k <= n){
            long minn = q.top();
            q.pop();
            ugly[k++] = minn;
            while(!q.empty() && q.top() == minn) q.pop();
            for(int fac: factor){
                q.push(minn * fac);
            }
        }
        return ugly[n];
    }

//手动合并的思路:三指针
    int nthUglyNumber(int n){
        vector<long> ugly(n+2, 0);
        vector<int> factor{2, 3, 5};
        priority_queue<node> q;
        
        int k = 1;
        for(int fac: factor){
            q.push(node{1, fac, 1});
        }
        while(k <= n){
            node t = q.top();
            q.pop();
            if(t.pro != ugly[k-1]) //1 
                ugly[k++] = t.pro;
            q.push(node{ugly[t.id] * t.fac, t.fac, t.id+1});

            // while(!q.empty() && q.top().pro == t.pro){ //2。1、2择一即可
            //     t = q.top();
            //     q.pop();
            //     q.push(node{ugly[t.id] * t.fac, t.fac, t.id+1});
            // }
        }
        return ugly[n];

    }
    struct node{
        long pro;
        int fac;
        int id;
        friend bool operator<(const node &a, const node &b){
            return a.pro > b.pro;
        }
    };

剑指 Offer 50. 第一个只出现一次的字符

剑指 Offer 50. 第一个只出现一次的字符

【哈希表&有序哈希表】

解:

  • 方法1:哈希表
    • 两遍,第一遍建立映射,第二遍找第一个mp[c]=1的字符
    char firstUniqChar(string s) {
        unordered_map<char,int> mp;
        for(char c: s) mp[c]++;
        for(char c: s){
            if(mp[c] == 1) return c;
        }
        return ' ';
    }
  • 方法2:有序哈希表
    • 通过vector,建立起有序的哈希表
      • 有序:按插入先后顺序排序
    char firstUniqChar(string s){
        vector<char> vec;
        unordered_map<char, bool> mp;
        for(char c: s){
            if(mp.find(c) == mp.end()){ //之前没出现过,就没操作过mp[c],mp.find(c)就返回末尾
                vec.push_back(c); //插入
            }
            mp[c] = (mp.find(c) == mp.end()); //若mp.find(c) == mp.end(),说明字符c是否之前没出现过,标记为1;若后面再出现了,mp.find(c)就不等于末尾了,赋值为0
        }
        for(char c: vec){
            if(mp[c]) return c;
        }
        return ' ';
    }

剑指 Offer 51. 数组中的逆序对【1】

剑指 Offer 51. 数组中的逆序对

【归并排序,离散化+树状数组(待整理),离散化+线段树(待整理)】

  • 方法1:归并排序
    • 在合并左右两个已经排好序的数组时,使用到了双指针i,j
      • 两种方法可求逆序对:
        • 1.对于左边区间的tmp[i],统计右边区间比它小的元素的个数
          • 所以要判断所有的i;
            • 所以要找i下一步会变化的情况,才能无重复无遗漏,有两种情况i会变化:
              • 在tmp[i]<=tmp[j]的情况下:
                • 说明对于i,右边区间(mid+1)~(j-1)这(j-1 + 1 -(mid+1))=(j-(mid+1))个元素都比i小
                • cnt += (j -(mid+1));
              • 最终j走完了i没走完的情况下:
                • 显然i是更大的,所以还要cnt+=(r+1-(mid+1))(左边区间还剩(mid+1-i)个没有判断,要循环累加这么多次)
        • 2.对于右边区间的tmp[j],统计左边区间比它大的元素的个数
          • 所以要判断所有的j;
            • 所以要找j下一步会变化的情况,才能无重复无遗漏,有两种情况j会变化:
              • 在tmp[i] > tmp[j]的情况下:
                • 说明对于j,左边区间i~mid这(mid+1-i)个元素都比j大
                • cnt+= (mid+1-i);
              • 最终i走完了j没走完的情况下:
                • 显然j是更大的,没有逆序对

第二种方法的实现:

    int cnt = 0;
    int reversePairs(vector<int>& nums) {
        tmp.reserve(nums.size());
        mergeSort(nums, 0, nums.size()-1);
        return cnt;
    }
    vector<int> tmp; //tmp.reserve(nums.size());
    void mergeSort(vector<int>&nums, int l, int r){
        if(l >= r) return;
        int mid = l + ((r - l) >> 1);
        mergeSort(nums, l, mid);
        mergeSort(nums, mid+1, r);
        merge(nums, l, mid, r);
    }
    void merge(vector<int>&nums, int l, int mid, int r){
        for(int p = l; p <= r; p++) tmp[p] = nums[p];
        int i = l, j = mid+1;
        for(int p = l; p <= r; p++){
            if(i == mid+1) nums[p] = tmp[j++];
            else if(j == r+1) nums[p] = tmp[i++];
            else if(tmp[i] <= tmp[j]){
                nums[p] = tmp[i++];
            }
            else{
                cnt += (mid + 1 - i); //方法2求逆序对,只需在这多加一句。当tmp[i]>tmp[j]时,对于编号j,编号i~mid的这(mid+1-i)个数与它组成逆序对
                nums[p] = tmp[j++];
            }
        }
    }

第一种方法的实现:

    int cnt = 0;
    int reversePairs(vector<int>& nums) {
        tmp.reserve(nums.size());
        mergeSort(nums, 0, nums.size()-1);
        return cnt;
    }
    vector<int> tmp; //tmp.reserve(nums.size());
    void mergeSort(vector<int>&nums, int l, int r){
        if(l >= r) return;
        int mid = l + ((r - l) >> 1);
        mergeSort(nums, l, mid);
        mergeSort(nums, mid+1, r);
        merge(nums, l, mid, r);
    }
    void merge(vector<int>&nums, int l, int mid, int r){
        for(int p = l; p <= r; p++) tmp[p] = nums[p];
        int i = l, j = mid+1;
        for(int p = l; p <= r; p++){
            if(i == mid+1) nums[p] = tmp[j++];
            else if(j == r+1){
                cnt += (r+1-(mid+1)); //方法1求逆序对,当j走完了时,对于编号i,右边区间比它小的元素有(r+1-(mid+1))个,与它组成逆序对
                nums[p] = tmp[i++];
            }
            else if(tmp[i] <= tmp[j]){
                cnt += (j - (mid+1)); //方法1求逆序对,当tmp[i]<=tmp[j]时,对于编号i,编号mid+1~j-1的这(j-1+1-(mid+1))=(j-(mid+1))个数与它组成逆序对
                nums[p] = tmp[i++];
            }
            else{
                nums[p] = tmp[j++];
            }
        }
    }

剑指 Offer 52. 两个链表的第一个公共节点

剑指 Offer 52. 两个链表的第一个公共节点

【链表相交】

    ListNode *getIntersectionNode(ListNode *headA, ListNode *headB) {
        ListNode* p1 = headA, *p2 = headB;
        while(p1 != p2){
            if(!p1) p1 = headB;
            else p1 = p1->next;
            if(!p2) p2  =headA;
            else p2 = p2->next;
        }  
        return p1;
    }

剑指 Offer 53 - I. 在排序数组中查找数字 I

剑指 Offer 53 - I. 在排序数组中查找数字 I

【二分】

解:二分

  • 思路1:==target的左边界l,==target的右边界r,若找不到返回0,找到了就是r+1-l

    • 优化:先查右边界,判断是否存在target,若不存在可直接得出结果;若存在,左边界查询可将范围缩小至[0,r]
  • 思路2:<=target的右边界r,<=(target-1)的右边界l,返回r-l

    • 优化:同上,查询范围缩小
  • (二分函数找不到返回的都是-1)

    int search(vector<int>& nums, int target){
        int r = binarySearch(nums, 0, nums.size(), target);
        int l = binarySearch(nums, 0, r+1, target-1);
        return r - l;
    }

    int binarySearch(vector<int> &nums, int l, int r, int target){ // 满足<=target的右边界
        int ans = -1;
        while(l < r){
            int mid = l + ((r - l) >> 1);
            if(nums[mid] <= target){
                ans = mid;
                l = mid+1;
            }
            else r = mid;
        }
        return ans;
    }

剑指 Offer 53 - II. 0~n-1中缺失的数字

剑指 Offer 53 - II. 0~n-1中缺失的数字

【位运算&二分】

  • 方法1:位运算
    • [0,…n-1]^nums,两两匹配的异或值为0,落单的那个元素就是结果
    int missingNumber(vector<int>& nums) {
        int n = nums.size();
        int res = 0;
        for(int i = 0; i<= n; i++){
            res^=i;
        }
        for(int num: nums) res^=num;
        return res;
    }
  • 方法2:二分
    • 因为是排序数组,对有序数组的搜索问题,考虑用二分
    • 缺失元素前面nums[i]==i,缺失元素后面nums[i]!=i
    • 二分搜索nums[i]==i的右边界
    int missingNumber(vector<int>& nums){
        int num = binarySearch(nums);
        return num+1;
    }
    int binarySearch(vector<int>& nums){
        int l = 0, r = nums.size();
        int ans = -1;
        while(l < r){
            int mid = l + ((r - l) >> 1);
            if(nums[mid] == mid){
                ans = mid;
                l = mid+1;
            }
            else r = mid;
        }
        return ans;
    }

剑指 Offer 54. 二叉搜索树的第k大节点

剑指 Offer 54. 二叉搜索树的第k大节点

【DFS,BST性质】

利用中序遍历BST得到升序序列(严谨:不递减序列)的性质。求第k大元素是倒着数的,那么使用先遍历右子树的中序遍历。

  • 方法1:直接利用BST中序遍历的性质
    • 中序遍历,但先遍历右子树(即右根左)
    • 外部变量cnt记录访问过的元素个数
    • 对当前节点,cnt++,若cnt==k,说明是第k大元素
    //利用BST中序遍历得到递增序列的特性
    int cnt = 0, ans = 0;
    int kthLargest(TreeNode* root, int k) {
        dfs(root, k);
        return ans;
    }
    void dfs(TreeNode* root, int k){
        if(!root) return;
        dfs(root->right, k);
        cnt++;
        if(cnt == k){
            ans = root->val;
            return;
        }
        else if(cnt > k) return;
        dfs(root->left, k);
    }
  • 方法2:利用节点所在的右子树大小,得到节点的排名,从而判断第k大元素在哪里(左子树/右子树/该节点)
    • 判断:
      • int size = siz[root->right];
        • size + 1 > k:在右子树
        • size + 1 < k:在左子树,往左子树找第(k - (size+1))大的节点
        • size + 1 == k:为该节点
    unordered_map<TreeNode*, int> siz;
    int kthLargest(TreeNode* root, int k){
        getSize(root);
        return dfs_kth(root, k);
    }
    int getSize(TreeNode* root){
        if(!root){
            return 0;
        }
        siz[root] = getSize(root->left) + getSize(root->right) +1;
        return siz[root];
    }
    int dfs_kth(TreeNode* root, int k){
        if(siz[root->right] + 1 > k) return dfs_kth(root->right, k);
        else if(siz[root->right] + 1 < k) return dfs_kth(root->left, k - (siz[root->right] + 1));
        return root->val;
    }

剑指 Offer 55 - I. 二叉树的深度

剑指 Offer 55 - I. 二叉树的深度

【DFS&BFS】

  • 方法1:DFS,分治
    int maxDepth(TreeNode* root) {
        return dfs(root);
    }
    int dfs(TreeNode* root){
        if(!root) return 0;
        return max(dfs(root->left), dfs(root->right)) + 1;
    }
  • 方法2:DFS,回溯
    int maxx = 0;
    int maxDepth(TreeNode* root) {
        dfs(root, 0);
        return ans;
    }
    void dfs(TreeNode* root, int depth){
        if(!root) return;
        depth++;
        maxx = max(maxx, depth);
        dfs(root->left, depth);
        dfs(root->right, depth);
    }
  • 方法3:BFS
    int maxDepth(TreeNode* root){
        if(!root) return 0;
        queue<TreeNode*> q;
        q.push(root);
        int depth = 0;
        while(!q.empty()){
            int n = q.size();
            depth++;
            for(int i = 0; i < n; i++){
                TreeNode* t = q.front();
                q.pop();
                if(t->left) q.push(t->left);
                if(t->right) q.push(t->right);
            }
        }
        return depth;
    }

剑指 Offer 55 - II. 平衡二叉树

剑指 Offer 55 - II. 平衡二叉树

【DFS】

  • 方法:
    • 开个外部变量flag,初始值设为1
    • DFS,自底向上的思想(分治,先求左右子树的深度,后序位置再算当前节点的),一边求深度一边判断是否平衡
      • 若不平衡,该节点往上的深度就不再计算了,flag标记为0,直接返回flag
    • 避免了重复计算
    bool flag = 1;
    bool isBalanced(TreeNode* root){
        getDepth(root);
        return flag;
    }
    int getDepth(TreeNode* root){
        if(!root || !flag) return 0; //子树已经不平衡了,不再继续找了,直接返回
        //下面还是flag=1时
        int depL = getDepth(root->left);
        int depR = getDepth(root->right);
        if(abs(depL - depR) > 1) flag = 0;
        return max(depL, depR)+1;
    }

剑指 Offer 56 - I. 数组中数字出现的次数

剑指 Offer 56 - I. 数组中数字出现的次数

【位运算,异或的性质】

一个数组中除了两个数字只出现一次外,其它数字都出现了两次,找到这两个只出现一次的数字。

  • 分析:
    • 如果是只有一个数字出现了一次,其它都出现了两次,那么可以直接计算这个数组的异或和,结果就是出现一次的这个数字
      • 利用了a^a = 0, a^0 = 0
    • 而该题是有两个数字都只出现一次,考虑能否分组异或
      • 就是通过一些条件,把数组的数分组:
        • 分组要求:
          • 1.把两两重复的数字分到同一个组
          • 2.把这两个只出现一次的数字分到不同的组
        • 那么对这两个组分别求异或和,结果就是这两个数
      • 设这两个数为a,b,这个数组的异或和为sum
        • 根据异或的性质,可得sum = a^b
        • 分析sum(即a^b)的二进制表示
          • 对某一位,为0表示它俩的该位值相同,为1表示它俩的该位值不同
            • ——为1的位展现了a与b的二进制表示下的差异
            • 假设sum的第k位为1
              • 因为a!=b,所以sum必然不为0,k一定存在
              • 第k位为1,说明a和b的第k位不同
                • 那么可以通过数组中数字的第k位的值是多少,把a和b分到不同的两个组里
                  • 满足分组要求2
              • 而对于相同的两个数c,d,它们的第k位一定相同,必然可以分到同一个组里
                • 满足分组要求1
            • 为了方便,第k位不妨取sum的最后一个1出现的位
              • 即掩码:第k位为1,其它位为0
                • 就是lowbit(sum)
              • 将掩码与某数相与,判断它的第k位是0还是1,将它分到不同的组里
    • 官方题解也写得很清楚
    vector<int> singleNumbers(vector<int>& nums){
        int sum = 0;
        for(int num: nums) sum^=num;
        int idx = lowbit(sum);
        int sum_0 = 0, sum_1 = 0;
        for(int num: nums){
            if(num & idx){
                sum_1 ^= num;
            }
            else sum_0 ^= num;
        }
        return vector<int>{sum_0, sum_1};
    }
    int lowbit(int x){ return x&-x;}

剑指 Offer 56 - II. 数组中数字出现的次数 II【1】

剑指 Offer 56 - II. 数组中数字出现的次数 II

【位运算,有限自动机(待整理)】

  • 分析:考虑数字的二进制形式
    • 统计数组中的数字,在32位二进制表示下,各位的1出现的次数
      • 对于出现3次的数字,在累计后,二进制表示下,所对应的各位的1出现的次数一定是3的倍数(即模3为0)
      • 所以,遍历32位各位的统计结果,为1出现的次数不是3的倍数的那些位赋1,所组成的数就是数组中只出现一次的那个数字
    int singleNumber(vector<int>& nums) {
        vector<int> mp(32,0); //统计数组中的数字,在32位二进制表示下,某一位为1的总个数
        for(int num: nums){
            int k = 0;
            while(num){
                mp[k] += (num & 1);
                num >>= 1;
                k++;
            }
        }
        int n = 0, t = 1;
        for(int i = 0; i < 32; i++){ 
            if(mp[i] && mp[i] % 3){ //当该位为1的总个数不能整除3时,说明包含了只出现一次的那个数a,a的第i位为1
                n += t; //加上2^i
            }
            t = (unsigned)t << 1; //用unsigned防止溢出,负数左移没有定义
        }
        return n;
    }

剑指 Offer 57. 和为s的两个数字

剑指 Offer 57. 和为s的两个数字

【双指针】

    vector<int> twoSum(vector<int>& nums, int target) {
        int left = 0, right = nums.size()-1;
        while(left < right){
            int sum = nums[left] + nums[right];
            if(sum == target){
                return vector<int>{nums[left], nums[right]};
            }
            else if(sum > target) right--;
            else left++;
        }
        return {};
    }

剑指 Offer 57 - II. 和为s的连续正数序列

剑指 Offer 57 - II. 和为s的连续正数序列

【双指针(滑动窗口)】

  • 思路:
    • 双指针i,j,之间形成一个窗口
      • 一般习惯左闭右开
    • 另开一个变量sum,记录当前窗口的和
      • 也可使用等差数列求和公式
    • 若:
      • sum < target:扩大窗口
      • sum > target:缩小窗口
      • sum == target:加入ans列表,缩小窗口
      • 扩大和缩小窗口时,同步更新sum

双指针思路:

    vector<vector<int>> findContinuousSequence(int target){
        int i = 1, j = 1; 
        int sum = 0;
        vector<vector<int>> ans;
        while(i <= target/2){
            if(sum < target){
                sum += j;
                j++;
            }
            else if(sum > target){
                sum -= i;
                i++;
            }
            else{
                vector<int> res;
                for(int k = i; k < j; k++) res.push_back(k); //是左闭右开[i,j)区间
                ans.push_back(res);
                sum -= i;
                i++;
            }
        }
        return ans;
    }

滑动窗口思路:

    vector<vector<int>> findContinuousSequence(int target){
        int left = 1, right = 1; 
        int sum = 0;
        vector<vector<int>> ans;
        while(left <= target/2){
            sum += right;
            right++;
            while(sum >= target){
                if(sum == target){
                    vector<int> res;
                    for(int i = left; i < right; i++) res.push_back(i); //是左闭右开[left,right)区间
                    ans.push_back(res);
                }
                sum -= left;
                left++;
            }
        }
        return ans;
    }

剑指 Offer 58 - I. 翻转单词顺序

剑指 Offer 58 - I. 翻转单词顺序

单词顺序翻转,多个空格变一个。且要求原地修改字符串

方法:

  • 思路:移出多余空格->字符串反转->单词反转

    • 1.移出多余空格

      • 双指针(快慢指针),resize字符串大小

        • O ( n ) O(n) O(n)

        • 思路同27.移除元素

        • void removeExtraSpaces(string &s){
              int slow = 0, fast = 0;
              while(fast < s.size()){
                  if(s[fast] != ' '){
                      if(slow) s[slow++] = ' '; //手动控制添加空格,在不是第一个的单词前添加一个空格
                      while(fast < s.size() && s[fast] != ' '){
                          s[slow++] = s[fast++];
                  	}
                  }
                  fast++;
              }
              s.resize(slow); //carefull!
          }
          
    • 2.字符串反转

      • 双指针

        • void reverse(string &s, int i, int j){
              while(i < j) swap(s[i++], s[j--]);
          }
          
    • 3.单词反转

      • 双指针,left记录开头,right找结尾(到s.size()或遇到空格即为末尾)
    • 没有使用额外空间,所以空间复杂度为 O ( 1 ) O(1) O(1)

    string reverseWords(string s) {
        removeExtraSpaces(s);
        reverse(s, 0, s.size()-1);
        int left = 0;
        for(int right = 0; right <= s.size(); right++){
            if(right == s.size() || s[right] == ' '){
                reverse(s, left, right-1);
                left = right+1;
            }
        }
        return s;
    }
    void removeExtraSpaces(string &s){
        int slow = 0, fast = 0;
        while(fast < s.size()){
            if(s[fast] != ' '){
                if(slow) s[slow++] = ' ';
                while(fast < s.size() && s[fast]!= ' '){
                    s[slow++] = s[fast++];
                }
            }
            fast++;
        }
        s.resize(slow);
    }
    void reverse(string &s, int i, int j){
        while(i < j) swap(s[i++], s[j--]);
    }

剑指 Offer 58 - II. 左旋转字符串

剑指 Offer 58 - II. 左旋转字符串

【切片,取余】

解:

  • 方法1:substr切片
    string reverseLeftWords(string s, int n) {
        int len = s.length();
        n %= len;
        // string t = s.substr(0, n);
        // s = s.substr(n, len-n);
        // s += t;
        return  s.substr(n, len-n) + s.substr(0, n);
    }
  • 方法2:取余(类似循环数组的思想)
    string reverseLeftWords(string s, int n){
        int len = s.length();
        n %= len;
        string res = "";
        for(int i = n; i< n+len; i++){
            res += s[i%len];
        }
        return res;
    }

剑指 Offer 59 - I. 滑动窗口的最大值

剑指 Offer 59 - I. 滑动窗口的最大值

【单调队列】

  • 思路同"面试题59 - II. 队列的最大值"
  • 滑动窗口:类似队列,先进先出
    • 但又要求最值
    • 即:既要求当前队列中最值,又要满足先进先出的顺序
      • 方法:维护一个单调不递增队列。移动窗口,即每次来新元素(num[right])时更新该队列(小于新元素的都出队,最后把新元素push到队尾),这样该队列的队首元素即是当前滑动窗口的最大元素
        • 而移动窗口,要去掉nums[left],若num[left]正好为当前的最大元素时,单调队列的队首元素即是该元素,将队首pop,该队列新的队首元素就是pop掉该元素后的(新窗口)的最大值
    vector<int> maxSlidingWindow(vector<int>& nums, int k) {
        if(!nums.size()) return {};
        vector<int> res;
        int left = 0, right =0;
        myQueue window;
        while(right < nums.size()){
            window.push(nums[right]);
            right++;
            if(right - left == k && left < nums.size()){
                res.push_back(window.maxValue());
                window.pop(nums[left]);
                left++;
            }
        }
        return res;
    }

    class myQueue{
    public:
        deque<int> dq;
        void push(int x){
            while(!dq.empty() && dq.back() < x){
                dq.pop_back();
            }
            dq.push_back(x);
        }
        int maxValue(){
            // if(dq.empty()) return -1;
            return dq.front();
        }
        void pop(int x){
            if(!dq.empty() && dq.front() == x) dq.pop_front();
        }
    };

剑指 Offer 60. n个骰子的点数

剑指 Offer 60. n个骰子的点数

【概率DP】

n个骰子所能掷出的点数之和的所有可能性的概率。

解:

  • 方法:概率DP
    • i个骰子,那么它所能掷出的点数之和为i~6i
      • 掷i个骰子,掷出的点数之和为j,这是一种状态,记为(i,j)
        • 它是从掷了(i-1)个骰子的状态转移来的
          • 因为一个骰子的点数是1~6,所以掷了i个骰子,和为j(状态(i,j)),只可能是掷了(i-1)个骰子,和为(j-k)(k=1,…6)(即状态(i-1,j-k))这6个状态转移过来
            • 所以选择:6个
      • 有了状态,有了选择,写出状态转移方程:
        • 设dp(i,j)为掷i个骰子,掷出的点数之和为j的概率
          • 那么 d p ( i , j ) = ∑ k = 1 6 1 6 ⋅ d p ( i − 1 , j − k ) dp(i,j)= \sum_{k=1}^6\frac{1}{6}\cdot dp(i-1,j-k) dp(i,j)=k=1661dp(i1,jk)
          • base case: d p ( 1 , j ) = 1 6 dp(1, j) = \frac{1}{6} dp(1,j)=61
          • 要求的是 d p ( n , j ) dp(n, j) dp(n,j) (j=n,…6n)
        • 关于如何遍历:
          • 很显然,对于第i个骰子,有效的和是i~6i6i的概率必为0,所以只关注i~6i即可
        • 其实也可以设dp(i,j)为掷i个骰子,掷出的点数之和为j的方法数
          • 则对于状态(n,j),概率为dp(i,j) / pow(6, n)
            • pow(6, n)为掷n个骰子的状态空间大小,即全部的方法数
// dp[i][j]:掷i个骰子,掷出的点数之和为j的概率
	vector<double> dicesProbability(int n) {
        vector<vector<double>> dp(n+1, vector<double>(6*n+1, 0.0)); //下标偏移1位
        for(int j = 1; j<= 6;j++) dp[1][j] = 1.0/6;
        for(int i = 2; i<= n; i++){
            for(int j = i; j <= 6*i; j++){
                for(int k = 1; k <= 6; k++){
                    if(j-k <= 0) break; //if(j-k <= 0 || j-k
                    dp[i][j] += 1.0/6*dp[i-1][j-k];
                }
            }
        }
        vector<double> res;
        for(int j = n; j<= 6*n; j++) res.push_back(dp[n][j]);
        return res;
    }

//dp[i,j]:掷i个骰子,掷出的点数之和为j的方法数
    vector<double> dicesProbability(int n) {
        vector<vector<double>> dp(n+1, vector<double>(6*n+1, 0.0)); //下标偏移1位
        for(int j = 1; j<= 6;j++) dp[1][j] = 1.0;
        for(int i = 2; i<= n; i++){
            for(int j = i; j <= 6*i; j++){
                for(int k = 1; k <= 6; k++){
                    if(j-k <= 0) break; //if(j-k <= 0 || j-k
                    dp[i][j] += dp[i-1][j-k];
                }
            }
        }
        vector<double> res;
        double cnt = pow(6.0, n); //n=6时状态空间总数
        for(int j = n; j<= 6*n; j++) res.push_back(dp[n][j]/cnt);
        return res;
    }

优化:空间压缩,降维

    vector<double> dicesProbability(int n) {
        vector<double> dp(6*n+1, 0.0); //下标偏移1位
        for(int j = 1; j<= 6;j++) dp[j] = 1.0/6;
        for(int i = 2; i<= n; i++){
            for(int j = 6*i; j >=i; j--){ //通过递推式(状态转移方程)得出要逆序遍历
                dp[j] = 0.0;
                for(int k = 1; k <= 6; k++){
                    if(j-k <= 0 || j-k<i-1) break; //必须大于(i-1)状态的对角线(即大于i-1)才行。因为状态压缩后,小于i-1的格子dp[k](k
                    dp[j] += 1.0/6*dp[j-k];
                }
            }
        }
        vector<double> res;
        for(int j = n; j<= 6*n; j++) res.push_back(dp[j]);
        return res;
    }

剑指 Offer 62. 圆圈中最后剩下的数字

剑指 Offer 62. 圆圈中最后剩下的数字

【DP,约瑟夫环】

  • 方法1:链表模拟,

    • (n-1)轮,删除 O ( 1 ) O(1) O(1)
    • 复杂度 O ( m n ) O(mn) O(mn),不能接受
  • 方法2:DP+数学

    • 约瑟夫环问题:n个人排成一个圆圈做游戏,每轮游戏去掉第m个人,然后从第(m+1)个开始计数开始新一轮,直到剩余一个人。

    • 分析:

      • 共n个数,每轮去掉一个数,显然需要有(n-1)轮,直到最后剩余1个数为止

        • 每轮去掉的都是第m个数,会把第(m+1)个数移到第一位

          • 所以对于有i个数的数组,它去掉第m个数,把第(m+1)个数移到第一位后,变成了有(i-1)个数的数组

            • 这两个数组(即两个状态)间有什么联系?
              • 因为有i个数的数组,把第(m+1)个数移到了第一位,对于第m个数,如果没被去掉,应该在有(i-1)个数的数组的最后一位
                • 设有i个数的数组的状态为(i),有(i-1)个数的数组的状态为(i-1)
              • 所以状态(i-1)是把状态(i)的元素循环左移了m位!
              • 所以对状态(i)来说,它里面元素对应的编号,应该是状态(i-1)元素的对应编号循环右移m位得到的
                • 即f(i) = (f(i-1) + m) % n
                • 但其它元素在游戏过程都被去掉了,只有最终剩余的那个数一直存在最后一轮(即状态:数组中只有1个数),所以这个递归式只适用于最终剩余的那个元素
                  • 正好是题目要求求的
                  • f[i]是最终剩余的那个元素的编号
                • 递归边界:当数组中只有1个数时,游戏结束,f[1]显然为0
                  • 写成递归那就从f[1]=0开始正向推,直到f[n]
                • 该题元素与编号是一一对应关系,所以求编号就是求元素
          • 举例(为清晰,把数组中的元素写成字母)

            • 设n=8,m=3

            • 0	 1	  2	  3	  4	  5	  6	  7
              A	 B    (C)  D   E   F  [G]  H   // n=8, id = (3+3)%8=6
              D	 E    (F) [G]  H   A   B //n=7, id = (0+3)%7 = 3
              [G]	 H    (A)  B   D   E //n=6, id = (3+3)%6 = 0
              B	 D    (E) [G]  H //n=5, id = (0+3)%5 = 3
              [G]	 H    (B)  D	//n=4, id = (1+3)%4 = 0
              D	[G]   (H)	//n=3, id = (1+3) % 3 = 1
              (D)	[G]  //n=2,id = (0+3) % 2 = 1
              [G] //n=1,id = 0
              

递归:

    int lastRemaining(int n, int m) {
        if(n == 1) return 0;
        return (lastRemaining(n-1, m)+m)%n;
    }

递推:

    int lastRemaining(int n, int m){
        int pos = 0;
        for(int i = 2; i <= n; i++){
            pos = (pos+m) % i;
        }
        return pos;
    }

剑指 Offer 63. 股票的最大利润

剑指 Offer 63. 股票的最大利润

【贪心&dp】

最多只能买卖一次股票

解:

  • 方法1:贪心
    • 使用贪心策略,假设第 i 轮进行卖出操作,买入操作价格应该在 i 之前并且价格最低。因此在遍历数组时记录当前最低的买入价格,并且尝试将每个位置都作为卖出价格,取收益最大的即可。
      • 即最大利润maxProfit = max(price[i] - min(price[j])) (j
    • 思路类似最大子数组和的贪心&前缀和做法
    int maxProfit(vector<int>& prices){
        int minPrice = INT_MAX;
        int maxProfit = 0;
        for(int i = 0; i< prices.size(); i++){
            int profit = prices[i] - minPrice;
            maxProfit = max(maxProfit, profit);
            minPrice = min(minPrice, prices[i]);
        }
        return maxProfit;
    }
  • 方法2:dp

剑指 Offer 64. 求1+2+…+n

剑指 Offer 64. 求1+2+…+n

利用逻辑运算符的短路效应,判断是否继续递归

    int sum = 0;
    int sumNums(int n) {
        bool tmp = (n - 1 > 0) && sumNums(n-1);
        sum += n;
        return sum;
    }

剑指 Offer 65. 不用加减乘除做加法

剑指 Offer 65. 不用加减乘除做加法

实现CPU加法器

  • 二进制下:
    • 求和:a^b
    • 进位:a&b
    • 不断重复这个操作,直到没有进位(进位为0)时停止
  • 参考

迭代版:

    int add(int a, int b) {
        while(b){
            int c = a & b;
            a = a ^ b;
            b = (unsigned)c << 1; //负数左移没有定义
        }
        return a;
    }

递归版:

    int add(int a, int b){
        return b? add(a ^ b, (unsigned)(a & b) << 1): a;
    }

剑指 Offer 66. 构建乘积数组

剑指 Offer 66. 构建乘积数组

【前缀和&后缀和思想】

  • 思路:求出元素的前缀乘、后缀乘(都不包含该元素),然后将二者相乘即为答案
    • 前缀乘一次正向遍历,空间 O ( n ) O(n) O(n);后缀乘一次逆向遍历,空间 O ( n ) O(n) O(n);数组元素的二者相乘,一次遍历
      • 优化:
        • 后缀乘和最后的答案求解(前缀乘后缀乘相乘)合到一个循环里(一次逆向遍历解决)
        • 且不另外开后缀乘的数组,而是通过tmp变量动态记录当前后缀乘的结果
        • 因为需要二者相乘,另开的前缀乘数组可直接乘上后缀乘,作为最后的答案数组
      • 复杂度
        • 时间复杂度: O ( n ) O(n) O(n)
        • 空间复杂度: O ( 1 ) O(1) O(1)
    vector<int> constructArr(vector<int>& a) {
        int n = a.size();
        if(!n) return{};
        vector<int> b(n, 1);
        for(int i = 1; i < n; i++) b[i] = b[i-1] * a[i-1]; //维护前缀乘
        int tmp = 1; //维护后缀乘
        for(int i = n-2; i >= 0; i--){
            tmp*= a[i+1];
            b[i] *= tmp;
        }
        return b;
    }

剑指 Offer 64. 求1+2+…+n

剑指 Offer 64. 求1+2+…+n

利用逻辑运算符的短路效应,判断是否继续递归

    int sum = 0;
    int sumNums(int n) {
        bool tmp = (n - 1 > 0) && sumNums(n-1);
        sum += n;
        return sum;
    }

面试题13. 机器人的运动范围

面试题13. 机器人的运动范围

【DFS&BFS】

很标准的DFS&BFS模板

  • 复杂度
    • 时间复杂度:最坏 O ( m n ) O(mn) O(mn)(遍历所有单元格)
    • 空间复杂度:最坏 O ( m n ) O(mn) O(mn)(vis)
  • 实际上,访问右和下两个方向就可以(?),代码是访问了四个方向

解:

  • 方法1:DFS
    • 注意下标,参数传入i,j,k,循环时要避开这些变量名
    int dx[4] = {-1,1,0,0};
    int dy[4] = {0,0,-1,1};
    int movingCount(int m, int n, int k) {
        vector<vector<bool>> vis(m+2, vector<bool>(n+2, 0));
        return dfs(0,0,m,n,k,vis);
    }
    int dfs(int i, int j, int m, int n,int k, vector<vector<bool>> &vis){
        if(!isValid(i, j, m, n, k) || vis[i][j]) return 0;

        vis[i][j] = 1;
        int res = 1;
        for(int z = 0; z < 4; z++){
            int x = i + dx[z];
            int y = j + dy[z];
            res += dfs(x, y,m, n,k, vis);
        }
        return res;
    }
    int isValid(int x, int y, int m, int n, int k){
        if(x < 0 || y < 0 || x >= m || y >= n || sumRC(x, y) > k) return 0;
        return 1;
    }
    int sumRC(int i, int j){
        int sum = 0;
        sum += getSum(i);
        sum += getSum(j);
        return sum;
    }
    int getSum(int x){
        int sum = 0;
        while(x){
            sum += x%10;
            x /= 10;
        }
        return sum;
    }
  • 方法2:BFS:
    int dx[4] = {-1, 1, 0, 0};
    int dy[4] = {0, 0, -1, 1};
    struct node{
        int x, y;
        node(int x, int y): x(x), y(y){}
    };
    int movingCount(int m, int n, int k){
        vector<vector<bool>> vis(m+2, vector<bool>(n+2, 0));
        return bfs(0, 0, m, n, k, vis);
    }
    int bfs(int stx, int sty, int m, int n, int k, vector<vector<bool>>& vis){
        queue<node> q;
        q.push(node(stx, sty));
        vis[stx][sty] = 1;
        int res = 0;
        while(!q.empty()){
            node p = q.front();
            q.pop();
            res++;
            for(int i = 0; i < 4; i++){
                int x = p.x + dx[i];
                int y = p.y + dy[i];
                if(isValid(x, y, m, n, k) && !vis[x][y]){
                    vis[x][y] = 1; //立即设定vis[x][y]=1,防止后面队列访问(x,y)之前又有点能够走到x,y,此时队列里就有重复的了。注意但dijsktra算法是允许放入重复的。
                    q.push(node(x, y));
                }
            }
        }
        return res;
    }
    int isValid(int x, int y, int m, int n, int k){
        if(x < 0 || y < 0 || x >= m || y >= n || sumRC(x, y) > k) return 0;
        return 1;
    }
    int sumRC(int x, int y){
        int sum = getSum(x);
        sum += getSum(y);
        return sum;
    }
    int getSum(int x){
        int sum = 0;
        while(x){
            sum += x%10;
            x /= 10;
        }
        return sum;
    }

面试题45. 把数组排成最小的数

面试题45. 把数组排成最小的数

【自定义排序】

自定义排序规则:return (s1+s2) < (s2+s1);

  • 分别拼接(拼接后长度相等),小的(也即字典序小的)在前

  • 方法1:内置sort函数

lambda表达式:

    string minNumber(vector<int>& nums) {
        sort(nums.begin(), nums.end(), [](int x, int y){
            string t1 = to_string(x), t2 = to_string(y);
            return (t1+t2) < (t2+t1);
        });
        string res = "";
        for(int num: nums){
            res += to_string(num);
        }
        return res;
    }

自定义:

    struct cmp{
        bool operator()(int x, int y){
            string t1 = to_string(x), t2 = to_string(y);
            return (t1+t2) < (t2+t1);
        }
    };
    string minNumber(vector<int>& nums){
        sort(nums.begin(), nums.end(), cmp());
        string res = "";
        for(int num: nums){
            res += to_string(num);
        }
        return res;
    }
  • 方法2:快排
    string minNumber(vector<int>& nums){
        quickSort(nums, 0, nums.size()-1);
        string res = "";
        for(int num: nums){
            res += to_string(num);
        }
        return res;
    }
    void quickSort(vector<int>& nums, int l, int r){
        if(l >= r) return;
        int p = partition(nums, l, r);
        quickSort(nums, l, p-1);
        quickSort(nums, p+1, r);
    }
    int partition(vector<int>& nums, int l, int r){
        string pivot = to_string(nums[l]);
        int i = l+1, j = r;
        while(i <= j){
            while(i <= r && (to_string(nums[i]) + pivot) <= (pivot+(to_string(nums[i]))) ) i++;
            while(j > l && ((pivot+(to_string(nums[j])) < (to_string(nums[j]) + pivot)))) j--;
            if(i > j) break;
            swap(nums[i], nums[j]);
        }
        swap(nums[l], nums[j]);
        return j;
    }
  • 方法3:归并排序
    • 用while循环版的比较方便
    string minNumber(vector<int>& nums){
        tmp.reserve(nums.size());
        mergeSort(nums, 0, nums.size()-1);
        string res = "";
        for(int num: nums){
            res += to_string(num);
        }
        return res;
    }
    vector<int> tmp;
    void mergeSort(vector<int>&nums, int l, int r){
        if(l >= r) return;
        int mid = l + ((r - l) >> 1);
        mergeSort(nums, l, mid);
        mergeSort(nums, mid+1, r);
        merge(nums, l, mid, r);
    }
    void merge(vector<int>&nums, int l, int mid, int r){
        for(int p = l; p <= r; p++) tmp[p] = nums[p];
        int i = l, j = mid+1;
        int p = l;
        while(i <= mid && j <= r){
            string t1 = to_string(tmp[i]), t2 = to_string(tmp[j]);
            if((t1 + t2) <= (t2 + t1)) nums[p++] = tmp[i++];
            else nums[p++] = tmp[j++];
        }
        while(j <= r) nums[p++] = tmp[j++];
        while(i <= mid) nums[p++] = tmp[i++];
    }

面试题59 - II. 队列的最大值

面试题59 - II. 队列的最大值

【单调队列】

  • 方法:辅助队列
  • 思想:
    • 维护一个单调不递增队列,每次进来新元素时更新该队列(小于新元素的都出队,最后把新元素push到队尾),这样该队列的队首元素即是当前的最大元素
      • 而当前的最大元素pop后,该队列里的下一个元素就是pop掉最大元素后的当前最大值
    • 注意单调队列是双端队列(deque)
    queue<int> q1;
    deque<int> q2;
    MaxQueue() {    
    }
    
    int max_value() {
        return q2.empty()? -1:q2.front();
    }
    
    void push_back(int value) {
        q1.push(value);
        while(!q2.empty() && q2.back() < value){
            q2.pop_back();
        }
        q2.push_back(value);
    }
    
    int pop_front() {
        if(q1.empty()) return -1;
        int x = q1.front();
        q1.pop();
        if(!q2.empty() && q2.front() == x) q2.pop_front();
        return x;
    }

面试题61. 扑克牌中的顺子

面试题61. 扑克牌中的顺子

  • 思路1:排序,数0的个数cnt,数需要填补的个数need,判断cnt是否>=need
    • 注意判断不能出现重复的元素
    bool isStraight(vector<int>& nums) {
        sort(nums.begin(), nums.end());
        int i = 0;
        while(i < nums.size() && !nums[i]) i++;

        bool flag = 1;
        for(int j = i+1; j < nums.size(); j++){
            if(nums[j] == nums[j-1]) return 0;
            i -= (nums[j] - 1 - nums[j-1]);
        }
        return i >= 0;
    }
  • 思路2:记录最大最小值,差要小于5
    • 注意判断不能出现重复的元素
      • (1)遍历找最大最小值,set找重复
      • (2)排序,除0外的数组的尾和头是最大最小值,num[i]与num[i-1]比较找重复
    • 思路来源

你可能感兴趣的:(剑指offer,算法,leetcode,数据结构,c++,动态规划)