LeetCode刷题(12.12)

LeetCode 刷题日记

声明:本文主要是记录自己学习过程,以下的所有东西并不完全准确,如有不正确的,望指正,在这里先谢谢各位大佬了(膜拜)

题目一 下一个更大元素 I

难度 : 简单

思路如下

  • 暴力枚举
  • 哈希表小优化
  • 单调栈优化

暴力枚举 :
枚举第一个数组中得所有数字,枚举第二个数组,首先找到第一层枚举的数字,然后再在第二个数组中枚之后的数字,如果有更大的数字,然后就记录直接返回,否则就返回-1

代码如下

class Solution {
public:
    vector<int> nextGreaterElement(vector<int>& nums1, vector<int>& nums2) {
        int n = nums1.size();
        vector<int> ans(n);
        for (int i = 0; i < n; i ++) {
            auto pos = find(nums2.begin(), nums2.end(), nums1[i]);
            int t = -1;
            if (pos != nums2.end())
            {
                int j = pos - nums2.begin();
                for (int k = j; k < nums2.size(); k ++)
                    if (nums2[k] > nums1[i]) {
                        t = nums2[k];
                        break;
                    }
            }
            ans[i] = t;
        }
        return ans;
    }
};

时间复杂度 O ( n m ) O(nm) O(nm) nnums1的长度, mnums2的长度
空间复杂度 O ( n ) O(n) O(n) 用到了ans数组和若干变量


哈希表小优化

用一个哈希表来记录nums2中所有元素位置然后可以在 O ( 1 ) O(1) O(1)的时间复杂度下来查找该元素,之后思路和暴力枚举类似

代码实现

class Solution {
public:
    vector<int> nextGreaterElement(vector<int>& nums1, vector<int>& nums2) {
        unordered_map<int, int> hash;
        for (int i = 0; i < nums2.size(); i ++) hash[nums2[i]] = i;
        vector<int> res;
        for (int i = 0; i < nums1.size(); i ++)
        {
            bool flag = true;
            for (int j = hash[nums1[i]] + 1; j < nums2.size(); j ++)
                if (nums2[j] > nums1[i])
                {
                    res.push_back(nums2[j]);
                    flag = false;
                    break;
                }
            if (flag) res.push_back(-1);
        }
        return res;
    }
};

时间复杂度 O ( n 2 ) O(n^2) O(n2) 本质还是需要两层循环
空间复杂度 O ( n ) O(n) O(n) 上面类似


单调栈思路

我们注意到,我们要在nums2中找nums1中每个元素位置之后第一个的比该元素更大的数,此时我们就可以想到联想到单调栈

简单回顾一下单调栈的思路 :

  • 单调栈(Monotone Stack):一种特殊的栈。在栈的先进后出规则基础上,要求从 栈顶 到 栈底 的元素是单调递增(或者单调递减)
  • 一般此类的题目有相关的字眼, 像元素某一侧第一个,或者要找某种相较于本元素有单调特性的元素(本题题目就是右侧第一个大于该元素的数字)

常见模板(基于C++STL库)

stack<type> stk;
for (int i = 0; i < n; i ++)  {
	while (stk.size() && check(stk.top(), element)) stk.pop();
	if (stk.size()) work(); 
	else ...
	stk.push(element); //把当前元素推入栈
}

回归正题 本题的单调栈写法如下 :

class Solution {
public:
    vector<int> nextGreaterElement(vector<int>& nums1, vector<int>& nums2) {
        int n = nums2.size();
        stack<int> stk;
        unordered_map<int, int> num_larger;
        for (int i = n - 1; i >= 0; i --) {
            while (stk.size() && stk.top() <= nums2[i]) stk.pop();
            if (stk.size()) {
                num_larger[nums2[i]] = stk.top();
            } else {
                num_larger[nums2[i]] = -1;
            }
            stk.push(nums2[i]); // 将元素入栈
        }
        vector<int> res;
        for (int i = 0; i < nums1.size(); i ++) {
            res.push_back(num_larger[nums1[i]]);
        }
        return res;
    }
};

时间复杂度 O ( n ) O(n) O(n)
空间复杂度 O ( n ) O(n) O(n) 与上面类似


总结

  • 暴力思路不难,基本都能想到,剩下的就是代码能力了
  • 哈希表优化可以优化find部分,可以稍微减少一点时间,但不影响整体的时间复杂度
  • 注意到元素的单调性,我们想到单调栈思路,将时间复杂度优化

题目二 下一个更大元素 II

难度 : 中等

思路:

  • 单调栈

和上一道题目类似, 只是需要稍微改变一下实现方式就行,可以将数组拉长为自己的两倍,本代码使用的是循环数组,可以简化这一部分代码,但是可能比较难想到

实现代码如下

class Solution {
public:
    vector<int> nextGreaterElements(vector<int>& nums) {
        int n = nums.size();
        vector<int> res(n, -1);
        stack<int> stk;
        for (int i = 2 * n - 1; i >= 0; i --) {
            while (!stk.empty() && nums[i % n] >= stk.top()) stk.pop();
            if (i < n && stk.size()) res[i] = stk.top();
            stk.push(nums[i % n]);
        }
        return res;
    }
};

难点

  • 循环数组
  • 单调栈
  • 代码实现

题目三 下一个更大元素 III

难度 : 中等

思路:

  • C++算法库next_permutation() 暴力解决
next_permutation() 函数简介:
  • 第一个参数是要排列的起点迭代器,第二个是终点迭代器
  • 原地修改
  • 不是最开始的排列返回true, 一直到最后排列回来原点返回false
  • 很常见的库函数,要记住 !!

实现代码如下

class Solution {
public:
    using LL = long long;
    int nextGreaterElement(int n) {
        function<string(int)> get = [&](int x) -> string {
            string res;
            while (x) {
                int t = x % 10;
                res += '0' + t;
                x /= 10;
            }
            reverse(res.begin(), res.end());
            return res;
        };
        string res = get(n);
        while (next_permutation(res.begin(), res.end())) {
            LL t = stoll(res);
            if (t <= INT32_MAX && t > n) return t;
        }
        return -1;
    }
};

时间复杂度 O ( n ! ) O(n!) O(n!) n是该数字的长度,就是最坏情况下的组合数


注意: 这个时间复杂度 很高,需要很大的代价,不建议使用这个算法

其他简单写法待补充

题目四 下一个更大元素 IV

难度 : 困难

思路:

  • 单调栈

我们要找的是某个数后面的第二个比这个数大的数字,我们先思考第一个比这个数字更大的数的情况,显然是单调栈的,思考如果用一个递减的单调栈s,当枚举一个数字x时,可能会在栈中弹出一些数字, 这些数字都是比枚举的x小的(可以自己模拟一下),而且是最接近的(下标),所以这些弹出去的数字右侧的第一个比自己更大的数字就是x,但是我们怎么来找在x之后的满足大于前面已经弹出的数字的数呢?我们可以把之前弹出去的数字记录下来,保存到一个新的栈t中,并且保持这个栈是也递减的,我们枚举x的时候去查看t的栈顶top是否满足x > top, 如果满足,就说明top的第二大的数字就是x,之后把已经top出栈,把s新弹出(要保持单调减性)的元素入栈t,反复操作就可以得到最终的答案数组res


为什么是正确的呢? ( 注意: 非证明,只是说明)

  • 解释:前面已经提到,弹出去数字是小于s栈顶的,并且栈顶是第一个大于他们的数,在下一个nums中枚举的数字x时,首先处理的是栈t中的元素,如果x是大于t栈顶时,说明x就是大于t的第二个数

为什么t也要保持单调递减呢?

  • 解释: 前面提到,先判断x是不是大于栈t的栈顶,我们要保证小于x的要全部出栈,所以栈底到栈顶一定是从大到小的,这样能确保小于x的全部出栈

tips: 为了方便处理,栈中存储的是下标注意下面栈的操作所有的变量表示其实都是该变量的下标

算法的具体实现步骤:

  1. 先初始化两个栈和答案数组(初始化为-1,如果没更新就说明右边没有符合条件的数),注意这两个栈用数组的方式实现
  2. 枚举nums中的每个数字x
  3. 先判断x是不是大于栈t的栈顶,如果是,就直接记录答案,并且将栈顶出栈,否则就跳过这一步
  4. 处理s栈,就是维护单调递减性质,我们首先找到第一个小于x的下标idx,然后将idxend这一段整体插入栈t的顶部 (因为s是递减的并且小于x的已经全部出了栈t所以直接插入并不会影响栈t的单调性) ,然后将栈s顶部出栈,并且将x入栈

栈的数组形式: 我们把数组的左边当作栈底最右端当作栈顶,入栈就相当于push_back(),出栈一个元素相当于pop_back(), 处理的时候把它当作栈处理,操作简便

实现代码:

class Solution {
public:
    vector<int> secondGreaterElement(vector<int>& nums) {
        int n = nums.size();
        vector<int> res(n, -1), s, t;
        for (int i = 0; i < n; i ++) {
            while (t.size() && nums[t.back()] < nums[i]) {
                res[t.back()] = nums[i];
                t.pop_back();
            }
            int j = s.size();
            while (j && nums[s[j - 1]] < nums[i]) j --;
            t.insert(t.end(), s.begin() + j, s.end());
            s.resize(j);
            s.push_back(i);
        }
        return res;
    }
};

时间复杂度 O ( n ) O(n) O(n)

代码tips :

  • 时刻注意栈中存储的是下标,所以res[t.back()]就不足为奇了
  • t.insert(t.end(), s.begin() + j, s.end());就是将栈s中已经出栈的连续元素原封不动的插入t的末尾
  • resize(n)函数中,如果 n < size那么会将n + 1, n + 2...size这些全部删除,相当于一次pop全部


总结

  • 单调栈使用很灵活,有很多种形式,积累方法
  • 积累思路
  • 要增强代码能力
  • 要锻炼思维
  • 要了解容器的各种方法

终于结束了 呼~~!!!

你可能感兴趣的:(leetcode,哈希算法,散列表,数据结构,笔记,c++)