leetcode 41-50

41. 缺失的第一个正数

分析

将每个数放到hash表中, 从小到大枚举每个正整数, 直到找到第一个没找到的正整数
第一步, 将每个数放到hash表中, O(n).
第二步, 枚举最多只会枚举到 n + 1, O(n)
因此整个时间复杂度O(n)

code

class Solution {
public:
    int firstMissingPositive(vector<int>& nums) {
        unordered_set<int> hash;
        for (auto &x : nums) hash.insert(x);

        int res = 1;
        while (hash.count(res)) res ++;

        return res;
    }
};

进阶

class Solution {
public:
    int firstMissingPositive(vector<int>& nums) {
        int n = nums.size();    
        if (!n) return 1;

        for (auto& x : nums) 
            if (x != INT_MIN)
                x -- ;
                
        for (int i = 0; i < nums.size(); i ++ ) {
            while (nums[i] >= 0 && nums[i] < n && nums[i] != nums[nums[i]]) swap(nums[i], nums[nums[i]]);

        }

        for (int i = 0; i < n; i ++ )   
            if (nums[i] != i)
                return i + 1;
        return n + 1;
    }
};

42. 接雨水

分析

先将前面柱子高度压入stack中,
在这里插入图片描述
到了第4根柱子, 计算当前栈顶元素和上一个元素的高度差,
在这里插入图片描述
第2次再加上鼠标所指的第2部分面积, 宽度 = (当前柱子的左边界~栈里往前数下一根柱子的右边界) * 高度(min(栈中下一根柱子的高度, 当前扫描的柱子的高度) - 当前栈中的柱子高度)
然后将第4根柱子加入到栈中, 然后栈中只有两个元素, 阶梯形
在这里插入图片描述
总结:
宽度 = (当前元素坐标 和 栈中下一个元素坐标)差
高度 = (当前元素高度 和 栈中下一个元素的高度)差
在这里插入图片描述

由于当前第i个元素, 不需要考虑进res中进行操作, 因此最后先统计res, 再stk.push(i)

code

class Solution {
public:
    int trap(vector<int>& height) {
        stack<int> stk;
        int res = 0;
        for (int i = 0; i < height.size(); i ++ ){
            int last = 0;
            while (stk.size() && height[stk.top()] <= height[i]){
            	// 	注意是拿 height[stk.top()] - last
                res += (height[stk.top()] - last) * (i - stk.top() - 1); // 统计面积
                last = height[stk.top()];
                stk.pop();
            }
			// 注意最后一段区间高度 = height[i] - last, 因为当前栈顶元素高度 > height[i]了
            if (stk.size()) res += (i - stk.top() - 1) * (height[i] - last); // 统计最后一部分面积
            stk.push(i);
        }
        return res;
    }
};

43. 字符串相乘

分析

高精度乘法 模版题

code

class Solution {
public:
    string multiply(string num1, string num2) {
        vector<int> A, B;
        int n = num1.size(), m = num2.size();
        for (int i = n - 1; i >= 0; i --) A.push_back(num1[i] - '0');
        for (int i = m - 1; i >= 0; i --) B.push_back(num2[i] - '0');

        vector<int> C(n + m);

        for (int i = 0; i < n; i ++)
            for (int j = 0; j < m; j ++)
                C[i + j] += A[i] * B[j];

        for (int i = 0, t = 0; i < C.size(); i ++){
            t += C[i]; // 这里不能取模, 因为t表示总的和, 下面要需要 / 10 计算进位
            C[i] = t % 10;
            t /= 10;
        }

        int k = C.size() - 1;
        while (k && !C[k]) k --;

        string res;
        for (int i = k; i >= 0; i --) res += C[i] + '0';
        return res;
    }
};

44. 通配符匹配

分析

f[i][j] : s[1 ~ i] 和p[1 ~ j] 是否匹配
要分情况考虑p[j]是否*
第2种情况p[j]不是*的时候, 比较好考虑, 只需要判断s[i] 是不是与p[j]相等, 然后|| f[i - 1][j - 1];
第1种情况, 比较复杂, 因为我们不知道当前*可以匹配多少个字符(*可以匹配任意多个字符)
需要枚举下* 匹配多少个字符

  1. 匹配0个字符 f[i, j - 1]
  2. 匹配1个字符 f[i - 1, j - 1]
  3. 匹配k个字符 f[i - k, j - 1]
  4. 全部匹配 f[0, j - 1]

f [ i , j ] { p [ j ] 是 ‘ ∗ ′ :   f [ i , j − 1 ] ∣ ∣ f [ i − 1 ] [ j − 1 ] ∣ ∣ f [ i − 2 ] [ j − 1 ] ∣ ∣ . . . ∣ ∣ f [ 0 , j − 1 ] p [ j ] 不 是 ‘ ∗ ’ :   s [ i ] = = p [ j ]   & &   f [ i − 1 ] [ j − 1 ]                               f[i, j]\left\{\begin{matrix} p[j]是`*': f[i, j - 1]\quad||\quad f[i - 1][j - 1]\quad||\quad f[i - 2][j - 1]\quad||\quad ...\quad||\quad f[0, j - 1]\\ p[j]不是 `*’: s[i] == p[j] \&\& f[i - 1][j - 1]               \\ \end{matrix}\right. f[i,j]{p[j]: f[i,j1]f[i1][j1]f[i2][j1]...f[0,j1]p[j]: s[i]==p[j] && f[i1][j1]               
由于上面的状态数量 O ( n 2 ) O(n^2) O(n2), 转移数量 O ( n ) O(n) O(n), 因此时间复杂度 O ( n 3 ) O(n^3) O(n3)
此题优化方式和leetcode10/完全背包问题相似

优化

考虑f[i - 1][j]的状态去优化
因为当前p[j] == '*', 所以f[i - 1][j]的表达式, 只需要将f[i][j]表达式中的i改成i - 1
f [ i − 1 ] [ j ] = f [ i − 1 ] [ j − 1 ]   ∣ ∣   f [ i − 2 ] [ j − 1 ]   ∣ ∣   . . . .   ∣ ∣   f [ 0 ] [ j − 1 ] f[i - 1][j] = f[i - 1][j - 1] || f[i- 2][j - 1] || .... || f[0][j - 1] f[i1][j]=f[i1][j1]  f[i2][j1]  ....  f[0][j1]

可以发现
f[i][j] = f[i][j - 1] || f[i - 1][j - 1] || f[i - 2][j - 1] || … || f[0, j - 1]
f[i - 1][j] = f[i - 1][j - 1] || f[i- 2][j - 1] || … || f[0][j - 1]
因此f[i][j] = f[i][j - 1] || f[i - 1][j]

code

f[0][0] = true, 因为两个空串是匹配的
f[0][j]是有意义的, 因为第2个字符串 可能很多***, 所有***可以匹配空串, 所以f[0][j] 可能是true
但是f[i][0], 当i > 0的时候, 一定是false
所以遍历的时候i从0开始, j 从1开始(j = 0, 初始化就是false)

class Solution {
public:
    bool isMatch(string s, string p) {
        int n = s.size(), m = p.size();
        s = ' ' + s, p = ' ' + p;
        vector<vector<bool>> f(n + 1, vector<bool>(m + 1));
        f[0][0] = true;
        
        for (int i = 0; i <= n; i ++ )
            for (int j = 1; j <= m; j ++ ){
                if (p[j] == '*')
                    f[i][j] = f[i][j - 1] || i && f[i - 1][j];
                else 
                    f[i][j] = (s[i] == p[j] || p[j] == '?') && i && f[i - 1][j - 1];
            }
        return f[n][m];
    }
};

45. 跳跃游戏 II

分析

可以发现贪心是错误的
在这里插入图片描述
需要3步,到达终点
而下面的方式, 只用2步可以到达终点
在这里插入图片描述
题目本身是一个图论的问题, 0的位置是一个点可以跳2步, 表示有两条边, 相当于给了我们一张图, 求起点到终点的最短距离. 如果用图论的方法去做, 由于每个点需要向后面的点连边, 边数 O ( n 2 ) O(n^2) O(n2), 这样的话时间复杂度 O ( n 2 ) O(n^2) O(n2), yxc试过, 会超时
所以需要想办法优化一下, 想一下有没有dp的做法, 先不考虑时间的问题, 考虑下怎么做dp
比如说从0这个位置, 从 0 → 1 0 \to 1 01, 想要计算的f[i], 其实是从1到终点最少需要多少步数
再从 0 → 2 0 \to 2 02, 想要计算的f[i], 还考虑从2到终点最少需要多少步数
从这些所有的选择方案中, 选择最小值
这启发我们可以用f[i]表示 从i到终点的最小步数(也反着表示, 从起点到i的最小步数)
然后我们倒着推一遍, 求f[0]就是答案. 但是这个做法也是O(n^2)的, 最坏的情况下, 需要将后面的所有点枚举一遍, 只要我们每个点的步长足够长, 我们需要将后面的都算一遍, 也会超时
继续优化
看f[i]有没有其他性质, 直觉上讲(估计) f[i]有一定单调性

fi]:从起点到i的最小步数
f[i]递增的话只会递增1, 因为前后两个点是紧挨着的, 并且前面点的步数 >0, 因为到达不了后面那个点, 因此+1步必定可以到达挨着的后面点

命题:f[i]是单调的
证明:反证法, 如果f[i]不是单调的, 那么必然存在两个相邻的点, 使得第一个点的值 > 第二个点的值(从起点跳到第一个点的步数 > 从起点跳到第2个点步数). 可以观察后一个点最后一步是从哪个一个点跳过来的, 由于后一个点比上一个点步数要小, 不可能从上一个点跳过来, 必然是从 前面某个点跳过来, 那么 前面某个点的范围如下
在这里插入图片描述
这样的话, 就推出了矛盾, 因为从起点到绿色点的步数 <= 红色点, 最多也只会相等, 不可能比起点到红色点的步数大.

f[i]的区间 证明了是一段一段后, 计算后面能跳右边界, 肯定是基于前面的能跳距离的max在这里插入图片描述
每次可以求出一个边界, 求出边界后, [前一段能跳的区间 + 1, 边界] = 前一段能跳的步数 + 1
每段如上述方法赋值 O ( n ) O(n) O(n), 整个线段扫描一遍 O ( n ) O(n) O(n)

code

写代码的时候, 求f[i]的时候, 枚举上一段的变量j, 当上一段距离跳不到i的时候, j一直往后走, 直到j可以跳到i为止, 停下来, 停下来的, 第一个找到的j就是最小步数
f[0] = 0, 因为从起点跳到0的步数,不用跳, 就是0

class Solution {
public:
    int jump(vector<int>& nums) {
        int n = nums.size();
        vector<int> f(n);

        for (int i = 1, j = 0; i < nums.size(); i ++ ){
            while (j + nums[j] < i) j ++;// 扫描前面的j, 找到第一个j, 能够使得 j + nums[j] >= i
            // 即表示是最小的j, 能够跳到i
            f[i] = f[j] + 1; // 那么从起点到i的步数 就是从起点到j的步数 + 1
        }
        return f[n - 1];
    }
};

46. 全排列

分析

考虑dfs的状态

  1. 我们需要考虑每个位置上填了什么, int[], 用来表示每个位置
  2. 我们当前看了第几位, u
  3. 当前哪些数已经用过来了 bool st[]
    然后要考虑, 每个分支里, 怎么把分支实现出来, 对于当前分支, 枚举下哪些分支没有被用过
    在这里插入图片描述

code(顺序1 : 枚举每个数, 填哪个位置)

注意leetcode中public:中只能声明vector, 定义vector大小, 需要在函数中

class Solution {
public:

    vector<vector<int>> ans;
    vector<int> path;
    vector<bool> st;

    vector<vector<int>> permute(vector<int>& nums) {
        path = vector<int>(nums.size());
        st = vector<bool>(nums.size());

        dfs(nums, 0);

        return ans;
    }

    void dfs(vector<int>& nums, int u) {
        if (u == nums.size()) {
            ans.push_back(path);
            return;
        }

        for (int i = 0; i < nums.size(); i ++ ) {
            if (st[i] == false) {
                path[u] = nums[i];
                st[i] = true;
                dfs(nums, u + 1);
                st[i] = false;
            }
        }
    }
};

code(顺序2 :枚举每个位置填哪个数)

class Solution {
public:
    vector<vector<int>> res;
    vector<int> path;
    vector<bool> st;
    vector<vector<int>> permute(vector<int>& nums) {
        path = vector<int>(nums.size());
        st = vector<bool> (nums.size());

        dfs(nums, 0);
        return res;
    }

    void dfs(vector<int>& nums, int u){
        if (u == nums.size()){
            res.push_back(path);
            return;
        }

        for (int i = 0; i < nums.size(); i ++ ){
            if (!st[i]){
                path[u] = nums[i];
                st[i] = true;
                dfs(nums, u + 1);
                st[i] = false;
            }
        }
    }
};

47. 全排列 II

分析

dfs顺序问题

有两种dfs顺序,

  1. 3个位置, 依次枚举每个位置填哪个数
  2. 枚举每个数, 填写到哪个位置上
    如果题目要求我们按照字典序 输出, 我们一定要按照每个位置上填哪个数, 第一个位置上填1行不行, 然后将第1个位置上填1的方案全都找出来, 然后第2个位置上填接着的方案.
    如果枚举每个数填到每个位置上, 不一定能保证字典序了, 因为不一定会把第一个位置上最小的数输出
    这一题按照第2中顺序

重复元素问题

有重复元素, 比如 1 1 , 1 2 , 2 1^1,1^2, 2 11,12,2,
1 1 1 2 _ 1^11^2 \_ 1112_
1 2 1 1 _ 1^2 1^1 \_ 1211_
是同一种方案
重复的原因是因为, 我们将两个1看成不同的1
为了让我们不搜索出重复的元素, 必须限定下, 对于相同的数, 规定下一种顺序
比如
1 1 , 1 2 , 1 3 1^1, 1^2, 1^3 11,12,13, 在搜索的时候, 一定要保证 1 1 _ _ _ , 1 2 _ _ , _ _ 1 3 1^1\_\_\_, 1^2\_\_, \_\_1^3 11___,12__,__13的顺序去搜索(中间可能隔一些数)
这样可以保证对于相同的数来说, 只会枚举一种顺序, 就可以保证唯一了
所以我们发现, 只需要保证他们的相对顺序不变, 就可以了

怎么能够保证所有相同数的相对顺序不变呢?
比如说有这么多 1 1 , 1 2 , 1 3 , 1 4 , 1 5 1^1, 1^2, 1^3, 1^4, 1^5 11,12,13,14,15, 我们需要用第1次没有用过的1, 用相同的数的时候, 一定要保证从前往后挨个用, 不能跳着用
为了方便, 对原数组排序, 这样相同的数就排在一起了, 在枚举的的时候, 如果不是第一个没有用过的数, 直接略过
总而言之, 判断一个数是否是第1次没有用过,是的话就用它, 不是的话就略过
具体操作:
怎么看num[i]是不是第1次没有用过呢, 如果nums[i - 1] == nums[i], 并且nums[i - 1]没有被用过, 说明我们当前这个数nums[i]不是第1个没有被用过的数, 就不要用它
nums[i - 1] == nums[i] && !st[i - 1] 那么就直接continue (说明不是第1个没有被用过的数)

code (顺序2)

class Solution {
public:
    vector<vector<int>> res;
    vector<int> path;
    vector<bool> st;
    vector<vector<int>> permuteUnique(vector<int>& nums) {
        sort(nums.begin(), nums.end());
        st = vector<bool> (nums.size());
        path = vector<int> (nums.size());
        dfs(nums, 0);
        return res;
    }

    void dfs(vector<int>& nums, int u){
        if (u == nums.size()){
            res.push_back(path);
            return ;
        }

        for (int i = 0; i < nums.size(); i ++ ){
            if (!st[i]){
                if (i && nums[i] == nums[i - 1] && !st[i - 1]) continue;
                st[i] = true;
                path[u] = nums[i];
                dfs(nums, u + 1);
                st[i] = false;
            }
        }
    }
};

48. 旋转图像

分析

矩阵转置, 再对每一层翻转

code

class Solution {
public:
    void rotate(vector<vector<int>>& matrix) {
        int n = matrix.size();
        for (int i = 0; i < n; i ++ )           
            for (int j = 0; j < i; j ++ )
                swap(matrix[i][j], matrix[j][i]);
        for (int i = 0; i < n; i ++ )
            reverse(matrix[i].begin(), matrix[i].end());
    }
};

49. 字母异位词分组

分析

建一个hash表, 然后遍历原字符串数组, 用一个变量保存当前字符串的最小表示, 作为hash表的key, 然后将原字符串塞到value里
再遍历一遍hash表, 将value(vector<string>) 塞到答案中

code

class Solution {
public:
    vector<vector<string>> groupAnagrams(vector<string>& strs) {
        unordered_map<string, vector<string>> hash;
        for (auto& str : strs){ // 遍历原字符串数组
            string nstr = str; // 新建代表元
            sort(nstr.begin(), nstr.end()); // 最小表示
            hash[nstr].push_back(str);
        }
        vector<vector<string>> res;
        for (auto& item : hash){
            res.push_back(item.second);
        }
        return res;
    }
};

50. Pow(x, n)

快速幂模版题, 注意n可能会是负数INT最大值, abs后可能会爆, 所以用abs(LL(n))

code

class Solution {
public:
    double myPow(double x, int n) {
        typedef long long LL;
        bool is_minus = false;
        if (n < 0) is_minus = true;
        double res = 1;
        for (LL k = abs(LL(n)); k; k >>= 1){
            if (k & 1) res *= x;
            x *= x;
        }
        if (is_minus) return 1.0 / res;
        return res;
    }
};
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值