leetcode hot 100

leetcode hot 100

哈希

双指针

滑动窗口

子串

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);
        }
    }
};

普通数组

矩阵

链表

  • 相交链表:找出相交点,可以先计算长度,让较长的链表先走,然后再一起走
  • 反转链表:没什么好说的。迭代和递归都要懂
  • 环形链表:快慢指针一起跑就好了,能碰到就代表有环
  • 环形链表 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可以出栈,也可以访问了。

图论

回溯

二分查找

一个重要的结论:

对于每条柱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]

答案肯定是以某个height[i]为高的矩形,遍历所有的可能即可。

根据这个结论,可以利用单调增栈的方式,找出每个i的left_i和right_i。

为了让第一个能找到left_i和最后一个元素能找到right_i,在数组前后各添加一个0.

单调增栈出入栈。一个元素出栈时就能找到left_i和right_i,此时计算面积即可。

  • 每日温度: 单调递减栈
  • 最小栈:两个栈模拟,一个正常放,一个最小栈min。每次入栈,先比较栈顶,比栈顶小,入栈。出栈一样,跟栈顶比较,相等,出栈。
  • 有效的括号:略

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

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

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

贪心算法

动态规划

  • 打家劫舍: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], gridi)
  • 最长回文子串: 对每个字符和作为中心,从两边展开获取回文串。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]

技巧

class Solution {
public:
    void sortColors(vector<int>& nums) {
        int p0 = 0, p1 = 0;
        for (int i = 0; i < nums.size(); ++i) {
            if (nums[i] == 0) {
                swap(nums[i], nums[p0]);
                if (p0 < p1) {              // 如果p0 < p1,证明nums[p0]=1
                    swap(nums[i], nums[p1]);// 将1换回来
                }
                ++p0;   // 因为是0,p0和p1都需要更新
                ++p1;
            } else if (nums[i] == 1) {
                swap(nums[i], nums[p1]);
                ++p1;
            }
        }
    }
};
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

  • 10
    点赞
  • 13
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值