Bootstrap

leetcode hot 100(1)

leetcode hot 100(1)

哈希

双指针

  • 283. 移动零 - 力扣(LeetCode):i,j两个指针,i指向非零数字该放到的位置,j指向当前遍历到的数字。当nums[j]为0,就只移动j;如果nums[j]不为0,就swap(nums[i++],nums[j++])。
  • 🥇11. 盛最多水的容器 - 力扣(LeetCode): l、r双指针,计算以l、r为边界的容器的最大容量,记录最大值。当 height[l] < height[r] ,就移动l:l++。否则移动r:r++。
  • 🥇 15. 三数之和 - 力扣(LeetCode):先对数组排序, 然后i遍历数组,对每个i都有双指针l、r指向i+1和n-1。然后就是l、r移动监测和是否为0,如果是,则放入记录。然后继续移动l、r去重,尝试找到更多的组合,直至两个指针相碰。注意去重时的指针移动
  • 🥇42. 接雨水 - 力扣(LeetCode):对于每根柱子height[i], 在i处能接的雨水等于max{min(leftmax,rightmax) - height[i], 0}。leftmax是i的左边包括i的最大height。同理,rightmax是i的右边包括i的最大height。 左右指针left、right,那边小就移动那个指针,持续更新leftmax和rightmax。

滑动窗口

子串

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;
    }
};
  • 🥇最小覆盖子串:先记录t的字符串情况,左右指针 l、r。向右移动r指针,直至[l,r]区间字符能覆盖t。然后向左移动l指针,直至不能覆盖t。记录区间。反复此步骤,直至r指针到尾
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);
        }
    }
};

普通数组

  • 53. 最大子数组和 - 力扣(LeetCode):dp[i]代表以nums[i]最大数组和。dp[i] = max(dp[i - 1], 0) + nums[i]。直接原地修改即可。
  • 56. 合并区间 - 力扣(LeetCode):贪心算法,先按照每个区间的左边界从小到大(尽可能合并多的区间)。记录左边界l、右边界r。对接下来的区间进行判断,如果左边界在l、r之间,就合并区间,如果右边界大于r就更新r。如果左边界大于r,就记录当前l、r为一个合并区间。并且更新l、r为当前区间的左右边界。
  • 189. 轮转数组 - 力扣(LeetCode):向右轮转 k 个位置。记得对k取余。先对后k个元素reverse,再对剩余元素reverse,之后对整改数组reverse。
  • 238. 除自身以外数组的乘积 - 力扣(LeetCode):最简单的用两个数组记录每个数字的左右乘积。或者可以用一个数组记录每个数字的左乘积,然后从右到左遍历,计算答案。剩下一个数组。
  • 41. 缺失的第一个正数 - 力扣(LeetCode):要求不能使用额外空间,那就只能使用原数组记录。假设数组长度为n,则可以记录1-n这n个数字的存在情况。流程:遍历数组,如果当前数字与数组下标不匹配,且当前数字小于等于n,大于等于1,就与以当前数字为小标的数组元素交换。swap(nums[i], nums[nums[i]]) 直至 nums[i] 或者 nums[nums[i]]匹配为止 或者 nums[i] == nums[nums[i]] 。后面遍历数组查看哪个数字缺少。

矩阵

链表

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

;