力扣面试150题图解学习

174 阅读12分钟

前言

力扣刷题记录,采用图解的方式,每日温习,方便记忆

算法题归类

来自豆包总结:

🔥 第一梯队(必刷,80% 面试靠这些)

  1. 数组 / 哈希表
  2. 双指针
  3. 滑动窗口
  4. 二分查找
  5. 前缀和 / 差分
  6. 栈 / 单调栈

🧠 第二梯队(高频中等题)

  1. BFS 广度优先
  2. DFS / 回溯
  3. 动态规划 DP(基础型)
  4. 链表
  5. 二叉树 / 二叉搜索树

⚙️ 第三梯队(大厂 / 难题常考)

  1. 图论(最短路径、拓扑排序)
  2. 并查集
  3. 贪心算法
  4. 高级 DP(区间、状态压缩)
  5. 位运算

手写方法

手写 reduce

记住三句话:

  1. 了解参数:callback 是回调参数,preValue 是上一个的返回值,this是该数组,this[index]是当前值
  2. 有传 preValue 则赋值;没传则取 this[0] 数组的第一个元素作为 preValue
  3. preValue = callback(上一个值preValue,当前值this[index])

image.png

时间O(n)空间O(1)

手写快排

递归口诀:递归终止,处理本层,递归调用,递归调用结果传递给上一层

快排核心:选中间,小的放左边,大的放右边

递归树:

image.png

image.png

时间O(nlogn)空间O(nlogn)

力扣面试150题-遍历

  • 滑动窗口:外层循环移动右边界,里层循环移动左边界
  • 动态规划

合并两个有序数组(tip:三尾指针往前)

leetcode.cn/problems/me…

输入: nums1 = [1,2,3,0,0,0], m = 3, nums2 = [2,5,6], n = 3
输出: [1,2,2,3,5,6]
解释: 需要合并 [1,2,3] 和 [2,5,6] 。
合并结果是 [1,2,2,3,5,6] ,其中斜体加粗标注的为 nums1 中的元素。

时间O(m+n),空间O(1)

image.png

有序数组去重,重复的放后面

leetcode.cn/problems/re…

输入: nums = [0,0,1,1,1,2,2,3,3,4]
输出: 5, nums = [0,1,2,3,4,_,_,_,_,_]
解释: 返回新的长度5,并且原数组 nums 的前五个元素被修改为 0,1,2,3,4。不需要考虑数组中超出新长度后面的元素

image.png

有序数组使得数字最多出现两次

leetcode.cn/problems/re…

输入: nums = [0,0,1,1,1,1,2,3,3]
输出: 7, nums = [0,0,1,1,2,3,3]
解释: 函数应返回新长度 length = 7, 并且原数组的前七个元素被修改为 0, 0, 1, 1, 2, 3, 3。
不需要考虑数组中超出新长度后面的元素。

image.png

投票法:找出超半数元素

给定一个大小为 `n` **的数组 `nums` ,返回其中的超半数元素
输入: nums = [2,2,1,1,1,2,2]
输出: 2
  1. 快排后,中间那个数就是超半数元素,时间O(nlogn)空间O(nlogn)
  2. 哈希表存储出现次数,时间O(n)空间O(n)
  3. 投票法:相同 + 1,不同 - 1,没票就换人,最后剩下的一定是多数元素

image.png

动态规划:打家劫舍【先后指针】

不能偷相邻的房子,偷这家不能偷上一家,偷上一家不能偷这家

image.png

【要偷这家就不能偷上家,偷了上家就不能偷这家】

image.png image.png image.png

var rob = function (nums) {
    if (nums == null || nums.length == 0) {
        return 0;
    }
    const length = nums.length;
    if (length == 1) {
        return nums[0];
    }
    let first = nums[0], second = Math.max(nums[0], nums[1]); // 先后指针,一前一后,后指针记录累积最优结果
    for (let i = 2; i < length; i++) {
        let temp = second;
        second = Math.max(first + nums[i], second);
        first = temp;
    }
    return second;
};

动态规划:单词拆分

图记忆法:快速回忆:

  • false 的情况: image.png
  • true 的情况: image.png

给你一个字符串 s 和一个字符串列表 wordDict 作为字典。如果可以利用字典中出现的单词(可重复,不需要全部用完)拼接出 s 则返回 true

输入: s = "applepenapple", wordDict = ["apple", "pen"]
输出: true
解释: 返回 true 因为 "applepenapple" 可以由 "apple" "pen" "apple" 拼接成。
      注意,你可以重复使用字典中的单词。

输入: s = "catsandog", wordDict = ["cats", "dog", "sand", "and", "cat"]
输出: false

动态规划思路:

  • 外层循环 i:维护一个数组 dp[i] 表示前i个字符能不能拆
  • 里层循环 j:遍历前 i 个字符找出切割点 jdp[i] = dp[j] && wordSet.has(s.slice(j, i)) 同时满足则表示前i个字符能拆

时间O(n²) 空间O(n)

var wordBreak = function(s, wordDict) {  
    const n = s.length;
    const wordSet = new Set(wordDict); // 转成 Set 方便快速查找
    const dp = new Array(n + 1).fill(false); // dp[i]:前i个字符能否拆分
    dp[0] = true// 空串初始合法
    for (let i = 1; i <= n; i++) {
        // 找分割点 j
        for (let j = 0; j < i; j++) {
            // 【前j个合法】 && 【j~i 子串在字典中】(动态规划重点:这里借助了走过的路直接得到 dp[j])
            if (dp[j] && wordSet.has(s.slice(j, i))) {
                dp[i] = true;
                break// 找到即可,不用再找其他j
            }
        }
    }
    return dp[n];
};

动态规划:最长递增子序列

注意子序列不是连续

输入: nums = [10,9,2,5,3,7,101,18]
输出: 4
解释: 最长递增子序列是 [2,3,7,101],因此长度为 4 。

输入: nums = [7,7,7,7,7,7,7]
输出: 1

时间O(n²) 空间O(n)

贪心算法+二分查找:最长递增子序列

子序列不是连续

输入: nums = [10,9,2,5,3,7,101,18]
输出: 4
解释: 最长递增子序列是 [2,3,7,101],因此长度为 4 。

输入: nums = [7,7,7,7,7,7,7]
输出: 1

贪心算法:能不能跳到终点【维护最远可达】

最初位于数组的 第一个下标 。数组中的每个元素代表你在该位置可以跳跃的最大长度。要求判断能不能到达最后一个位置

输入: nums = [2,3,1,1,4]
输出: true
解释: 可以先跳 1 步,从下标 0 到达下标 1, 然后再从下标 13 步到达最后一个下标。

输入: nums = [3,2,1,0,4]
输出: false
解释: 无论怎样,总会到达下标为 3 的位置。但该下标的最大跳跃长度是 0 , 所以永远不可能到达最后一个下标。

思路:遍历每个位置,维护出一个最远可达位置

image.png

可达的情况:

image.png

不可达的情况:

image.png

贪心算法:跳到终点最小步数【维护最远可达+边界再跳】

输入: nums = [2,3,1,1,4]
输出: 2
解释: 跳到最后一个位置的最小跳跃数是 2。
     从下标为 0 跳到下标为 1 的位置,跳 1 步,然后跳 3 步到达数组的最后一个位置。

输入: nums = [2,3,0,1,4]
输出: 2

image.png

image.png

滑动窗口的通用思路:

  • 外层循环移动右边界里层循环移动左边界
  • 外层循环for一直在移动,里层循环while要判定退出循环
  • 重点在于判定进入里层循环(暂停移动右边界,开始移动左边界)的条件

基本代码结构:

const fn = function (arr) {
    let left = 0;
    for (let right = 0; right < arr.length; right++) {
        while (`暂停移动右边界,开始移动左边界的条件`) {
            left++;
        }
    }
};

滑动窗口:长度最小的子数组

输入: target = 7, nums = [2,3,1,2,4,3]
输出: 2
解释: 子数组 [4,3] 是该条件下的长度最小的子数组。

输入: target = 11, nums = [1,1,1,1,1,1,1,1]
输出: 0
解释: 因为不存在符合条件的子数组,返回0

时间O(n)空间O(1)

思路:

  • 外层for循环移动右边界(延伸窗口),暂停条件:当前窗口总和 sum >= target 进入里层循环
  • 里层while循环移动左边界(收缩窗口),暂停条件:当前窗口总和 sum < target 退出里层循环

image.png

image.png

滑动窗口:无重复字符的最长子串

输入: s = "abcabcbb"
输出: 3 
解释: 因为无重复字符的最长子串是 "abc",所以其长度为 3。注意 "bca""cab" 也是正确答案。

输入: s = "pwwkew"
输出: 3
解释: 因为无重复字符的最长子串是 "wke",所以其长度为 3。注意,"pwke" 是一个子序列,不是子串。

时间O(n)空间O(n)

思路:

  • 外层for循环移动右边界(延伸窗口),暂停条件:哈希表里出现重复字符 进入里层循环
  • 里层while循环移动左边界(收缩窗口),暂停条件:哈希表里没有重复字符了 退出里层循环
  • 怎么判定:哈希表里字符是否重复了?:

维护一个哈希表存一下{每个字符: 当前字符的最新位置}

image.png

滑动窗口:最小覆盖子串

输入: s = "ADOBEACODEBANC", t = "ABCA"
输出: "ADOBAEC"
解释: 最小覆盖子串 "ADOBAEC" 包含来自字符串 t 的 2个'A'、1个'B' 和 1个'C'。

思路:

  • 外层for循环移动右边界(延伸窗口),暂停条件:满足包含子串条件 进入里层循环
  • 里层while循环移动左边界(收缩窗口),暂停条件:不满足包含子串条件 退出里层循环
  • 怎么判定 满足包含子串条件?:

目标子串:"ABCA" 转成哈希表结构为:

const map = {
    "A": 2,
    "B": 1,
    "C": 1
}

移动右边界时,每添加一个字符,判断一下已经满足几个条件了记为 x,当 x===Object.keys(map).length 时则表示已经满足全部条件

回溯:9键键盘的字母组合

回溯算法用于寻找所有的可行解,如果发现一个解不可行就舍弃掉。如果不存在不可行的解,直接穷举所有的解即可。

image.png
输入: digits = "23"
输出: ["ad","ae","af","bd","be","bf","cd","ce","cf"]

输入: digits = "2"
输出: ["a","b","c"]

image.png

力扣面试150题-递归

递归的理解

所谓递归,可以通俗理解为:公司的等级制度:

  • CEO→事业群老大→部门老大→总监→组长→员工(每一个上下级关系都是相似的,只是title不一样而已)

递归:爬楼梯

leetcode.cn/problems/cl…

假设你正在爬楼梯。需要 n 阶你才能到达楼顶。每次你可以爬 1 或 2 个台阶。你有多少种不同的方法可以爬到楼顶呢?

输入: n = 3
输出: 3
解释: 有三种方法可以爬到楼顶。
1. 1 阶 + 1 阶 + 12. 1 阶 + 23. 2 阶 + 1

第 n 阶的爬法 = 第 n-1 阶的爬法 + 第 n-2 阶的爬法,状态转移方程为:f(n) = f(n - 1) + f(n - 2)

递归树:

image.png

function climbStairs(n) {
  if (n === 1) return 1;
  if (n === 2) return 2;
  return climbStairs(n - 1) + climbStairs(n - 2);
}

二叉树的4种遍历

对于任意一棵树而言,前3种遍历方式都是 深度优先搜索(DFS)

前序遍历

前序遍历的形式总是(递归式的左右)

[ 根节点, [左子树的前序遍历结果], [右子树的前序遍历结果] ]
function preOrder(root) {
    if (!root) return;
    console.log(root.val);   // 根
    preOrder(root.left);     // 左
    preOrder(root.right);    // 右
}

image.png

中序遍历

而中序遍历的形式总是(递归式的左右)

[ [左子树的中序遍历结果], 根节点, [右子树的中序遍历结果] ]
function inOrder(root) {
    if (!root) return
    inOrder(root.left)      // 左
    console.log(root.val)   // 根
    inOrder(root.right)     // 右
}

image.png

后序遍历

而后序遍历的形式总是(递归式的左右

[ [左子树的中序遍历结果], [右子树的中序遍历结果] , 根节点]
function postOrder(root) {
    if (!root) return
    postOrder(root.left)    // 左
    postOrder(root.right)   // 右
    console.log(root.val)   // 根
}

image.png

层序遍历

层序遍历是广度优先搜索(BFS),采用队列的方式处理,需要O(n)的空间去存储队列

function levelOrder(root) {
    if (!root) return []
    let queue = [root]
    let res = []
    while (queue.length) {
        let node = queue.shift()
        res.push(node.val)
        if (node.left) queue.push(node.left)
        if (node.right) queue.push(node.right)
    }
    return res
}

image.png

二叉树:求最大深度

递归口诀:递归终止,处理本层,递归调用,递归调用结果传递给上一层

  • 递归退出条件:叶子节点的深度为 0
  • 本层逻辑核心:当前节点的左节点深度和右节点深度的较大值 + 1 则是当前节点的深度
image.png
输入: root = [3,9,20,null,null,15,7]
输出: 3

image.png

二叉树:两棵树是否相同

递归口诀:递归终止,处理本层,递归调用,递归调用结果传递给上一层

  • 递归终止条件:左(右)节点都是空 || 有其中一个节点的左(右)节点是空,但另一个不是
  • 本层处理核心逻辑:当前节点的值要相同 && 左节点相同 && 右节点相同
image.png
输入: p = [1,2,3], q = [1,2,3]
输出: true

image.png

二叉树:对称二叉树

递归口诀:递归终止,处理本层,递归调用,递归调用结果传递给上一层

  • 递归终止条件:左(右)节点都是空 || 有其中一个节点的左(右)节点是空,但另一个不是
  • 本层处理核心逻辑:

图:岛屿数量

image.png
输入:grid = [
  ['1','1','1','1','0'],
  ['1','1','0','1','0'],
  ['1','1','0','0','0'],
  ['0','0','0','0','0']
]
输出:1

输入:grid = [
  ['1','1','0','0','0'],
  ['1','1','0','0','0'],
  ['0','0','1','0','0'],
  ['0','0','0','1','1']
]
输出:3

二叉树的递归和网格图的递归有何区别?

image.png

时间O(mn)寻找岛屿,发现岛屿后 DFS 递归(插旗标记,避免无限递归)

image.png

力扣面试150题-栈

栈:有效的括号

输入: s = "([])"
输出: true
输入: s = "([)]"
输出: false

image.png

栈:简化路径

输入: path = "/home/"
输出: "/home"
输入: path = "/home//foo/"
输出: "/home/foo"
输入: path = "/home/user/Documents/../Pictures"
输出: "/home/user/Pictures"
输入: path = "/.../a/../b/c/../d/./"
输出: "/.../b/d"

image.png

栈:表达式求值

输入: tokens = ["4","13","5","/","+"]
输出: 6
解释: 该算式转化为常见的中缀算术表达式为:(4 + (13 / 5)) = 6

image.png

力扣面试150题-链表

链表:判断是否存在环

image.png

输入:head = [3,2,0,-4], pos = 1
输出:true
解释:链表中有一个环,其尾部连接到第二个节点。

快慢指针会相遇,说明有环,否则没环。时间O(n)空间O(1)

image.png

链表:两数相加

image.png

输入:l1 = [2,4,3], l2 = [5,6,4]
输出:[7,0,8]
解释:342 + 465 = 807.
  1. 直接求和
  2. 对 sum 取余,并修改结果链表的头尾指针(用于返回结果)
  3. 对 sum 取整
  4. 较长链表继续走,走完的也要等
  5. 最后判断是否还有一次进位

时间O(max(m,n))空间O(1)

image.png

链表: 合并两个有序链表

直接原地改指向即可:

  1. 谁更小,prev指向谁,谁走一格
  2. prev也得走一格
  3. 直接将链表末尾指向未合并完的链表即可

image.png

链表:反转链表部分节点

image.png

输入:head = [1,2,3,4,5], left = 2, right = 4
输出:[1,4,3,2,5]

image.png