leetcode hot100刷题记录

# leetcode hot 100

哈希

链表

  • 相交链表:找出相交点,可以先计算长度,让较长的链表先走,然后再一起走
  • 反转链表:没什么好说的。迭代和递归都要懂
  • 环形链表:快慢指针一起跑就好了,能碰到就代表有环
  • 环形链表 II:不止要判断是否有环,而且要给出环的入口。先判断有环,有环后再让一个指针从头开始,然后快慢指针一步步走,碰到的就是环的入口(难的是数学证明)
  • 合并两个有序链表:没什么好说的
  • 两数相加:将和存到list1就好了,记录进位,到最后,如果还有进位还是1,就新增
  • 删除链表的倒数第 N 个结点:注意边界情况,则链表长度是否大于等于N。a先走到第n个点,b开始指向链表头,a、b开始一起一步步走,b到链表尾,a就是结果。l = n + m,倒数第n,就是正数第l-n(m),a先走n,b再下场,这样一起走,a后面刚好走l-n到链表尾,b刚好到第l-n个点
  • 两两交换链表中的节点:就是有点恶心,本身不难。迭代很恶心,递归简单一点。
  • K 个一组翻转链表:其实就是上一题的进阶,用递归简单点,先判断当前节点开头的链表是否有k个,不够直接返回,够的话先记下这k个节点的开头s和结尾e,翻转这个区间,然后对e->next递归调用,得到结果i,然后链接s->next=i,返回e
  • 随机链表的复制:随机链表其实就是比普通链表多了一个random节点,然后这个random节点随机指向链表中的一个节点。可以用一个map保存旧节点映射到新节点的,第一次就按普通链表复制,第二次遍历旧链表,查看random链接情况,根据map,按照映射关系链接新节点的random
  • 排序链表:n2要么用插入、要么冒泡。nlogn:自底向上的归并。n2:自顶向下的归并(先快慢指针划分链表,然后递归对两个链表排序)
  • 合并 K 个升序链表:就是合并两个升序链表的升级,两两合并就好。但是一般是选择当前最短的两个链表合并,这个可以通过堆来完成。
  • LRU 缓存:经典题目,值得背诵。首先要记得是map+双链表完成的。越接近链表头部,就是最近用的。map存的是key到链表节点的映射,链表存的是key和value
    • put操作:
      • key已存在时,通过key获取到节点node,更新value,然后将node移动到链表头
      • key未存在时,且未满时,新建节点node,将node插入到链表尾,并且更新map(添加key到node的映射)
      • key未存在时,且已满时,同上上面的情况一样(新建节点node,将node插入到链表尾,并且更新map(添加key到node的映射)),只是要将将链表尾节点从链表删除,更新map(删除链表尾节点中的key到链表尾节点的映射)
    • get操作:
      • 通过key获取到节点node,获取value,然后将node移动到链表头
    • 链表设计:强烈建议一个head一个tail作为头尾,并实现以下函数:
      • removeNode(node) : 移除节点node
      • removeTail(): 移除尾部节点(不是tail,而是tail->prev)
      • addToHead(node): 往链表头部插入节点node
      • moveToHead(node): 将node从原来位置删除,并插入到链表头

请你设计并实现一个满足 LRU (最近最少使用) 缓存 约束的数据结构。

实现 LRUCache 类:

  • LRUCache(int capacity)正整数 作为容量 capacity 初始化 LRU 缓存
  • int get(int key) 如果关键字 key 存在于缓存中,则返回关键字的值,否则返回 -1
  • void put(int key, int value) 如果关键字 key 已经存在,则变更其数据值 value ;如果不存在,则向缓存中插入该组 key-value 。如果插入操作导致关键字数量超过 capacity ,则应该 逐出 最久未使用的关键字。

函数 getput 必须以 O(1) 的平均时间复杂度运行。

二叉树

递归不解释,迭代:

主要用栈模拟递归过程,注意就是节点访问(visit)和出栈的时机

前序:visit:入栈前先visit,出栈:当一直往左走,无路可走时,出栈一个节点,取其右孩子(注意,栈中的节点都是已经访问过的

中序:visit:出栈之后再visit,出栈:当一直往左走,无路可走时,出栈一个节点,取其右孩子(注意,栈中的节点都是没有访问过的,并且他们的左孩子都已入栈)

后序:这个有点特殊,需要用一个prev节点记录上一次visit的节点,这样当root->right == prev,证明root可以出栈,也可以访问了。

技巧

class Solution {
public:
    void nextPermutation(vector<int>& nums) {
        int len = nums.size();
        if (len < 2) return ;

        int i = len - 2; 
        while (i >= 0 && nums[i] >= nums[i + 1]) {
            --i;
        }
        
        // 此时nums[i] < nums[i+1],代表nums[i+1, n)中必有数nums[k]大于nums[i]
        // 尽量从后面找nums[k], 此时
        int k = len - 1;
        if (i >= 0) {
            while (nums[i] >= nums[k]) {
                --k;
            }
            swap(nums[i], nums[k]);
        }

        reverse(nums.begin()+i+1, nums.end());
    }
};

123456
123465
123546
...
654321
可以看到有这样的关系:123456 < 123465 < 123546 < ... < 654321。

算法推导
如何得到这样的排列顺序?这是本文的重点。我们可以这样来分析:

我们希望下一个数 比当前数大,这样才满足 “下一个排列” 的定义。因此只需要 将后面的「大数」与前面的「小数」交换,就能得到一个更大的数。比如 123456,将 5 和 6 交换就能得到一个更大的数 123465。
我们还希望下一个数增加的幅度尽可能的小,这样才满足“下一个排列与当前排列紧邻“的要求。为了满足这个要求,我们需要:在尽可能靠右的低位 进行交换,需要从后向前查找将一个尽可能小的「大数」 与前面的「小数」交换。比如 123465,下一个排列应该把 5 和 4 交换而不是把 6 和 4 交换将「大数」换到前面后,需要将「大数」后面的所有数 重置为升序,升序排列就是最小的排列。以 123465 为例:首先按照上一步,交换 5 和 4,得到 123564;然后需要将 5 之后的数重置为升序,得到 123546。显然 123546 比 123564 更小,123546 就是 123465 的下一个排列
以上就是求 “下一个排列” 的分析过程。

算法过程
标准的 “下一个排列” 算法可以描述为:

从后向前 查找第一个 相邻升序 的元素对 (i,j),满足 A[i] < A[j]。此时 [j,end) 必然是降序
在 [j,end) 从后向前 查找第一个满足 A[i] < A[k] 的 k。A[i]、A[k] 分别就是上文所说的「小数」、「大数」
将 A[i] 与 A[k] 交换
可以断定这时 [j,end) 必然是降序,逆置 [j,end),使其升序
如果在步骤 1 找不到符合的相邻元素对,说明当前 [begin,end) 为一个降序顺序,则直接跳到步骤 4
该方法支持数据重复,且在 C++ STL 中被采用。

  • 寻找重复数: 数组元素与下标组成有环链表,变成找出环的入口。变成快慢指针找有环链表环的入口。
 0 1 2 3 4
[3,1,3,4,2]
0->3->4->2->3

动态规划

  • 打家劫舍:dp[i] = max(dp[i-1], dp[i - 2] + nums[i])

  • 完全平方数: 完全背包,物品是各个完全平方数,而背包大小是目标和。用最少的物品装满背包,物品可以重复利用。用一维的话,背包从小到大就是完全背包.从大到小,就是01背包。dp[j] 就是j的答案。

  • 零钱兑换:同上,一样是完全背包,一样是求物品数最少

  • 单词拆分:dp[j]代表s[0~j]可以划分,每到一个位置对所有单词遍历,看看是否能划分

多维动态规划

  • 不同路径: dp[i] [j] = dp[i-1] [j] + dp[i] [j-1]

  • 最小路径和: grid[i] [j] += min(grid[i-1] [j], grid[i][j-1])

  • 最长回文子串: 对每个字符和作为中心,从两边展开获取回文串。n^2。(回文串长度既可以是单数也可以是双数)

  • 最长公共子序列: dp[i] [j] = (t1[i] == t2[j]) ? dp[i-1] [j-1] + 1 : min(dp[i-1] [j], dp[i] [j - 1])

  • 编辑距离: 要知道删除和增加和修改都是等价的. word1[i] == word2[j] , dp[i] [j] = dp[i-1] [j-1], 否则,dp[i] [j] = min(dp[i-1] [j-1], dp[i-1] [j], dp[i] [j]) + 1,注意不要漏了dp[i-1] [j-1]

  • 数据流的中位数:用两个堆来完成,一个大根堆,一个小根堆。1、大根堆放数据流的较小半部分,小根堆放数据流的较大的半部分。2、大根堆元素个数始终最多比小根堆元素个数多一。插入元素时主要要维持这两个性质。

  • 前 K 个高频元素:map统计数字出现的频次,然后维护大小为k的小根堆。

  • 数组中的第K个最大元素:维护大小为k的小根堆 或者 基于快排

class Solution {
public:
    /*
    对于每条柱heights[i],以他为中心,最大面积是 heights[i] = (right_i - left_i + 1)
    right_i是从i开始,往右找,第一个heights[right_i] < heights[i]
    left_i是从i开始,往左找,第一个heights[left_i] < heights[i]
    
    暴力法:对每个i,找出left_i和right_i,时间复杂度为n^2
    单调增栈:栈里的元素都是单调递增的。
    遍历到i元素时,如果比栈顶(cur)小的元素,此时i就是right_i。而栈顶的下一个元素就是left_i(将cur出栈,获取left_i)。计算获取以heights[i]为高的矩形面积。
    
    前面添加0:让第一个元素也能找到left_i
    后面添加0:让最后一个元素也能找到right_i
    */
    int largestRectangleArea(vector<int>& heights)
    {
        int ans = 0;
        vector<int> st;
        // 前后加入0
        heights.insert(heights.begin(), 0);
        heights.push_back(0);
        for (int i = 0; i < heights.size(); i++)
        {
            while (!st.empty() && heights[st.back()] > heights[i])
            {
                int cur = st.back();
                st.pop_back();
                int left = st.back();
                int right = i;
                // 因为是单调增栈,所以此时 height[left] < height[cur] > height[right]
                ans = max(ans, (right - left + 1) * heights[cur]);
            }
            st.push_back(i);
        }
        return ans;
    }
};

滑动窗口

子串

class Solution {
public:
    vector<int> maxSlidingWindow(vector<int>& nums, int k) {
        int n = nums.size();
        int cur = 0;
        vector<int> res(n - k + 1);
        deque<int> dq;
        // 单调减队列(队头到队尾)
        for (int i = 0; i < n; ++i) {
            // dq.back() < i && nums[dq.back()] < nums[i], 此时:
            // dq.back()要么在(i-k, i]范围内,但是这个范围中最大值肯定不是nums[dq.back()],因为nums[i]>nums[dq.back()]
            // dq.back()要么在(i-k, i]不在范围内,更可以删除(如果nums[dq.back()] >= nums[i],就给下个for循环删除)
            while (!dq.empty() && nums[dq.back()] < nums[i]) {  
                dq.pop_back();
            }
            dq.push_back(i);
            if (i >= k - 1) {   // 从第k个数才开始记录
                while (dq.front() <= i - k) {   // 将超出(i-k, i]范围的数字删除
                    dq.pop_front();
                }
                res[cur++] = nums[dq.front()];
            }
        }
        return res;
    }
};
class Solution {
public:
    string minWindow(string s, string t) {
        int matchChar = 0;  // 目前已匹配的字符数
        int res_left = -1;  // 目前匹配的最小区间的左区间
        int res_len = s.size() + 1; // 目前匹配的最小区间的长度
        int left = 0, right = 0;    // 滑动窗口
        map<char, int> tmap;
        map<char, int> smap;
        for (char c : t) {
            tmap[c]++;
        }
        while (right < s.size()) {
            while (right < s.size() && matchChar < t.size()) {  // 一直匹配,直至s[left, right]能完全覆盖,或者right右区间超出范围
                int cnt = tmap[s[right]];
                if (cnt > 0) {  // 只记录t有的字符
                    if (smap[s[right]] < cnt) { // 对于重复的字符c,当区间中c的数量已超t中c的数量,就不再增加匹配字符数
                        ++matchChar;
                    } 
                    ++smap[s[right]];   // 记录区间中的字符对应的数量
                }
                ++right;
            }

            while (matchChar == t.size()) { // 缩小,直至区间不能完全覆盖t(通过匹配字符数来判断)
                int cnt = tmap[s[left]];
                if (cnt > 0) {
                    if (--smap[s[left]] < cnt) {
                        --matchChar;
                    }
                }
                ++left;
            }

            if (right - left + 1 < res_len) {   // 记录最小区间[left - 1, right)
                res_len = right - left + 1;
                res_left = left - 1;
            }

        }
        if (res_left == -1) {
            return "";
        } else {
            return s.substr(res_left, res_len);
        }
    }
};
posted @ 2024-07-12 16:47  DavidJIAN  阅读(117)  评论(0)    收藏  举报