Algorithm practice notes

LeetCode 热题 100 Python 解法整理

按官方 Hot100 分组整理题意摘要、解题分析、复杂度和 Python 3 代码。重点不是背答案,而是把每题归到可复用的算法模式。

总览:先识别模式,再写代码

Hot100 覆盖哈希、指针、窗口、链表、树、图、回溯、二分、栈、堆、贪心和 DP。每题都按“题目、分析、解法、复杂度”组织,代码默认使用 LeetCode Python 3 环境。

学习路线

先按主题刷题,遇到卡点时展开题目规格与题目分析;写完代码后对照复杂度和边界条件。题目规格基于官方题面改写,完整原题以每题“官方题目”链接为准。

HashTwo PointersWindowLinked ListTreeGraphBacktrackingBinary SearchStackHeapGreedyDP

当前显示 100 / 100 题

哈希

本组 3 题,围绕 哈希 的核心模式展开。先识别不变量,再选择哈希、指针、递归、队列、堆或 DP 状态。

1两数之和 Two Sum简单

题目规格

官方题意(改写):给定一个整数数组和目标值,找出两个不同位置的数,使它们的和等于目标值。 完整原题、样例和英文表述以本题官方链接为准。

输入:判题入口是 Solution.twoSum(...),输入参数为 nums(List[int])、target(int)。

输出:返回 List[int];返回值含义是:给定一个整数数组和目标值,找出两个不同位置的数,使它们的和等于目标值。

约束要点
  • 2 <= nums.length <= 10^4
  • -10^9 <= nums[i] <= 10^9
  • -10^9 <= target <= 10^9
  • 只会存在一个有效答案

题目分析

关键是把“找另一个数”变成哈希表查询。遍历到 x 时,只要 target - x 已经出现,就立即得到答案;否则记录 x 的下标。因为题目要求不同位置,必须先查再写入当前数。

  • 建模:两数之和 可以先理解为“给定一个整数数组和目标值,找出两个不同位置的数,使它们的和等于目标值。”。先确定答案依赖的是下标、值、路径、窗口、集合还是 DP 状态,再决定数据结构。
  • 不变量:先决定哈希表的 key 和 value:key 通常是元素值、前缀和或结构签名,value 保存下标、次数或分组结果。查询与写入顺序要和题目是否允许复用当前元素保持一致。
  • 边界:重点检查重复值、负数、空分组和“当前元素是否能与自己匹配”。
  • 正确性:维护 value -> index,当前元素只负责匹配之前出现过的补数。 只要每次更新后不变量仍成立,最终保留下来的状态就对应题目要求的结果。

解法:哈希表一次遍历

维护 value -> index,当前元素只负责匹配之前出现过的补数。

class Solution:
    def twoSum(self, nums: List[int], target: int) -> List[int]:
        seen = {}
        for i, x in enumerate(nums):
            need = target - x
            if need in seen:
                return [seen[need], i]
            # 先查再存,避免同一个元素被用两次
            seen[x] = i
        return []

复杂度:时间 O(n),空间 O(n)。

复杂度分析

  • 解法:哈希表一次遍历:时间 O(n),主循环中每个元素或节点只进入核心结构常数次。 空间 O(n),主要来自哈希表、前缀和计数或分组容器。
49字母异位词分组 Group Anagrams中等

题目规格

官方题意(改写):把一组字符串按字母异位词分组:字符种类和次数完全相同的字符串放在一起。 完整原题、样例和英文表述以本题官方链接为准。

输入:判题入口是 Solution.groupAnagrams(...),输入参数为 strs(List[str])。

输出:返回 List[List[str]];返回值含义是:把一组字符串按字母异位词分组:字符种类和次数完全相同的字符串放在一起。

约束要点
  • 1 <= strs.length <= 10^4
  • 0 <= strs[i].length <= 100
  • strs[i] 仅包含小写字母

题目分析

异位词的顺序不同,但排序后的字符序列相同。也可以用 26 维计数元组作为 key,避免排序成本。这里使用计数元组,适合全小写字母。

  • 建模:字母异位词分组 可以先理解为“把一组字符串按字母异位词分组:字符种类和次数完全相同的字符串放在一起。”。先确定答案依赖的是下标、值、路径、窗口、集合还是 DP 状态,再决定数据结构。
  • 不变量:先决定哈希表的 key 和 value:key 通常是元素值、前缀和或结构签名,value 保存下标、次数或分组结果。查询与写入顺序要和题目是否允许复用当前元素保持一致。
  • 边界:重点检查重复值、负数、空分组和“当前元素是否能与自己匹配”。
  • 正确性:每个字符串转成长度 26 的 tuple,tuple 可哈希,能唯一表示字母频次。 只要每次更新后不变量仍成立,最终保留下来的状态就对应题目要求的结果。

解法:字符计数作为哈希键

每个字符串转成长度 26 的 tuple,tuple 可哈希,能唯一表示字母频次。

from collections import defaultdict

class Solution:
    def groupAnagrams(self, strs: List[str]) -> List[List[str]]:
        groups = defaultdict(list)
        for s in strs:
            cnt = [0] * 26
            for ch in s:
                cnt[ord(ch) - ord('a')] += 1
            groups[tuple(cnt)].append(s)
        return list(groups.values())

复杂度:时间 O(nk),空间 O(nk)。

复杂度分析

  • 解法:字符计数作为哈希键:时间 O(nk),主循环中每个元素或节点只进入核心结构常数次。 空间 O(nk),额外空间来自代码中显式维护的辅助结构;返回结果本身通常不计入额外空间。
128最长连续序列 Longest Consecutive Sequence中等

题目规格

官方题意(改写):在未排序数组中,求最长连续整数序列的长度。 完整原题、样例和英文表述以本题官方链接为准。

输入:判题入口是 Solution.longestConsecutive(...),输入参数为 nums(List[int])。

输出:返回 int;返回值含义是:在未排序数组中,求最长连续整数序列的长度。

约束要点
  • 0 <= nums.length <= 10^5
  • -10^9 <= nums[i] <= 10^9

题目分析

不能排序后求解,否则是 O(n log n)。把所有数字放进 set,只从没有前驱 x-1 的数字开始扩展,这样每个连续段只被扫描一次。

  • 建模:最长连续序列 可以先理解为“在未排序数组中,求最长连续整数序列的长度。”。先确定答案依赖的是下标、值、路径、窗口、集合还是 DP 状态,再决定数据结构。
  • 不变量:先决定哈希表的 key 和 value:key 通常是元素值、前缀和或结构签名,value 保存下标、次数或分组结果。查询与写入顺序要和题目是否允许复用当前元素保持一致。
  • 边界:重点检查重复值、负数、空分组和“当前元素是否能与自己匹配”。
  • 正确性:x-1 不存在时,x 才是某段起点;从起点向右计数。 只要每次更新后不变量仍成立,最终保留下来的状态就对应题目要求的结果。

解法:集合找连续段起点

x-1 不存在时,x 才是某段起点;从起点向右计数。

class Solution:
    def longestConsecutive(self, nums: List[int]) -> int:
        values = set(nums)
        best = 0
        for x in values:
            if x - 1 in values:
                continue
            y = x
            while y in values:
                y += 1
            best = max(best, y - x)
        return best

复杂度:时间 O(n),空间 O(n)。

复杂度分析

  • 解法:集合找连续段起点:时间 O(n),主循环中每个元素或节点只进入核心结构常数次。 空间 O(n),主要来自哈希表、前缀和计数或分组容器。

双指针

本组 4 题,围绕 双指针 的核心模式展开。先识别不变量,再选择哈希、指针、递归、队列、堆或 DP 状态。

283移动零 Move Zeroes简单

题目规格

官方题意(改写):把数组中的 0 移到末尾,同时保持非零元素的相对顺序。 完整原题、样例和英文表述以本题官方链接为准。

输入:判题入口是 Solution.moveZeroes(...),输入参数为 nums(List[int])。

输出:函数原地修改输入对象,不需要返回新对象;修改后的数据应满足:把数组中的 0 移到末尾,同时保持非零元素的相对顺序。

约束要点
  • 1 <= nums.length <= 10^4
  • -2^31 <= nums[i] <= 2^31 - 1

题目分析

用 slow 指向下一个应该放非零元素的位置。扫描到非零值就和 slow 交换,然后 slow 前进。交换写法能原地完成。

  • 建模:移动零 可以先理解为“把数组中的 0 移到末尾,同时保持非零元素的相对顺序。”。先确定答案依赖的是下标、值、路径、窗口、集合还是 DP 状态,再决定数据结构。
  • 不变量:两个指针必须单调移动,每次移动都要能证明被丢弃的一侧不会产生更优解或不会再影响答案。
  • 边界:重点检查指针相遇、重复值去重、宽度或窗口长度为 0/1 的情况。
  • 正确性:前半段始终维护为已处理的非零元素序列。 只要每次更新后不变量仍成立,最终保留下来的状态就对应题目要求的结果。

解法:稳定压缩非零元素

前半段始终维护为已处理的非零元素序列。

class Solution:
    def moveZeroes(self, nums: List[int]) -> None:
        slow = 0
        for fast, x in enumerate(nums):
            if x != 0:
                nums[slow], nums[fast] = nums[fast], nums[slow]
                slow += 1

复杂度:时间 O(n),空间 O(1)。

复杂度分析

  • 解法:稳定压缩非零元素:时间 O(n),主循环中每个元素或节点只进入核心结构常数次。 空间 O(1),只使用常数个指针、计数器或滚动变量;题目要求的输出不计入额外空间。
11盛最多水的容器 Container With Most Water中等

题目规格

官方题意(改写):给定每个位置的高度,选择两条竖线和 x 轴组成容器,使盛水面积最大。 完整原题、样例和英文表述以本题官方链接为准。

输入:判题入口是 Solution.maxArea(...),输入参数为 height(List[int])。

输出:返回 int;返回值含义是:给定每个位置的高度,选择两条竖线和 x 轴组成容器,使盛水面积最大。

约束要点
  • n == height.length
  • 2 <= n <= 10^5
  • 0 <= height[i] <= 10^4

题目分析

面积由较短边决定。左右指针从两端开始,若移动较高边,宽度变小且短板不变或更差,不可能更优;因此每次移动较短边。

  • 建模:盛最多水的容器 可以先理解为“给定每个位置的高度,选择两条竖线和 x 轴组成容器,使盛水面积最大。”。先确定答案依赖的是下标、值、路径、窗口、集合还是 DP 状态,再决定数据结构。
  • 不变量:两个指针必须单调移动,每次移动都要能证明被丢弃的一侧不会产生更优解或不会再影响答案。
  • 边界:重点检查指针相遇、重复值去重、宽度或窗口长度为 0/1 的情况。
  • 正确性:每轮计算面积,然后丢弃当前较短的边。 只要每次更新后不变量仍成立,最终保留下来的状态就对应题目要求的结果。

解法:双指针收缩短板

每轮计算面积,然后丢弃当前较短的边。

class Solution:
    def maxArea(self, height: List[int]) -> int:
        left, right = 0, len(height) - 1
        best = 0
        while left < right:
            h = min(height[left], height[right])
            best = max(best, h * (right - left))
            if height[left] < height[right]:
                left += 1
            else:
                right -= 1
        return best

复杂度:时间 O(n),空间 O(1)。

复杂度分析

  • 解法:双指针收缩短板:时间 O(n),主循环中每个元素或节点只进入核心结构常数次。 空间 O(1),只使用常数个指针、计数器或滚动变量;题目要求的输出不计入额外空间。
15三数之和 3Sum中等

题目规格

官方题意(改写):找出数组中所有不重复的三元组,使三数之和为 0。 完整原题、样例和英文表述以本题官方链接为准。

输入:判题入口是 Solution.threeSum(...),输入参数为 nums(List[int])。

输出:返回 List[List[int]];返回值含义是:找出数组中所有不重复的三元组,使三数之和为 0。

约束要点
  • 3 <= nums.length <= 3000
  • -10^5 <= nums[i] <= 10^5

题目分析

先排序,固定第一个数后,剩余问题变成有序数组两数之和。去重发生在固定数和双指针移动时,否则会输出重复三元组。

  • 建模:三数之和 可以先理解为“找出数组中所有不重复的三元组,使三数之和为 0。”。先确定答案依赖的是下标、值、路径、窗口、集合还是 DP 状态,再决定数据结构。
  • 不变量:两个指针必须单调移动,每次移动都要能证明被丢弃的一侧不会产生更优解或不会再影响答案。
  • 边界:重点检查指针相遇、重复值去重、宽度或窗口长度为 0/1 的情况。
  • 正确性:固定 i,left/right 根据 sum 与 0 的关系移动。 只要每次更新后不变量仍成立,最终保留下来的状态就对应题目要求的结果。

解法:排序 + 固定一位 + 双指针

固定 i,left/right 根据 sum 与 0 的关系移动。

class Solution:
    def threeSum(self, nums: List[int]) -> List[List[int]]:
        nums.sort()
        ans = []
        n = len(nums)
        for i in range(n - 2):
            if i > 0 and nums[i] == nums[i - 1]:
                continue
            if nums[i] > 0:
                break
            left, right = i + 1, n - 1
            while left < right:
                s = nums[i] + nums[left] + nums[right]
                if s == 0:
                    ans.append([nums[i], nums[left], nums[right]])
                    left += 1
                    right -= 1
                    while left < right and nums[left] == nums[left - 1]:
                        left += 1
                    while left < right and nums[right] == nums[right + 1]:
                        right -= 1
                elif s < 0:
                    left += 1
                else:
                    right -= 1
        return ans

复杂度:时间 O(n^2),空间 O(1) 不计输出。

复杂度分析

  • 解法:排序 + 固定一位 + 双指针:时间 O(n^2),需要枚举两个维度、中心扩展或区间端点,形成二次级状态/比较。 空间 O(1) 不计输出,只使用常数个指针、计数器或滚动变量;题目要求的输出不计入额外空间。
42接雨水 Trapping Rain Water困难

题目规格

官方题意(改写):给定柱状高度,计算下雨后能接住的总水量。 完整原题、样例和英文表述以本题官方链接为准。

输入:判题入口是 Solution.trap(...),输入参数为 height(List[int])。

输出:返回 int;返回值含义是:给定柱状高度,计算下雨后能接住的总水量。

约束要点
  • n == height.length
  • 1 <= n <= 2 * 10^4
  • 0 <= height[i] <= 10^5

题目分析

某位置水量由左右最高柱的较小值决定。双指针维护 left_max/right_max,哪边最高上界更低,就能确定那边当前位置的水量。

  • 建模:接雨水 可以先理解为“给定柱状高度,计算下雨后能接住的总水量。”。先确定答案依赖的是下标、值、路径、窗口、集合还是 DP 状态,再决定数据结构。
  • 不变量:两个指针必须单调移动,每次移动都要能证明被丢弃的一侧不会产生更优解或不会再影响答案。
  • 边界:重点检查指针相遇、重复值去重、宽度或窗口长度为 0/1 的情况。
  • 正确性:较低一侧的水位已被另一侧保证,直接结算。 只要每次更新后不变量仍成立,最终保留下来的状态就对应题目要求的结果。

解法:双指针维护两侧最高

较低一侧的水位已被另一侧保证,直接结算。

class Solution:
    def trap(self, height: List[int]) -> int:
        left, right = 0, len(height) - 1
        left_max = right_max = 0
        water = 0
        while left < right:
            if height[left] < height[right]:
                left_max = max(left_max, height[left])
                water += left_max - height[left]
                left += 1
            else:
                right_max = max(right_max, height[right])
                water += right_max - height[right]
                right -= 1
        return water

复杂度:时间 O(n),空间 O(1)。

复杂度分析

  • 解法:双指针维护两侧最高:时间 O(n),主循环中每个元素或节点只进入核心结构常数次。 空间 O(1),只使用常数个指针、计数器或滚动变量;题目要求的输出不计入额外空间。

滑动窗口

本组 2 题,围绕 滑动窗口 的核心模式展开。先识别不变量,再选择哈希、指针、递归、队列、堆或 DP 状态。

3无重复字符的最长子串 Longest Substring Without Repeating Characters中等

题目规格

官方题意(改写):求字符串中不含重复字符的最长连续子串长度。 完整原题、样例和英文表述以本题官方链接为准。

输入:判题入口是 Solution.lengthOfLongestSubstring(...),输入参数为 s(str)。

输出:返回 int;返回值含义是:求字符串中不含重复字符的最长连续子串长度。

约束要点
  • 0 <= s.length <= 5 * 10^4
  • s 由英文字母、数字、符号和空格组成

题目分析

滑动窗口维护当前无重复区间。记录字符上次出现位置,遇到重复且位置在窗口内,就把左边界移动到重复字符后一位。

  • 建模:无重复字符的最长子串 可以先理解为“求字符串中不含重复字符的最长连续子串长度。”。先确定答案依赖的是下标、值、路径、窗口、集合还是 DP 状态,再决定数据结构。
  • 不变量:窗口内维护一个可增量更新的条件,右端负责纳入新元素,左端只在条件被破坏或可以优化答案时移动。
  • 边界:重点检查窗口刚好满足条件、左端移动后频次归零、答案为空的情况。
  • 正确性:left 永不回退,窗口始终无重复。 只要每次更新后不变量仍成立,最终保留下来的状态就对应题目要求的结果。

解法:滑动窗口 + 最近位置

left 永不回退,窗口始终无重复。

class Solution:
    def lengthOfLongestSubstring(self, s: str) -> int:
        last = {}
        left = 0
        best = 0
        for right, ch in enumerate(s):
            if ch in last and last[ch] >= left:
                left = last[ch] + 1
            last[ch] = right
            best = max(best, right - left + 1)
        return best

复杂度:时间 O(n),空间 O(字符集大小)。

复杂度分析

  • 解法:滑动窗口 + 最近位置:时间 O(n),主循环中每个元素或节点只进入核心结构常数次。 空间 O(字符集大小),额外空间来自代码中显式维护的辅助结构;返回结果本身通常不计入额外空间。
438找到字符串中所有字母异位词 Find All Anagrams in a String中等

题目规格

官方题意(改写):在字符串 s 中找出所有与 p 互为字母异位词的子串起始下标。 完整原题、样例和英文表述以本题官方链接为准。

输入:判题入口是 Solution.findAnagrams(...),输入参数为 s(str)、p(str)。

输出:返回 List[int];返回值含义是:在字符串 s 中找出所有与 p 互为字母异位词的子串起始下标。

约束要点
  • 1 <= s.length, p.length <= 3 * 10^4
  • s 和 p 仅包含小写字母

题目分析

窗口长度固定为 len(p)。维护窗口字符计数和目标计数,每次右扩并在长度超出时左缩,计数相同即命中。

  • 建模:找到字符串中所有字母异位词 可以先理解为“在字符串 s 中找出所有与 p 互为字母异位词的子串起始下标。”。先确定答案依赖的是下标、值、路径、窗口、集合还是 DP 状态,再决定数据结构。
  • 不变量:窗口内维护一个可增量更新的条件,右端负责纳入新元素,左端只在条件被破坏或可以优化答案时移动。
  • 边界:重点检查窗口刚好满足条件、左端移动后频次归零、答案为空的情况。
  • 正确性:窗口大小固定后,只需比较字符频次。 只要每次更新后不变量仍成立,最终保留下来的状态就对应题目要求的结果。

解法:固定长度滑动窗口

窗口大小固定后,只需比较字符频次。

from collections import Counter

class Solution:
    def findAnagrams(self, s: str, p: str) -> List[int]:
        need = Counter(p)
        window = Counter()
        ans = []
        m = len(p)
        for right, ch in enumerate(s):
            window[ch] += 1
            if right >= m:
                left_ch = s[right - m]
                window[left_ch] -= 1
                if window[left_ch] == 0:
                    del window[left_ch]
            if window == need:
                ans.append(right - m + 1)
        return ans

复杂度:时间 O(n * 字符集);小写字母可视为 O(n),空间 O(字符集)。

复杂度分析

  • 解法:固定长度滑动窗口:时间 O(n * 字符集);小写字母可视为 O(n),主循环中每个元素或节点只进入核心结构常数次。 空间 O(字符集),额外空间来自代码中显式维护的辅助结构;返回结果本身通常不计入额外空间。

子串

本组 3 题,围绕 子串 的核心模式展开。先识别不变量,再选择哈希、指针、递归、队列、堆或 DP 状态。

560和为 K 的子数组 Subarray Sum Equals K中等

题目规格

官方题意(改写):统计和等于 k 的连续子数组数量。 完整原题、样例和英文表述以本题官方链接为准。

输入:判题入口是 Solution.subarraySum(...),输入参数为 nums(List[int])、k(int)。

输出:返回 int;返回值含义是:统计和等于 k 的连续子数组数量。

约束要点
  • 1 <= nums.length <= 2 * 10^4
  • -1000 <= nums[i] <= 1000
  • -10^7 <= k <= 10^7

题目分析

令 prefix[i] 为前 i 个数之和。若 prefix[j] - prefix[i] = k,则 prefix[i] = prefix[j] - k。遍历右端点时,查询之前出现过多少个目标前缀和。

  • 建模:和为 K 的子数组 可以先理解为“统计和等于 k 的连续子数组数量。”。先确定答案依赖的是下标、值、路径、窗口、集合还是 DP 状态,再决定数据结构。
  • 不变量:把连续子串统一看作窗口或前缀差:窗口题维护字符/元素频次,前缀和题把区间问题变成两个前缀状态的关系。
  • 边界:重点检查负数导致窗口法失效、字符重复、目标不存在和最短/最长答案初始化。
  • 正确性:把连续区间和转化为两个前缀和之差。 只要每次更新后不变量仍成立,最终保留下来的状态就对应题目要求的结果。

解法:前缀和 + 频次哈希

把连续区间和转化为两个前缀和之差。

from collections import defaultdict

class Solution:
    def subarraySum(self, nums: List[int], k: int) -> int:
        count = defaultdict(int)
        count[0] = 1
        prefix = 0
        ans = 0
        for x in nums:
            prefix += x
            ans += count[prefix - k]
            count[prefix] += 1
        return ans

复杂度:时间 O(n),空间 O(n)。

复杂度分析

  • 解法:前缀和 + 频次哈希:时间 O(n),主循环中每个元素或节点只进入核心结构常数次。 空间 O(n),主要来自哈希表、前缀和计数或分组容器。
239滑动窗口最大值 Sliding Window Maximum困难

题目规格

官方题意(改写):给定窗口大小 k,输出每个滑动窗口中的最大值。 完整原题、样例和英文表述以本题官方链接为准。

输入:判题入口是 Solution.maxSlidingWindow(...),输入参数为 nums(List[int])、k(int)。

输出:返回 List[int];返回值含义是:给定窗口大小 k,输出每个滑动窗口中的最大值。

约束要点
  • 1 <= nums.length <= 10^5
  • -10^4 <= nums[i] <= 10^4
  • 1 <= k <= nums.length

题目分析

单调队列存下标,队首始终是当前窗口最大值下标。新元素进入时,弹出队尾所有不大于它的元素,因为这些元素之后不可能再成为最大值。

  • 建模:滑动窗口最大值 可以先理解为“给定窗口大小 k,输出每个滑动窗口中的最大值。”。先确定答案依赖的是下标、值、路径、窗口、集合还是 DP 状态,再决定数据结构。
  • 不变量:把连续子串统一看作窗口或前缀差:窗口题维护字符/元素频次,前缀和题把区间问题变成两个前缀状态的关系。
  • 边界:重点检查负数导致窗口法失效、字符重复、目标不存在和最短/最长答案初始化。
  • 正确性:队列按值递减、按下标递增,队首过期时弹出。 只要每次更新后不变量仍成立,最终保留下来的状态就对应题目要求的结果。

解法:单调递减队列

队列按值递减、按下标递增,队首过期时弹出。

from collections import deque

class Solution:
    def maxSlidingWindow(self, nums: List[int], k: int) -> List[int]:
        dq = deque()
        ans = []
        for i, x in enumerate(nums):
            while dq and nums[dq[-1]] <= x:
                dq.pop()
            dq.append(i)
            if dq[0] <= i - k:
                dq.popleft()
            if i >= k - 1:
                ans.append(nums[dq[0]])
        return ans

复杂度:时间 O(n),空间 O(k)。

复杂度分析

  • 解法:单调递减队列:时间 O(n),主循环中每个元素或节点只进入核心结构常数次。 空间 O(k),主要来自大小受 k 控制的堆、窗口或候选集合。
76最小覆盖子串 Minimum Window Substring困难

题目规格

官方题意(改写):在 s 中找出包含 t 所有字符及其次数的最短连续子串。 完整原题、样例和英文表述以本题官方链接为准。

输入:判题入口是 Solution.minWindow(...),输入参数为 s(str)、t(str)。

输出:返回 str;返回值含义是:在 s 中找出包含 t 所有字符及其次数的最短连续子串。

约束要点
  • m == s.length
  • n == t.length
  • 1 <= m, n <= 10^5
  • s 和 t 由英文字母组成

题目分析

右指针扩张直到窗口满足需求,再移动左指针尽量缩短。用 formed 记录已经满足频次的字符种类数,避免每次全量比较。

  • 建模:最小覆盖子串 可以先理解为“在 s 中找出包含 t 所有字符及其次数的最短连续子串。”。先确定答案依赖的是下标、值、路径、窗口、集合还是 DP 状态,再决定数据结构。
  • 不变量:把连续子串统一看作窗口或前缀差:窗口题维护字符/元素频次,前缀和题把区间问题变成两个前缀状态的关系。
  • 边界:重点检查负数导致窗口法失效、字符重复、目标不存在和最短/最长答案初始化。
  • 正确性:满足后立刻收缩,收缩到刚好不满足前的窗口就是候选答案。 只要每次更新后不变量仍成立,最终保留下来的状态就对应题目要求的结果。

解法:可变窗口 + 满足种类计数

满足后立刻收缩,收缩到刚好不满足前的窗口就是候选答案。

from collections import Counter, defaultdict

class Solution:
    def minWindow(self, s: str, t: str) -> str:
        need = Counter(t)
        window = defaultdict(int)
        required = len(need)
        formed = 0
        left = 0
        best_len = float('inf')
        best = (0, 0)
        for right, ch in enumerate(s):
            window[ch] += 1
            if ch in need and window[ch] == need[ch]:
                formed += 1
            while formed == required:
                if right - left + 1 < best_len:
                    best_len = right - left + 1
                    best = (left, right + 1)
                out = s[left]
                window[out] -= 1
                if out in need and window[out] < need[out]:
                    formed -= 1
                left += 1
        return '' if best_len == float('inf') else s[best[0]:best[1]]

复杂度:时间 O(|s| + |t|),空间 O(字符集)。

复杂度分析

  • 解法:可变窗口 + 满足种类计数:时间 O(|s| + |t|),把连续子串统一看作窗口或前缀差:窗口题维护字符/元素频次,前缀和题把区间问题变成两个前缀状态的关系。 空间 O(字符集),额外空间来自代码中显式维护的辅助结构;返回结果本身通常不计入额外空间。

普通数组

本组 5 题,围绕 普通数组 的核心模式展开。先识别不变量,再选择哈希、指针、递归、队列、堆或 DP 状态。

53最大子数组和 Maximum Subarray中等

题目规格

官方题意(改写):求数组中和最大的连续子数组。 完整原题、样例和英文表述以本题官方链接为准。

输入:判题入口是 Solution.maxSubArray(...),输入参数为 nums(List[int])。

输出:返回 int;返回值含义是:求数组中和最大的连续子数组。

约束要点
  • 1 <= nums.length <= 10^5
  • -10^4 <= nums[i] <= 10^4

题目分析

Kadane 算法维护“以当前位置结尾的最大子数组和”。如果之前的和为负,就不如从当前元素重新开始。

  • 建模:最大子数组和 可以先理解为“求数组中和最大的连续子数组。”。先确定答案依赖的是下标、值、路径、窗口、集合还是 DP 状态,再决定数据结构。
  • 不变量:先判断数组操作的核心结构:是前缀/后缀、排序后合并、原地交换,还是以当前位置结尾的局部最优。
  • 边界:重点检查空数组以外的最小规模、全负数、原地修改和输出空间是否计入额外空间。
  • 正确性:local 表示必须以当前元素结尾的最优值,best 表示全局最优。 只要每次更新后不变量仍成立,最终保留下来的状态就对应题目要求的结果。

解法:Kadane 动态规划

local 表示必须以当前元素结尾的最优值,best 表示全局最优。

class Solution:
    def maxSubArray(self, nums: List[int]) -> int:
        local = best = nums[0]
        for x in nums[1:]:
            local = max(x, local + x)
            best = max(best, local)
        return best

复杂度:时间 O(n),空间 O(1)。

复杂度分析

  • 解法:Kadane 动态规划:时间 O(n),主循环中每个元素或节点只进入核心结构常数次。 空间 O(1),只使用常数个指针、计数器或滚动变量;题目要求的输出不计入额外空间。
56合并区间 Merge Intervals中等

题目规格

官方题意(改写):合并所有重叠区间,返回互不重叠的区间集合。 完整原题、样例和英文表述以本题官方链接为准。

输入:判题入口是 Solution.merge(...),输入参数为 intervals(List[List[int]])。

输出:返回 List[List[int]];返回值含义是:合并所有重叠区间,返回互不重叠的区间集合。

约束要点
  • 1 <= intervals.length <= 10^4
  • intervals[i].length == 2
  • 0 <= start i <= end i <= 10^4

题目分析

先按左端点排序。当前区间与结果末尾区间重叠时更新右端点,否则开启新区间。

  • 建模:合并区间 可以先理解为“合并所有重叠区间,返回互不重叠的区间集合。”。先确定答案依赖的是下标、值、路径、窗口、集合还是 DP 状态,再决定数据结构。
  • 不变量:先判断数组操作的核心结构:是前缀/后缀、排序后合并、原地交换,还是以当前位置结尾的局部最优。
  • 边界:重点检查空数组以外的最小规模、全负数、原地修改和输出空间是否计入额外空间。
  • 正确性:排序保证只需和最后一个已合并区间比较。 只要每次更新后不变量仍成立,最终保留下来的状态就对应题目要求的结果。

解法:排序后线性合并

排序保证只需和最后一个已合并区间比较。

class Solution:
    def merge(self, intervals: List[List[int]]) -> List[List[int]]:
        intervals.sort(key=lambda x: x[0])
        ans = []
        for start, end in intervals:
            if not ans or start > ans[-1][1]:
                ans.append([start, end])
            else:
                ans[-1][1] = max(ans[-1][1], end)
        return ans

复杂度:时间 O(n log n),空间 O(n) 输出空间。

复杂度分析

  • 解法:排序后线性合并:时间 O(n log n),主要成本来自排序、堆操作或二分搜索中的对数级收缩。 空间 O(n) 输出空间,主要来自辅助数组、队列、哈希集合或递归栈。
189轮转数组 Rotate Array中等

题目规格

官方题意(改写):将数组向右轮转 k 个位置,要求原地修改。 完整原题、样例和英文表述以本题官方链接为准。

输入:判题入口是 Solution.rotate(...),输入参数为 nums(List[int])、k(int)。

输出:函数原地修改输入对象,不需要返回新对象;修改后的数据应满足:将数组向右轮转 k 个位置,要求原地修改。

约束要点
  • 1 <= nums.length <= 10^5
  • -2^31 <= nums[i] <= 2^31 - 1
  • 0 <= k <= 10^5

题目分析

右轮转可以拆成三次反转:整体反转后,前 k 个元素和后 n-k 个元素各自反转,就恢复各段内部顺序。

  • 建模:轮转数组 可以先理解为“将数组向右轮转 k 个位置,要求原地修改。”。先确定答案依赖的是下标、值、路径、窗口、集合还是 DP 状态,再决定数据结构。
  • 不变量:先判断数组操作的核心结构:是前缀/后缀、排序后合并、原地交换,还是以当前位置结尾的局部最优。
  • 边界:重点检查空数组以外的最小规模、全负数、原地修改和输出空间是否计入额外空间。
  • 正确性:先让目标后缀来到前面,再修正两个分段内部顺序。 只要每次更新后不变量仍成立,最终保留下来的状态就对应题目要求的结果。

解法:三次反转

先让目标后缀来到前面,再修正两个分段内部顺序。

class Solution:
    def rotate(self, nums: List[int], k: int) -> None:
        n = len(nums)
        k %= n

        def reverse(l: int, r: int) -> None:
            while l < r:
                nums[l], nums[r] = nums[r], nums[l]
                l += 1
                r -= 1

        reverse(0, n - 1)
        reverse(0, k - 1)
        reverse(k, n - 1)

复杂度:时间 O(n),空间 O(1)。

复杂度分析

  • 解法:三次反转:时间 O(n),主循环中每个元素或节点只进入核心结构常数次。 空间 O(1),只使用常数个指针、计数器或滚动变量;题目要求的输出不计入额外空间。
238除了自身以外数组的乘积 Product of Array Except Self中等

题目规格

官方题意(改写):返回数组中每个位置除自身外所有元素的乘积,不能使用除法。 完整原题、样例和英文表述以本题官方链接为准。

输入:判题入口是 Solution.productExceptSelf(...),输入参数为 nums(List[int])。

输出:返回 List[int];返回值含义是:返回数组中每个位置除自身外所有元素的乘积,不能使用除法。

约束要点
  • 2 <= nums.length <= 10^5
  • -30 <= nums[i] <= 30
  • 输入 保证 数组 answer[i] 在 32 位 整数范围内

题目分析

每个答案等于左侧乘积乘以右侧乘积。先把前缀乘积写入答案,再从右向左累乘后缀。

  • 建模:除了自身以外数组的乘积 可以先理解为“返回数组中每个位置除自身外所有元素的乘积,不能使用除法。”。先确定答案依赖的是下标、值、路径、窗口、集合还是 DP 状态,再决定数据结构。
  • 不变量:先判断数组操作的核心结构:是前缀/后缀、排序后合并、原地交换,还是以当前位置结尾的局部最优。
  • 边界:重点检查空数组以外的最小规模、全负数、原地修改和输出空间是否计入额外空间。
  • 正确性:输出数组复用为前缀结果,后缀只需一个变量。 只要每次更新后不变量仍成立,最终保留下来的状态就对应题目要求的结果。

解法:前缀乘积 + 后缀滚动变量

输出数组复用为前缀结果,后缀只需一个变量。

class Solution:
    def productExceptSelf(self, nums: List[int]) -> List[int]:
        n = len(nums)
        ans = [1] * n
        prefix = 1
        for i in range(n):
            ans[i] = prefix
            prefix *= nums[i]
        suffix = 1
        for i in range(n - 1, -1, -1):
            ans[i] *= suffix
            suffix *= nums[i]
        return ans

复杂度:时间 O(n),空间 O(1) 不计输出。

复杂度分析

  • 解法:前缀乘积 + 后缀滚动变量:时间 O(n),主循环中每个元素或节点只进入核心结构常数次。 空间 O(1) 不计输出,只使用常数个指针、计数器或滚动变量;题目要求的输出不计入额外空间。
41缺失的第一个正数 First Missing Positive困难

题目规格

官方题意(改写):在未排序整数数组中找出最小缺失正整数,要求线性时间和常数额外空间。 完整原题、样例和英文表述以本题官方链接为准。

输入:判题入口是 Solution.firstMissingPositive(...),输入参数为 nums(List[int])。

输出:返回 int;返回值含义是:在未排序整数数组中找出最小缺失正整数,要求线性时间和常数额外空间。

约束要点
  • 1 <= nums.length <= 10^5
  • -2^31 <= nums[i] <= 2^31 - 1

题目分析

答案一定在 1..n+1。把值 x 放到下标 x-1 的位置,最后第一个 nums[i] != i+1 的位置就是缺失值。

  • 建模:缺失的第一个正数 可以先理解为“在未排序整数数组中找出最小缺失正整数,要求线性时间和常数额外空间。”。先确定答案依赖的是下标、值、路径、窗口、集合还是 DP 状态,再决定数据结构。
  • 不变量:先判断数组操作的核心结构:是前缀/后缀、排序后合并、原地交换,还是以当前位置结尾的局部最优。
  • 边界:重点检查空数组以外的最小规模、全负数、原地修改和输出空间是否计入额外空间。
  • 正确性:通过交换让每个合法正整数尽量回到自己的槽位。 只要每次更新后不变量仍成立,最终保留下来的状态就对应题目要求的结果。

解法:原地哈希归位

通过交换让每个合法正整数尽量回到自己的槽位。

class Solution:
    def firstMissingPositive(self, nums: List[int]) -> int:
        n = len(nums)
        for i in range(n):
            while 1 <= nums[i] <= n and nums[nums[i] - 1] != nums[i]:
                j = nums[i] - 1
                nums[i], nums[j] = nums[j], nums[i]
        for i, x in enumerate(nums):
            if x != i + 1:
                return i + 1
        return n + 1

复杂度:时间 O(n),空间 O(1)。

复杂度分析

  • 解法:原地哈希归位:时间 O(n),主循环中每个元素或节点只进入核心结构常数次。 空间 O(1),只使用常数个指针、计数器或滚动变量;题目要求的输出不计入额外空间。

矩阵

本组 4 题,围绕 矩阵 的核心模式展开。先识别不变量,再选择哈希、指针、递归、队列、堆或 DP 状态。

73矩阵置零 Set Matrix Zeroes中等

题目规格

官方题意(改写):若矩阵某个元素为 0,则把它所在行和列都置为 0,要求原地完成。 完整原题、样例和英文表述以本题官方链接为准。

输入:判题入口是 Solution.setZeroes(...),输入参数为 matrix(List[List[int]])。

输出:函数原地修改输入对象,不需要返回新对象;修改后的数据应满足:若矩阵某个元素为 0,则把它所在行和列都置为 0,要求原地完成。

约束要点
  • m == matrix.length
  • n == matrix[0].length
  • 1 <= m, n <= 200
  • -2^31 <= matrix[i][j] <= 2^31 - 1

题目分析

用第一行和第一列作为标记数组。由于它们本身也可能需要清零,先额外记录第一列是否需要清零,第一行可通过 matrix[0][0] 表示。

  • 建模:矩阵置零 可以先理解为“若矩阵某个元素为 0,则把它所在行和列都置为 0,要求原地完成。”。先确定答案依赖的是下标、值、路径、窗口、集合还是 DP 状态,再决定数据结构。
  • 不变量:矩阵题要先确定遍历方向和边界收缩规则;若要求原地修改,需要避免标记信息污染还未处理的格子。
  • 边界:重点检查单行、单列、方阵/非方阵差异,以及边界收缩后是否越界。
  • 正确性:先标记,再根据标记清内部,最后处理首行首列。 只要每次更新后不变量仍成立,最终保留下来的状态就对应题目要求的结果。

解法:首行首列做标记

先标记,再根据标记清内部,最后处理首行首列。

class Solution:
    def setZeroes(self, matrix: List[List[int]]) -> None:
        m, n = len(matrix), len(matrix[0])
        first_col_zero = any(matrix[i][0] == 0 for i in range(m))
        first_row_zero = any(matrix[0][j] == 0 for j in range(n))

        for i in range(1, m):
            for j in range(1, n):
                if matrix[i][j] == 0:
                    matrix[i][0] = matrix[0][j] = 0

        for i in range(1, m):
            for j in range(1, n):
                if matrix[i][0] == 0 or matrix[0][j] == 0:
                    matrix[i][j] = 0

        if first_row_zero:
            for j in range(n):
                matrix[0][j] = 0
        if first_col_zero:
            for i in range(m):
                matrix[i][0] = 0

复杂度:时间 O(mn),空间 O(1)。

复杂度分析

  • 解法:首行首列做标记:时间 O(mn),矩阵或二维 DP 中每个格子会被访问或更新常数次。 空间 O(1),只使用常数个指针、计数器或滚动变量;题目要求的输出不计入额外空间。
54螺旋矩阵 Spiral Matrix中等

题目规格

官方题意(改写):按顺时针螺旋顺序返回矩阵中的所有元素。 完整原题、样例和英文表述以本题官方链接为准。

输入:判题入口是 Solution.spiralOrder(...),输入参数为 matrix(List[List[int]])。

输出:返回 List[int];返回值含义是:按顺时针螺旋顺序返回矩阵中的所有元素。

约束要点
  • m == matrix.length
  • n == matrix[i].length
  • 1 <= m, n <= 10
  • -100 <= matrix[i][j] <= 100

题目分析

维护上、下、左、右四个边界,每走完一条边就收缩对应边界。每次收缩后都要检查边界是否仍合法。

  • 建模:螺旋矩阵 可以先理解为“按顺时针螺旋顺序返回矩阵中的所有元素。”。先确定答案依赖的是下标、值、路径、窗口、集合还是 DP 状态,再决定数据结构。
  • 不变量:矩阵题要先确定遍历方向和边界收缩规则;若要求原地修改,需要避免标记信息污染还未处理的格子。
  • 边界:重点检查单行、单列、方阵/非方阵差异,以及边界收缩后是否越界。
  • 正确性:按右、下、左、上的顺序逐层剥离矩阵。 只要每次更新后不变量仍成立,最终保留下来的状态就对应题目要求的结果。

解法:四边界模拟

按右、下、左、上的顺序逐层剥离矩阵。

class Solution:
    def spiralOrder(self, matrix: List[List[int]]) -> List[int]:
        top, bottom = 0, len(matrix) - 1
        left, right = 0, len(matrix[0]) - 1
        ans = []
        while top <= bottom and left <= right:
            for j in range(left, right + 1):
                ans.append(matrix[top][j])
            top += 1
            for i in range(top, bottom + 1):
                ans.append(matrix[i][right])
            right -= 1
            if top <= bottom:
                for j in range(right, left - 1, -1):
                    ans.append(matrix[bottom][j])
                bottom -= 1
            if left <= right:
                for i in range(bottom, top - 1, -1):
                    ans.append(matrix[i][left])
                left += 1
        return ans

复杂度:时间 O(mn),空间 O(1) 不计输出。

复杂度分析

  • 解法:四边界模拟:时间 O(mn),矩阵或二维 DP 中每个格子会被访问或更新常数次。 空间 O(1) 不计输出,只使用常数个指针、计数器或滚动变量;题目要求的输出不计入额外空间。
48旋转图像 Rotate Image中等

题目规格

官方题意(改写):将 n x n 矩阵顺时针旋转 90 度,要求原地修改。 完整原题、样例和英文表述以本题官方链接为准。

输入:判题入口是 Solution.rotate(...),输入参数为 matrix(List[List[int]])。

输出:函数原地修改输入对象,不需要返回新对象;修改后的数据应满足:将 n x n 矩阵顺时针旋转 90 度,要求原地修改。

约束要点
  • n == matrix.length == matrix[i].length
  • 1 <= n <= 20
  • -1000 <= matrix[i][j] <= 1000

题目分析

顺时针旋转可以分解为先沿主对角线转置,再反转每一行。这样每个元素移动到正确位置。

  • 建模:旋转图像 可以先理解为“将 n x n 矩阵顺时针旋转 90 度,要求原地修改。”。先确定答案依赖的是下标、值、路径、窗口、集合还是 DP 状态,再决定数据结构。
  • 不变量:矩阵题要先确定遍历方向和边界收缩规则;若要求原地修改,需要避免标记信息污染还未处理的格子。
  • 边界:重点检查单行、单列、方阵/非方阵差异,以及边界收缩后是否越界。
  • 正确性:matrix[i][j] 与 matrix[j][i] 交换后,再水平镜像。 只要每次更新后不变量仍成立,最终保留下来的状态就对应题目要求的结果。

解法:转置 + 行反转

matrix[i][j] 与 matrix[j][i] 交换后,再水平镜像。

class Solution:
    def rotate(self, matrix: List[List[int]]) -> None:
        n = len(matrix)
        for i in range(n):
            for j in range(i + 1, n):
                matrix[i][j], matrix[j][i] = matrix[j][i], matrix[i][j]
        for row in matrix:
            row.reverse()

复杂度:时间 O(n^2),空间 O(1)。

复杂度分析

  • 解法:转置 + 行反转:时间 O(n^2),需要枚举两个维度、中心扩展或区间端点,形成二次级状态/比较。 空间 O(1),只使用常数个指针、计数器或滚动变量;题目要求的输出不计入额外空间。
240搜索二维矩阵 II Search a 2D Matrix II中等

题目规格

官方题意(改写):在每行每列都递增的矩阵中查找目标值。 完整原题、样例和英文表述以本题官方链接为准。

输入:判题入口是 Solution.searchMatrix(...),输入参数为 matrix(List[List[int]])、target(int)。

输出:返回 bool;返回值含义是:在每行每列都递增的矩阵中查找目标值。

约束要点
  • m == matrix.length
  • n == matrix[i].length
  • 1 <= n, m <= 300
  • -10^9 <= matrix[i][j] <= 10^9
  • 每行的所有元素从左到右升序排列
  • 每列的所有元素从上到下升序排列

题目分析

从右上角开始:当前值大于目标时只能向左变小,小于目标时只能向下变大。每步排除一行或一列。

  • 建模:搜索二维矩阵 II 可以先理解为“在每行每列都递增的矩阵中查找目标值。”。先确定答案依赖的是下标、值、路径、窗口、集合还是 DP 状态,再决定数据结构。
  • 不变量:矩阵题要先确定遍历方向和边界收缩规则;若要求原地修改,需要避免标记信息污染还未处理的格子。
  • 边界:重点检查单行、单列、方阵/非方阵差异,以及边界收缩后是否越界。
  • 正确性:利用二维有序性,单步排除一个方向。 只要每次更新后不变量仍成立,最终保留下来的状态就对应题目要求的结果。

解法:右上角阶梯搜索

利用二维有序性,单步排除一个方向。

class Solution:
    def searchMatrix(self, matrix: List[List[int]], target: int) -> bool:
        row, col = 0, len(matrix[0]) - 1
        while row < len(matrix) and col >= 0:
            x = matrix[row][col]
            if x == target:
                return True
            if x > target:
                col -= 1
            else:
                row += 1
        return False

复杂度:时间 O(m+n),空间 O(1)。

复杂度分析

  • 解法:右上角阶梯搜索:时间 O(m+n),矩阵题要先确定遍历方向和边界收缩规则;若要求原地修改,需要避免标记信息污染还未处理的格子。 空间 O(1),只使用常数个指针、计数器或滚动变量;题目要求的输出不计入额外空间。

链表

本组 14 题,围绕 链表 的核心模式展开。先识别不变量,再选择哈希、指针、递归、队列、堆或 DP 状态。

160相交链表 Intersection of Two Linked Lists简单

题目规格

官方题意(改写):找出两个单链表开始相交的节点;相交指的是节点对象相同,不是值相同。 完整原题、样例和英文表述以本题官方链接为准。

输入:判题入口是 Solution.getIntersectionNode(...),输入参数为 headA(ListNode)、headB(ListNode)。

输出:返回 Optional[ListNode];返回值含义是:找出两个单链表开始相交的节点;相交指的是节点对象相同,不是值相同。

约束要点
  • listA 中节点数目为 m
  • listB 中节点数目为 n
  • 1 <= m, n <= 3 * 10^4
  • 1 <= Node.val <= 10^5
  • 0 <= skipA <= m
  • 0 <= skipB <= n

题目分析

两个指针分别从 A、B 出发,走到尾后切换到另一条链。若有交点,二者会在走过相同总长度后相遇;若无交点,都会走到 None。

  • 建模:相交链表 可以先理解为“找出两个单链表开始相交的节点;相交指的是节点对象相同,不是值相同。”。先确定答案依赖的是下标、值、路径、窗口、集合还是 DP 状态,再决定数据结构。
  • 不变量:链表题的不变量通常落在指针关系上:dummy 简化头节点变化,prev/cur/nxt 保证断链和重连顺序不会丢节点。
  • 边界:重点检查空链表、单节点、删除/交换头节点、奇偶长度和尾节点 next 是否正确。
  • 正确性:A+B 与 B+A 总长度相同,自动抵消长度差。 只要每次更新后不变量仍成立,最终保留下来的状态就对应题目要求的结果。

解法:双指针换头对齐长度

A+B 与 B+A 总长度相同,自动抵消长度差。

class Solution:
    def getIntersectionNode(self, headA: ListNode, headB: ListNode) -> Optional[ListNode]:
        p, q = headA, headB
        while p is not q:
            p = p.next if p else headB
            q = q.next if q else headA
        return p

复杂度:时间 O(m+n),空间 O(1)。

复杂度分析

  • 解法:双指针换头对齐长度:时间 O(m+n),链表题的不变量通常落在指针关系上:dummy 简化头节点变化,prev/cur/nxt 保证断链和重连顺序不会丢节点。 空间 O(1),只使用常数个指针、计数器或滚动变量;题目要求的输出不计入额外空间。
206反转链表 Reverse Linked List简单

题目规格

官方题意(改写):反转单链表并返回新的头节点。 完整原题、样例和英文表述以本题官方链接为准。

输入:判题入口是 Solution.reverseList(...),输入参数为 head(Optional[ListNode])。

输出:返回 Optional[ListNode];返回值含义是:反转单链表并返回新的头节点。

约束要点
  • 链表中节点的数目范围是 [0, 5000]
  • -5000 <= Node.val <= 5000

题目分析

迭代写法维护 prev 和 cur,每次暂存 next 后把 cur.next 指回 prev。递归写法先反转后半段,再让当前节点接到后半段尾部。

  • 建模:反转链表 可以先理解为“反转单链表并返回新的头节点。”。先确定答案依赖的是下标、值、路径、窗口、集合还是 DP 状态,再决定数据结构。
  • 不变量:链表题的不变量通常落在指针关系上:dummy 简化头节点变化,prev/cur/nxt 保证断链和重连顺序不会丢节点。
  • 边界:重点检查空链表、单节点、删除/交换头节点、奇偶长度和尾节点 next 是否正确。
  • 正确性:prev 始终是已经反转好的前缀头。 只要每次更新后不变量仍成立,最终保留下来的状态就对应题目要求的结果。

解法1:迭代反转指针

prev 始终是已经反转好的前缀头。

class Solution:
    def reverseList(self, head: Optional[ListNode]) -> Optional[ListNode]:
        prev, cur = None, head
        while cur:
            nxt = cur.next
            cur.next = prev
            prev = cur
            cur = nxt
        return prev

复杂度:时间 O(n),空间 O(1)。

解法2:递归反转

递归返回新头,再把当前节点挂到后继节点之后。

class Solution:
    def reverseList(self, head: Optional[ListNode]) -> Optional[ListNode]:
        if not head or not head.next:
            return head
        new_head = self.reverseList(head.next)
        head.next.next = head
        head.next = None
        return new_head

复杂度:时间 O(n),空间 O(n) 递归栈。

复杂度分析

  • 解法1:迭代反转指针:时间 O(n),主循环中每个元素或节点只进入核心结构常数次。 空间 O(1),只使用常数个指针、计数器或滚动变量;题目要求的输出不计入额外空间。
  • 解法2:递归反转:时间 O(n),主循环中每个元素或节点只进入核心结构常数次。 空间 O(n) 递归栈,主要来自辅助数组、队列、哈希集合或递归栈。
234回文链表 Palindrome Linked List简单

题目规格

官方题意(改写):判断单链表的节点值是否构成回文序列。 完整原题、样例和英文表述以本题官方链接为准。

输入:判题入口是 Solution.isPalindrome(...),输入参数为 head(Optional[ListNode])。

输出:返回 bool;返回值含义是:判断单链表的节点值是否构成回文序列。

约束要点
  • 链表中节点数目在范围 [1, 10^5] 内
  • 0 <= Node.val <= 9

题目分析

快慢指针找到中点,反转后半段,然后逐个比较前半段和后半段。若需要保持链表,可比较后再反转恢复。

  • 建模:回文链表 可以先理解为“判断单链表的节点值是否构成回文序列。”。先确定答案依赖的是下标、值、路径、窗口、集合还是 DP 状态,再决定数据结构。
  • 不变量:链表题的不变量通常落在指针关系上:dummy 简化头节点变化,prev/cur/nxt 保证断链和重连顺序不会丢节点。
  • 边界:重点检查空链表、单节点、删除/交换头节点、奇偶长度和尾节点 next 是否正确。
  • 正确性:把链表问题转成两段同步扫描。 只要每次更新后不变量仍成立,最终保留下来的状态就对应题目要求的结果。

解法:中点 + 反转后半段

把链表问题转成两段同步扫描。

class Solution:
    def isPalindrome(self, head: Optional[ListNode]) -> bool:
        slow = fast = head
        while fast and fast.next:
            slow = slow.next
            fast = fast.next.next

        prev, cur = None, slow
        while cur:
            nxt = cur.next
            cur.next = prev
            prev = cur
            cur = nxt

        left, right = head, prev
        while right:
            if left.val != right.val:
                return False
            left = left.next
            right = right.next
        return True

复杂度:时间 O(n),空间 O(1)。

复杂度分析

  • 解法:中点 + 反转后半段:时间 O(n),主循环中每个元素或节点只进入核心结构常数次。 空间 O(1),只使用常数个指针、计数器或滚动变量;题目要求的输出不计入额外空间。
141环形链表 Linked List Cycle简单

题目规格

官方题意(改写):判断链表中是否存在环。 完整原题、样例和英文表述以本题官方链接为准。

输入:判题入口是 Solution.hasCycle(...),输入参数为 head(Optional[ListNode])。

输出:返回 bool;返回值含义是:判断链表中是否存在环。

约束要点
  • 链表中节点的数目范围是 [0, 10^4]
  • -10^5 <= Node.val <= 10^5
  • pos 为 -1 或者链表中的一个 有效索引

题目分析

快慢指针同时走,slow 一次一步、fast 一次两步。如果有环,fast 会在环内追上 slow;如果无环,fast 会到达 None。

  • 建模:环形链表 可以先理解为“判断链表中是否存在环。”。先确定答案依赖的是下标、值、路径、窗口、集合还是 DP 状态,再决定数据结构。
  • 不变量:链表题的不变量通常落在指针关系上:dummy 简化头节点变化,prev/cur/nxt 保证断链和重连顺序不会丢节点。
  • 边界:重点检查空链表、单节点、删除/交换头节点、奇偶长度和尾节点 next 是否正确。
  • 正确性:速度差为 1,在有限环内必然相遇。 只要每次更新后不变量仍成立,最终保留下来的状态就对应题目要求的结果。

解法:Floyd 判环

速度差为 1,在有限环内必然相遇。

class Solution:
    def hasCycle(self, head: Optional[ListNode]) -> bool:
        slow = fast = head
        while fast and fast.next:
            slow = slow.next
            fast = fast.next.next
            if slow is fast:
                return True
        return False

复杂度:时间 O(n),空间 O(1)。

复杂度分析

  • 解法:Floyd 判环:时间 O(n),主循环中每个元素或节点只进入核心结构常数次。 空间 O(1),只使用常数个指针、计数器或滚动变量;题目要求的输出不计入额外空间。
142环形链表 II Linked List Cycle II中等

题目规格

官方题意(改写):若链表有环,返回环的入口节点;否则返回 None。 完整原题、样例和英文表述以本题官方链接为准。

输入:判题入口是 Solution.detectCycle(...),输入参数为 head(Optional[ListNode])。

输出:返回 Optional[ListNode];返回值含义是:若链表有环,返回环的入口节点;否则返回 None。

约束要点
  • 链表中节点的数目范围在范围 [0, 10^4] 内
  • -10^5 <= Node.val <= 10^5
  • pos 的值为 -1 或者链表中的一个有效索引

题目分析

快慢指针相遇后,让一个指针回到头节点,两个指针都一次一步,它们再次相遇处就是入环点。这来自链表头到入口距离与相遇点到入口距离的模环长关系。

  • 建模:环形链表 II 可以先理解为“若链表有环,返回环的入口节点;否则返回 None。”。先确定答案依赖的是下标、值、路径、窗口、集合还是 DP 状态,再决定数据结构。
  • 不变量:链表题的不变量通常落在指针关系上:dummy 简化头节点变化,prev/cur/nxt 保证断链和重连顺序不会丢节点。
  • 边界:重点检查空链表、单节点、删除/交换头节点、奇偶长度和尾节点 next 是否正确。
  • 正确性:先判相遇,再用同速指针对齐入口。 只要每次更新后不变量仍成立,最终保留下来的状态就对应题目要求的结果。

解法:Floyd 找入口

先判相遇,再用同速指针对齐入口。

class Solution:
    def detectCycle(self, head: Optional[ListNode]) -> Optional[ListNode]:
        slow = fast = head
        while fast and fast.next:
            slow = slow.next
            fast = fast.next.next
            if slow is fast:
                p = head
                while p is not slow:
                    p = p.next
                    slow = slow.next
                return p
        return None

复杂度:时间 O(n),空间 O(1)。

复杂度分析

  • 解法:Floyd 找入口:时间 O(n),主循环中每个元素或节点只进入核心结构常数次。 空间 O(1),只使用常数个指针、计数器或滚动变量;题目要求的输出不计入额外空间。
21合并两个有序链表 Merge Two Sorted Lists简单

题目规格

官方题意(改写):合并两个升序链表,返回合并后的升序链表。 完整原题、样例和英文表述以本题官方链接为准。

输入:判题入口是 Solution.mergeTwoLists(...),输入参数为 list1(Optional[ListNode])、list2(Optional[ListNode])。

输出:返回 Optional[ListNode];返回值含义是:合并两个升序链表,返回合并后的升序链表。

约束要点
  • 两个链表的节点数目范围是 [0, 50]
  • -100 <= Node.val <= 100
  • l1 和 l2 均按非递减顺序排列

题目分析

使用 dummy 节点简化头节点处理。每次取两个链表头部较小者接到结果尾部,最后接上剩余部分。

  • 建模:合并两个有序链表 可以先理解为“合并两个升序链表,返回合并后的升序链表。”。先确定答案依赖的是下标、值、路径、窗口、集合还是 DP 状态,再决定数据结构。
  • 不变量:链表题的不变量通常落在指针关系上:dummy 简化头节点变化,prev/cur/nxt 保证断链和重连顺序不会丢节点。
  • 边界:重点检查空链表、单节点、删除/交换头节点、奇偶长度和尾节点 next 是否正确。
  • 正确性:tail 始终指向结果链表最后一个节点。 只要每次更新后不变量仍成立,最终保留下来的状态就对应题目要求的结果。

解法:哑节点迭代合并

tail 始终指向结果链表最后一个节点。

class Solution:
    def mergeTwoLists(self, list1: Optional[ListNode], list2: Optional[ListNode]) -> Optional[ListNode]:
        dummy = ListNode(0)
        tail = dummy
        while list1 and list2:
            if list1.val <= list2.val:
                tail.next = list1
                list1 = list1.next
            else:
                tail.next = list2
                list2 = list2.next
            tail = tail.next
        tail.next = list1 or list2
        return dummy.next

复杂度:时间 O(m+n),空间 O(1)。

复杂度分析

  • 解法:哑节点迭代合并:时间 O(m+n),链表题的不变量通常落在指针关系上:dummy 简化头节点变化,prev/cur/nxt 保证断链和重连顺序不会丢节点。 空间 O(1),只使用常数个指针、计数器或滚动变量;题目要求的输出不计入额外空间。
2两数相加 Add Two Numbers中等

题目规格

官方题意(改写):两个非空链表按逆序存储非负整数,返回它们相加后的逆序链表。 完整原题、样例和英文表述以本题官方链接为准。

输入:判题入口是 Solution.addTwoNumbers(...),输入参数为 l1(Optional[ListNode])、l2(Optional[ListNode])。

输出:返回 Optional[ListNode];返回值含义是:两个非空链表按逆序存储非负整数,返回它们相加后的逆序链表。

约束要点
  • 每个链表中的节点数在范围 [1, 100] 内
  • 0 <= Node.val <= 9
  • 题目数据保证列表表示的数字不含前导零

题目分析

从低位到高位同步相加,用 carry 保存进位。只要任一链表未结束或还有进位,就继续生成节点。

  • 建模:两数相加 可以先理解为“两个非空链表按逆序存储非负整数,返回它们相加后的逆序链表。”。先确定答案依赖的是下标、值、路径、窗口、集合还是 DP 状态,再决定数据结构。
  • 不变量:链表题的不变量通常落在指针关系上:dummy 简化头节点变化,prev/cur/nxt 保证断链和重连顺序不会丢节点。
  • 边界:重点检查空链表、单节点、删除/交换头节点、奇偶长度和尾节点 next 是否正确。
  • 正确性:链表天然从最低位开始,正好模拟竖式加法。 只要每次更新后不变量仍成立,最终保留下来的状态就对应题目要求的结果。

解法:逐位相加

链表天然从最低位开始,正好模拟竖式加法。

class Solution:
    def addTwoNumbers(self, l1: Optional[ListNode], l2: Optional[ListNode]) -> Optional[ListNode]:
        dummy = ListNode(0)
        tail = dummy
        carry = 0
        while l1 or l2 or carry:
            total = carry
            if l1:
                total += l1.val
                l1 = l1.next
            if l2:
                total += l2.val
                l2 = l2.next
            carry, digit = divmod(total, 10)
            tail.next = ListNode(digit)
            tail = tail.next
        return dummy.next

复杂度:时间 O(max(m,n)),空间 O(max(m,n)) 输出空间。

复杂度分析

  • 解法:逐位相加:时间 O(max(m,n)),链表题的不变量通常落在指针关系上:dummy 简化头节点变化,prev/cur/nxt 保证断链和重连顺序不会丢节点。 空间 O(max(m,n)) 输出空间,额外空间来自代码中显式维护的辅助结构;返回结果本身通常不计入额外空间。
19删除链表的倒数第 N 个结点 Remove Nth Node From End of List中等

题目规格

官方题意(改写):删除链表倒数第 n 个节点,并返回头节点。 完整原题、样例和英文表述以本题官方链接为准。

输入:判题入口是 Solution.removeNthFromEnd(...),输入参数为 head(Optional[ListNode])、n(int)。

输出:返回 Optional[ListNode];返回值含义是:删除链表倒数第 n 个节点,并返回头节点。

约束要点
  • 链表中结点的数目为 sz
  • 1 <= sz <= 30
  • 0 <= Node.val <= 100
  • 1 <= n <= sz

题目分析

使用 dummy 处理删除头节点的情况。快指针先走 n+1 步,保持 fast 和 slow 间隔 n 个节点;fast 到尾时 slow.next 就是待删节点。

  • 建模:删除链表的倒数第 N 个结点 可以先理解为“删除链表倒数第 n 个节点,并返回头节点。”。先确定答案依赖的是下标、值、路径、窗口、集合还是 DP 状态,再决定数据结构。
  • 不变量:链表题的不变量通常落在指针关系上:dummy 简化头节点变化,prev/cur/nxt 保证断链和重连顺序不会丢节点。
  • 边界:重点检查空链表、单节点、删除/交换头节点、奇偶长度和尾节点 next 是否正确。
  • 正确性:dummy 让“删除第一个节点”与普通删除统一。 只要每次更新后不变量仍成立,最终保留下来的状态就对应题目要求的结果。

解法:快慢指针固定间距

dummy 让“删除第一个节点”与普通删除统一。

class Solution:
    def removeNthFromEnd(self, head: Optional[ListNode], n: int) -> Optional[ListNode]:
        dummy = ListNode(0, head)
        fast = slow = dummy
        for _ in range(n + 1):
            fast = fast.next
        while fast:
            fast = fast.next
            slow = slow.next
        slow.next = slow.next.next
        return dummy.next

复杂度:时间 O(n),空间 O(1)。

复杂度分析

  • 解法:快慢指针固定间距:时间 O(n),主循环中每个元素或节点只进入核心结构常数次。 空间 O(1),只使用常数个指针、计数器或滚动变量;题目要求的输出不计入额外空间。
24两两交换链表中的节点 Swap Nodes in Pairs中等

题目规格

官方题意(改写):两两交换链表中的相邻节点,不能只交换节点值。 完整原题、样例和英文表述以本题官方链接为准。

输入:判题入口是 Solution.swapPairs(...),输入参数为 head(Optional[ListNode])。

输出:返回 Optional[ListNode];返回值含义是:两两交换链表中的相邻节点,不能只交换节点值。

约束要点
  • 链表中节点的数目在范围 [0, 100] 内
  • 0 <= Node.val <= 100

题目分析

每轮处理 prev 后面的两个节点 a、b,把 prev -> a -> b -> next 改成 prev -> b -> a -> next,再让 prev 移到 a。

  • 建模:两两交换链表中的节点 可以先理解为“两两交换链表中的相邻节点,不能只交换节点值。”。先确定答案依赖的是下标、值、路径、窗口、集合还是 DP 状态,再决定数据结构。
  • 不变量:链表题的不变量通常落在指针关系上:dummy 简化头节点变化,prev/cur/nxt 保证断链和重连顺序不会丢节点。
  • 边界:重点检查空链表、单节点、删除/交换头节点、奇偶长度和尾节点 next 是否正确。
  • 正确性:dummy + prev 让每组交换都使用同一套指针操作。 只要每次更新后不变量仍成立,最终保留下来的状态就对应题目要求的结果。

解法:指针重连

dummy + prev 让每组交换都使用同一套指针操作。

class Solution:
    def swapPairs(self, head: Optional[ListNode]) -> Optional[ListNode]:
        dummy = ListNode(0, head)
        prev = dummy
        while prev.next and prev.next.next:
            a = prev.next
            b = a.next
            a.next = b.next
            b.next = a
            prev.next = b
            prev = a
        return dummy.next

复杂度:时间 O(n),空间 O(1)。

复杂度分析

  • 解法:指针重连:时间 O(n),主循环中每个元素或节点只进入核心结构常数次。 空间 O(1),只使用常数个指针、计数器或滚动变量;题目要求的输出不计入额外空间。
25K 个一组翻转链表 Reverse Nodes in k-Group困难

题目规格

官方题意(改写):每 k 个节点一组反转链表,不足 k 个节点的尾部保持原样。 完整原题、样例和英文表述以本题官方链接为准。

输入:判题入口是 Solution.reverseKGroup(...),输入参数为 head(Optional[ListNode])、k(int)。

输出:返回 Optional[ListNode];返回值含义是:每 k 个节点一组反转链表,不足 k 个节点的尾部保持原样。

约束要点
  • 链表中的节点数目为 n
  • 1 <= k <= n <= 5000
  • 0 <= Node.val <= 1000

题目分析

先检查当前组是否有 k 个节点。若有,原地反转这 k 个节点,再把前一组尾部接到反转后的头,当前组原头变为新尾。

  • 建模:K 个一组翻转链表 可以先理解为“每 k 个节点一组反转链表,不足 k 个节点的尾部保持原样。”。先确定答案依赖的是下标、值、路径、窗口、集合还是 DP 状态,再决定数据结构。
  • 不变量:链表题的不变量通常落在指针关系上:dummy 简化头节点变化,prev/cur/nxt 保证断链和重连顺序不会丢节点。
  • 边界:重点检查空链表、单节点、删除/交换头节点、奇偶长度和尾节点 next 是否正确。
  • 正确性:每组反转前先找到 kth,避免错误反转不足 k 的尾部。 只要每次更新后不变量仍成立,最终保留下来的状态就对应题目要求的结果。

解法:分组检查 + 局部反转

每组反转前先找到 kth,避免错误反转不足 k 的尾部。

class Solution:
    def reverseKGroup(self, head: Optional[ListNode], k: int) -> Optional[ListNode]:
        dummy = ListNode(0, head)
        group_prev = dummy

        while True:
            kth = group_prev
            for _ in range(k):
                kth = kth.next
                if not kth:
                    return dummy.next
            group_next = kth.next

            prev, cur = group_next, group_prev.next
            while cur is not group_next:
                nxt = cur.next
                cur.next = prev
                prev = cur
                cur = nxt

            new_tail = group_prev.next
            group_prev.next = kth
            group_prev = new_tail

复杂度:时间 O(n),空间 O(1)。

复杂度分析

  • 解法:分组检查 + 局部反转:时间 O(n),主循环中每个元素或节点只进入核心结构常数次。 空间 O(1),只使用常数个指针、计数器或滚动变量;题目要求的输出不计入额外空间。
138随机链表的复制 Copy List with Random Pointer中等

题目规格

官方题意(改写):复制一个带 next 和 random 指针的链表,返回深拷贝后的头节点。 完整原题、样例和英文表述以本题官方链接为准。

输入:判题入口是 Solution.copyRandomList(...),输入参数为 head('Optional[Node]')。

输出:返回 'Optional[Node]';返回值含义是:复制一个带 next 和 random 指针的链表,返回深拷贝后的头节点。

约束要点
  • 0 <= n <= 1000
  • -10^4 <= Node.val <= 10^4
  • Node.random 为 null 或指向链表中的节点

题目分析

哈希表可以记录原节点到新节点的映射。先创建所有节点,再第二遍连接 next/random。也可以原地穿插节点来做到 O(1) 额外空间。

  • 建模:随机链表的复制 可以先理解为“复制一个带 next 和 random 指针的链表,返回深拷贝后的头节点。”。先确定答案依赖的是下标、值、路径、窗口、集合还是 DP 状态,再决定数据结构。
  • 不变量:链表题的不变量通常落在指针关系上:dummy 简化头节点变化,prev/cur/nxt 保证断链和重连顺序不会丢节点。
  • 边界:重点检查空链表、单节点、删除/交换头节点、奇偶长度和尾节点 next 是否正确。
  • 正确性:random 指针可能指向任意节点,先建立完整映射最直接。 只要每次更新后不变量仍成立,最终保留下来的状态就对应题目要求的结果。

解法:哈希表映射原节点到新节点

random 指针可能指向任意节点,先建立完整映射最直接。

class Solution:
    def copyRandomList(self, head: 'Optional[Node]') -> 'Optional[Node]':
        if not head:
            return None
        mp = {}
        cur = head
        while cur:
            mp[cur] = Node(cur.val)
            cur = cur.next
        cur = head
        while cur:
            mp[cur].next = mp.get(cur.next)
            mp[cur].random = mp.get(cur.random)
            cur = cur.next
        return mp[head]

复杂度:时间 O(n),空间 O(n)。

复杂度分析

  • 解法:哈希表映射原节点到新节点:时间 O(n),主循环中每个元素或节点只进入核心结构常数次。 空间 O(n),主要来自辅助数组、队列、哈希集合或递归栈。
148排序链表 Sort List中等

题目规格

官方题意(改写):在 O(n log n) 时间内对链表进行排序。 完整原题、样例和英文表述以本题官方链接为准。

输入:判题入口是 Solution.sortList(...),输入参数为 head(Optional[ListNode])。

输出:返回 Optional[ListNode];返回值含义是:在 O(n log n) 时间内对链表进行排序。

约束要点
  • 链表中节点的数目在范围 [0, 5 * 10^4] 内
  • -10^5 <= Node.val <= 10^5

题目分析

链表适合归并排序。用快慢指针切成两半,递归排序后再按有序链表合并。

  • 建模:排序链表 可以先理解为“在 O(n log n) 时间内对链表进行排序。”。先确定答案依赖的是下标、值、路径、窗口、集合还是 DP 状态,再决定数据结构。
  • 不变量:链表题的不变量通常落在指针关系上:dummy 简化头节点变化,prev/cur/nxt 保证断链和重连顺序不会丢节点。
  • 边界:重点检查空链表、单节点、删除/交换头节点、奇偶长度和尾节点 next 是否正确。
  • 正确性:找中点断链,分别排序,再合并两个有序链表。 只要每次更新后不变量仍成立,最终保留下来的状态就对应题目要求的结果。

解法:链表归并排序

找中点断链,分别排序,再合并两个有序链表。

class Solution:
    def sortList(self, head: Optional[ListNode]) -> Optional[ListNode]:
        if not head or not head.next:
            return head
        slow, fast = head, head.next
        while fast and fast.next:
            slow = slow.next
            fast = fast.next.next
        mid = slow.next
        slow.next = None
        left = self.sortList(head)
        right = self.sortList(mid)
        return self.merge(left, right)

    def merge(self, a: Optional[ListNode], b: Optional[ListNode]) -> Optional[ListNode]:
        dummy = ListNode(0)
        tail = dummy
        while a and b:
            if a.val <= b.val:
                tail.next = a
                a = a.next
            else:
                tail.next = b
                b = b.next
            tail = tail.next
        tail.next = a or b
        return dummy.next

复杂度:时间 O(n log n),空间 O(log n) 递归栈。

复杂度分析

  • 解法:链表归并排序:时间 O(n log n),主要成本来自排序、堆操作或二分搜索中的对数级收缩。 空间 O(log n) 递归栈,主要来自递归深度或平衡分治栈。
23合并 K 个升序链表 Merge k Sorted Lists困难

题目规格

官方题意(改写):合并 k 个升序链表,返回一个升序链表。 完整原题、样例和英文表述以本题官方链接为准。

输入:判题入口是 Solution.mergeKLists(...),输入参数为 lists(List[Optional[ListNode]])。

输出:返回 Optional[ListNode];返回值含义是:合并 k 个升序链表,返回一个升序链表。

约束要点
  • k == lists.length
  • 0 <= k <= 10^4
  • 0 <= lists[i].length <= 500
  • -10^4 <= lists[i][j] <= 10^4
  • lists[i] 按 升序 排列
  • lists[i].length 的总和不超过 10^4

题目分析

小根堆维护每条链表当前头节点。每次弹出最小节点接到结果后,把该节点的 next 放回堆。

  • 建模:合并 K 个升序链表 可以先理解为“合并 k 个升序链表,返回一个升序链表。”。先确定答案依赖的是下标、值、路径、窗口、集合还是 DP 状态,再决定数据结构。
  • 不变量:链表题的不变量通常落在指针关系上:dummy 简化头节点变化,prev/cur/nxt 保证断链和重连顺序不会丢节点。
  • 边界:重点检查空链表、单节点、删除/交换头节点、奇偶长度和尾节点 next 是否正确。
  • 正确性:堆中最多保留 k 个候选头节点,用序号打破值相同的比较。 只要每次更新后不变量仍成立,最终保留下来的状态就对应题目要求的结果。

解法:小根堆多路归并

堆中最多保留 k 个候选头节点,用序号打破值相同的比较。

import heapq

class Solution:
    def mergeKLists(self, lists: List[Optional[ListNode]]) -> Optional[ListNode]:
        heap = []
        for i, node in enumerate(lists):
            if node:
                heapq.heappush(heap, (node.val, i, node))
        dummy = ListNode(0)
        tail = dummy
        while heap:
            _, i, node = heapq.heappop(heap)
            tail.next = node
            tail = tail.next
            if node.next:
                heapq.heappush(heap, (node.next.val, i, node.next))
        return dummy.next

复杂度:时间 O(N log k),空间 O(k)。

复杂度分析

  • 解法:小根堆多路归并:时间 O(N log k),每个元素最多触发一次堆插入/弹出,堆大小受 k 控制。 空间 O(k),主要来自大小受 k 控制的堆、窗口或候选集合。
146LRU 缓存 LRU Cache中等

题目规格

官方题意(改写):设计 LRU 缓存,支持 get 和 put,容量满时淘汰最近最少使用的键。 完整原题、样例和英文表述以本题官方链接为准。

输入:设计类 LRUCache,判题器会按照官方操作序列调用构造器和公开方法。

输出:每次查询方法返回题目要求的布尔值、整数或浮点数;更新类方法只改变对象内部状态。核心目标是:设计 LRU 缓存,支持 get 和 put,容量满时淘汰最近最少使用的键。

约束要点
  • 1 <= capacity <= 3000
  • 0 <= key <= 10000
  • 0 <= value <= 10^5
  • 最多调用 2 * 10^5 次 get 和 put

题目分析

Python 的 OrderedDict 可以同时维护 key 查找和访问顺序。每次 get/put 命中后把 key 移到末尾;容量超限时弹出开头的最旧项。

  • 建模:LRU 缓存 可以先理解为“设计 LRU 缓存,支持 get 和 put,容量满时淘汰最近最少使用的键。”。先确定答案依赖的是下标、值、路径、窗口、集合还是 DP 状态,再决定数据结构。
  • 不变量:链表题的不变量通常落在指针关系上:dummy 简化头节点变化,prev/cur/nxt 保证断链和重连顺序不会丢节点。
  • 边界:重点检查空链表、单节点、删除/交换头节点、奇偶长度和尾节点 next 是否正确。
  • 正确性:字典负责 O(1) 查找,双向链表顺序由 OrderedDict 内部维护。 只要每次更新后不变量仍成立,最终保留下来的状态就对应题目要求的结果。

解法:OrderedDict 维护访问顺序

字典负责 O(1) 查找,双向链表顺序由 OrderedDict 内部维护。

from collections import OrderedDict

class LRUCache:
    def __init__(self, capacity: int):
        self.capacity = capacity
        self.data = OrderedDict()

    def get(self, key: int) -> int:
        if key not in self.data:
            return -1
        self.data.move_to_end(key)
        return self.data[key]

    def put(self, key: int, value: int) -> None:
        if key in self.data:
            self.data.move_to_end(key)
        self.data[key] = value
        if len(self.data) > self.capacity:
            self.data.popitem(last=False)

复杂度:时间 get/put 均摊 O(1),空间 O(capacity)。

复杂度分析

  • 解法:OrderedDict 维护访问顺序:时间 get/put 均摊 O(1),操作只读取或更新固定数量的变量/指针。 空间 O(capacity),额外空间来自代码中显式维护的辅助结构;返回结果本身通常不计入额外空间。

二叉树

本组 15 题,围绕 二叉树 的核心模式展开。先识别不变量,再选择哈希、指针、递归、队列、堆或 DP 状态。

94二叉树的中序遍历 Binary Tree Inorder Traversal简单

题目规格

官方题意(改写):返回二叉树的中序遍历结果。 完整原题、样例和英文表述以本题官方链接为准。

输入:判题入口是 Solution.inorderTraversal(...),输入参数为 root(Optional[TreeNode])。

输出:返回 List[int];返回值含义是:返回二叉树的中序遍历结果。

约束要点
  • 树中节点数目在范围 [0, 100] 内
  • -100 <= Node.val <= 100

题目分析

中序遍历顺序是左、根、右。递归最简洁;迭代版用栈模拟递归路径。

  • 建模:二叉树的中序遍历 可以先理解为“返回二叉树的中序遍历结果。”。先确定答案依赖的是下标、值、路径、窗口、集合还是 DP 状态,再决定数据结构。
  • 不变量:二叉树题先定义递归函数返回什么:高度、是否合法、路径贡献、构造出的子树,还是目标节点;返回值要能服务父节点。
  • 边界:重点检查空树、单节点、左右子树缺失、重复值限制和递归返回 None 的处理。
  • 正确性:一路压入左链,弹出节点访问后转向右子树。 只要每次更新后不变量仍成立,最终保留下来的状态就对应题目要求的结果。

解法:迭代栈

一路压入左链,弹出节点访问后转向右子树。

class Solution:
    def inorderTraversal(self, root: Optional[TreeNode]) -> List[int]:
        ans, stack = [], []
        cur = root
        while cur or stack:
            while cur:
                stack.append(cur)
                cur = cur.left
            cur = stack.pop()
            ans.append(cur.val)
            cur = cur.right
        return ans

复杂度:时间 O(n),空间 O(h)。

复杂度分析

  • 解法:迭代栈:时间 O(n),主循环中每个元素或节点只进入核心结构常数次。 空间 O(h),主要来自树高相关的递归调用栈或显式栈。
104二叉树的最大深度 Maximum Depth of Binary Tree简单

题目规格

官方题意(改写):求二叉树的最大深度。 完整原题、样例和英文表述以本题官方链接为准。

输入:判题入口是 Solution.maxDepth(...),输入参数为 root(Optional[TreeNode])。

输出:返回 int;返回值含义是:求二叉树的最大深度。

约束要点
  • 树中节点的数量在 [0, 10^4] 区间内
  • -100 <= Node.val <= 100

题目分析

树的最大深度等于左右子树最大深度的较大值加 1。空节点深度为 0。

  • 建模:二叉树的最大深度 可以先理解为“求二叉树的最大深度。”。先确定答案依赖的是下标、值、路径、窗口、集合还是 DP 状态,再决定数据结构。
  • 不变量:二叉树题先定义递归函数返回什么:高度、是否合法、路径贡献、构造出的子树,还是目标节点;返回值要能服务父节点。
  • 边界:重点检查空树、单节点、左右子树缺失、重复值限制和递归返回 None 的处理。
  • 正确性:后序返回子树高度。 只要每次更新后不变量仍成立,最终保留下来的状态就对应题目要求的结果。

解法:递归求高度

后序返回子树高度。

class Solution:
    def maxDepth(self, root: Optional[TreeNode]) -> int:
        if not root:
            return 0
        return 1 + max(self.maxDepth(root.left), self.maxDepth(root.right))

复杂度:时间 O(n),空间 O(h)。

复杂度分析

  • 解法:递归求高度:时间 O(n),主循环中每个元素或节点只进入核心结构常数次。 空间 O(h),主要来自树高相关的递归调用栈或显式栈。
226翻转二叉树 Invert Binary Tree简单

题目规格

官方题意(改写):翻转二叉树,使每个节点的左右子树交换。 完整原题、样例和英文表述以本题官方链接为准。

输入:判题入口是 Solution.invertTree(...),输入参数为 root(Optional[TreeNode])。

输出:返回 Optional[TreeNode];返回值含义是:翻转二叉树,使每个节点的左右子树交换。

约束要点
  • 树中节点数目范围在 [0, 100] 内
  • -100 <= Node.val <= 100

题目分析

对每个节点交换 left/right,然后递归处理新的左右子树。前序或后序都可以。

  • 建模:翻转二叉树 可以先理解为“翻转二叉树,使每个节点的左右子树交换。”。先确定答案依赖的是下标、值、路径、窗口、集合还是 DP 状态,再决定数据结构。
  • 不变量:二叉树题先定义递归函数返回什么:高度、是否合法、路径贡献、构造出的子树,还是目标节点;返回值要能服务父节点。
  • 边界:重点检查空树、单节点、左右子树缺失、重复值限制和递归返回 None 的处理。
  • 正确性:局部交换对每个节点独立成立。 只要每次更新后不变量仍成立,最终保留下来的状态就对应题目要求的结果。

解法:递归交换左右子树

局部交换对每个节点独立成立。

class Solution:
    def invertTree(self, root: Optional[TreeNode]) -> Optional[TreeNode]:
        if not root:
            return None
        root.left, root.right = root.right, root.left
        self.invertTree(root.left)
        self.invertTree(root.right)
        return root

复杂度:时间 O(n),空间 O(h)。

复杂度分析

  • 解法:递归交换左右子树:时间 O(n),主循环中每个元素或节点只进入核心结构常数次。 空间 O(h),主要来自树高相关的递归调用栈或显式栈。
101对称二叉树 Symmetric Tree简单

题目规格

官方题意(改写):判断二叉树是否关于中心轴镜像对称。 完整原题、样例和英文表述以本题官方链接为准。

输入:判题入口是 Solution.isSymmetric(...),输入参数为 root(Optional[TreeNode])。

输出:返回 bool;返回值含义是:判断二叉树是否关于中心轴镜像对称。

约束要点
  • 树中节点数目在范围 [1, 1000] 内
  • -100 <= Node.val <= 100

题目分析

对称不是左右子树相等,而是左子树的左节点对应右子树的右节点,左子树的右节点对应右子树的左节点。

  • 建模:对称二叉树 可以先理解为“判断二叉树是否关于中心轴镜像对称。”。先确定答案依赖的是下标、值、路径、窗口、集合还是 DP 状态,再决定数据结构。
  • 不变量:二叉树题先定义递归函数返回什么:高度、是否合法、路径贡献、构造出的子树,还是目标节点;返回值要能服务父节点。
  • 边界:重点检查空树、单节点、左右子树缺失、重复值限制和递归返回 None 的处理。
  • 正确性:比较两个节点是否互为镜像。 只要每次更新后不变量仍成立,最终保留下来的状态就对应题目要求的结果。

解法:镜像递归

比较两个节点是否互为镜像。

class Solution:
    def isSymmetric(self, root: Optional[TreeNode]) -> bool:
        def mirror(a, b):
            if not a or not b:
                return a is b
            return a.val == b.val and mirror(a.left, b.right) and mirror(a.right, b.left)
        return mirror(root.left, root.right) if root else True

复杂度:时间 O(n),空间 O(h)。

复杂度分析

  • 解法:镜像递归:时间 O(n),主循环中每个元素或节点只进入核心结构常数次。 空间 O(h),主要来自树高相关的递归调用栈或显式栈。
543二叉树的直径 Diameter of Binary Tree简单

题目规格

官方题意(改写):求二叉树任意两个节点之间最长路径的边数。 完整原题、样例和英文表述以本题官方链接为准。

输入:判题入口是 Solution.diameterOfBinaryTree(...),输入参数为 root(Optional[TreeNode])。

输出:返回 int;返回值含义是:求二叉树任意两个节点之间最长路径的边数。

约束要点
  • 树中节点数目在范围 [1, 10^4] 内
  • -100 <= Node.val <= 100

题目分析

经过某个节点的最长路径等于左子树高度加右子树高度。后序遍历返回高度,同时更新全局最大直径。

  • 建模:二叉树的直径 可以先理解为“求二叉树任意两个节点之间最长路径的边数。”。先确定答案依赖的是下标、值、路径、窗口、集合还是 DP 状态,再决定数据结构。
  • 不变量:二叉树题先定义递归函数返回什么:高度、是否合法、路径贡献、构造出的子树,还是目标节点;返回值要能服务父节点。
  • 边界:重点检查空树、单节点、左右子树缺失、重复值限制和递归返回 None 的处理。
  • 正确性:每个节点作为路径最高拐点被计算一次。 只要每次更新后不变量仍成立,最终保留下来的状态就对应题目要求的结果。

解法:后序高度更新直径

每个节点作为路径最高拐点被计算一次。

class Solution:
    def diameterOfBinaryTree(self, root: Optional[TreeNode]) -> int:
        best = 0
        def depth(node):
            nonlocal best
            if not node:
                return 0
            left = depth(node.left)
            right = depth(node.right)
            best = max(best, left + right)
            return 1 + max(left, right)
        depth(root)
        return best

复杂度:时间 O(n),空间 O(h)。

复杂度分析

  • 解法:后序高度更新直径:时间 O(n),主循环中每个元素或节点只进入核心结构常数次。 空间 O(h),主要来自树高相关的递归调用栈或显式栈。
102二叉树的层序遍历 Binary Tree Level Order Traversal中等

题目规格

官方题意(改写):按层从上到下返回二叉树节点值。 完整原题、样例和英文表述以本题官方链接为准。

输入:判题入口是 Solution.levelOrder(...),输入参数为 root(Optional[TreeNode])。

输出:返回 List[List[int]];返回值含义是:按层从上到下返回二叉树节点值。

约束要点
  • 树中节点数目在范围 [0, 2000] 内
  • -1000 <= Node.val <= 1000

题目分析

广度优先搜索天然按层访问。每轮固定当前队列长度,正好是一层的节点数。

  • 建模:二叉树的层序遍历 可以先理解为“按层从上到下返回二叉树节点值。”。先确定答案依赖的是下标、值、路径、窗口、集合还是 DP 状态,再决定数据结构。
  • 不变量:二叉树题先定义递归函数返回什么:高度、是否合法、路径贡献、构造出的子树,还是目标节点;返回值要能服务父节点。
  • 边界:重点检查空树、单节点、左右子树缺失、重复值限制和递归返回 None 的处理。
  • 正确性:用 level_size 把队列中的当前层与下一层分开。 只要每次更新后不变量仍成立,最终保留下来的状态就对应题目要求的结果。

解法:BFS 按层遍历

用 level_size 把队列中的当前层与下一层分开。

from collections import deque

class Solution:
    def levelOrder(self, root: Optional[TreeNode]) -> List[List[int]]:
        if not root:
            return []
        q = deque([root])
        ans = []
        while q:
            level = []
            for _ in range(len(q)):
                node = q.popleft()
                level.append(node.val)
                if node.left:
                    q.append(node.left)
                if node.right:
                    q.append(node.right)
            ans.append(level)
        return ans

复杂度:时间 O(n),空间 O(w)。

复杂度分析

  • 解法:BFS 按层遍历:时间 O(n),主循环中每个元素或节点只进入核心结构常数次。 空间 O(w),额外空间来自代码中显式维护的辅助结构;返回结果本身通常不计入额外空间。
108将有序数组转换为二叉搜索树 Convert Sorted Array to Binary Search Tree简单

题目规格

官方题意(改写):把升序数组转换为高度平衡二叉搜索树。 完整原题、样例和英文表述以本题官方链接为准。

输入:判题入口是 Solution.sortedArrayToBST(...),输入参数为 nums(List[int])。

输出:返回 Optional[TreeNode];返回值含义是:把升序数组转换为高度平衡二叉搜索树。

约束要点
  • 1 <= nums.length <= 10^4
  • -10^4 <= nums[i] <= 10^4
  • nums 按 严格递增 顺序排列

题目分析

平衡要求左右子树节点数尽量接近,因此每次取中点作为根,左右区间递归构造。

  • 建模:将有序数组转换为二叉搜索树 可以先理解为“把升序数组转换为高度平衡二叉搜索树。”。先确定答案依赖的是下标、值、路径、窗口、集合还是 DP 状态,再决定数据结构。
  • 不变量:二叉树题先定义递归函数返回什么:高度、是否合法、路径贡献、构造出的子树,还是目标节点;返回值要能服务父节点。
  • 边界:重点检查空树、单节点、左右子树缺失、重复值限制和递归返回 None 的处理。
  • 正确性:有序数组中点天然满足 BST 的左右大小关系。 只要每次更新后不变量仍成立,最终保留下来的状态就对应题目要求的结果。

解法:中点递归建树

有序数组中点天然满足 BST 的左右大小关系。

class Solution:
    def sortedArrayToBST(self, nums: List[int]) -> Optional[TreeNode]:
        def build(l, r):
            if l > r:
                return None
            mid = (l + r) // 2
            root = TreeNode(nums[mid])
            root.left = build(l, mid - 1)
            root.right = build(mid + 1, r)
            return root
        return build(0, len(nums) - 1)

复杂度:时间 O(n),空间 O(log n)。

复杂度分析

  • 解法:中点递归建树:时间 O(n),主循环中每个元素或节点只进入核心结构常数次。 空间 O(log n),主要来自递归深度或平衡分治栈。
98验证二叉搜索树 Validate Binary Search Tree中等

题目规格

官方题意(改写):判断一棵二叉树是否是合法二叉搜索树。 完整原题、样例和英文表述以本题官方链接为准。

输入:判题入口是 Solution.isValidBST(...),输入参数为 root(Optional[TreeNode])。

输出:返回 bool;返回值含义是:判断一棵二叉树是否是合法二叉搜索树。

约束要点
  • 树中节点数目范围在 [1, 10^4] 内
  • -2^31 <= Node.val <= 2^31 - 1

题目分析

不能只比较父子节点;每个节点都必须落在祖先限定出的开区间内。递归传递 lower/upper 边界即可。

  • 建模:验证二叉搜索树 可以先理解为“判断一棵二叉树是否是合法二叉搜索树。”。先确定答案依赖的是下标、值、路径、窗口、集合还是 DP 状态,再决定数据结构。
  • 不变量:二叉树题先定义递归函数返回什么:高度、是否合法、路径贡献、构造出的子树,还是目标节点;返回值要能服务父节点。
  • 边界:重点检查空树、单节点、左右子树缺失、重复值限制和递归返回 None 的处理。
  • 正确性:左子树上界是当前值,右子树下界是当前值。 只要每次更新后不变量仍成立,最终保留下来的状态就对应题目要求的结果。

解法:上下界递归

左子树上界是当前值,右子树下界是当前值。

class Solution:
    def isValidBST(self, root: Optional[TreeNode]) -> bool:
        def dfs(node, low, high):
            if not node:
                return True
            if not (low < node.val < high):
                return False
            return dfs(node.left, low, node.val) and dfs(node.right, node.val, high)
        return dfs(root, float('-inf'), float('inf'))

复杂度:时间 O(n),空间 O(h)。

复杂度分析

  • 解法:上下界递归:时间 O(n),主循环中每个元素或节点只进入核心结构常数次。 空间 O(h),主要来自树高相关的递归调用栈或显式栈。
230二叉搜索树中第 K 小的元素 Kth Smallest Element in a BST中等

题目规格

官方题意(改写):返回二叉搜索树中第 k 小的节点值。 完整原题、样例和英文表述以本题官方链接为准。

输入:判题入口是 Solution.kthSmallest(...),输入参数为 root(Optional[TreeNode])、k(int)。

输出:返回 int;返回值含义是:返回二叉搜索树中第 k 小的节点值。

约束要点
  • 树中的节点数为 n
  • 1 <= k <= n <= 10^4
  • 0 <= Node.val <= 10^4

题目分析

BST 的中序遍历是升序序列。迭代中序遍历时,第 k 次弹出的节点就是答案。

  • 建模:二叉搜索树中第 K 小的元素 可以先理解为“返回二叉搜索树中第 k 小的节点值。”。先确定答案依赖的是下标、值、路径、窗口、集合还是 DP 状态,再决定数据结构。
  • 不变量:二叉树题先定义递归函数返回什么:高度、是否合法、路径贡献、构造出的子树,还是目标节点;返回值要能服务父节点。
  • 边界:重点检查空树、单节点、左右子树缺失、重复值限制和递归返回 None 的处理。
  • 正确性:不必遍历完整棵树,数到第 k 个即可返回。 只要每次更新后不变量仍成立,最终保留下来的状态就对应题目要求的结果。

解法:中序遍历提前停止

不必遍历完整棵树,数到第 k 个即可返回。

class Solution:
    def kthSmallest(self, root: Optional[TreeNode], k: int) -> int:
        stack = []
        cur = root
        while cur or stack:
            while cur:
                stack.append(cur)
                cur = cur.left
            cur = stack.pop()
            k -= 1
            if k == 0:
                return cur.val
            cur = cur.right

复杂度:时间 O(h+k),空间 O(h)。

复杂度分析

  • 解法:中序遍历提前停止:时间 O(h+k),二叉树题先定义递归函数返回什么:高度、是否合法、路径贡献、构造出的子树,还是目标节点;返回值要能服务父节点。 空间 O(h),主要来自树高相关的递归调用栈或显式栈。
199二叉树的右视图 Binary Tree Right Side View中等

题目规格

官方题意(改写):返回从右侧观察二叉树时每一层能看到的节点值。 完整原题、样例和英文表述以本题官方链接为准。

输入:判题入口是 Solution.rightSideView(...),输入参数为 root(Optional[TreeNode])。

输出:返回 List[int];返回值含义是:返回从右侧观察二叉树时每一层能看到的节点值。

约束要点
  • 二叉树的节点个数的范围是 [0,100]
  • -100 <= Node.val <= 100

题目分析

层序遍历时,每层最后一个节点就是右视图节点;也可以 DFS 优先访问右子树并记录每层第一个节点。

  • 建模:二叉树的右视图 可以先理解为“返回从右侧观察二叉树时每一层能看到的节点值。”。先确定答案依赖的是下标、值、路径、窗口、集合还是 DP 状态,再决定数据结构。
  • 不变量:二叉树题先定义递归函数返回什么:高度、是否合法、路径贡献、构造出的子树,还是目标节点;返回值要能服务父节点。
  • 边界:重点检查空树、单节点、左右子树缺失、重复值限制和递归返回 None 的处理。
  • 正确性:每层循环结束前最后弹出的节点代表该层最右侧。 只要每次更新后不变量仍成立,最终保留下来的状态就对应题目要求的结果。

解法:BFS 每层最后一个

每层循环结束前最后弹出的节点代表该层最右侧。

from collections import deque

class Solution:
    def rightSideView(self, root: Optional[TreeNode]) -> List[int]:
        if not root:
            return []
        q = deque([root])
        ans = []
        while q:
            size = len(q)
            for i in range(size):
                node = q.popleft()
                if node.left:
                    q.append(node.left)
                if node.right:
                    q.append(node.right)
                if i == size - 1:
                    ans.append(node.val)
        return ans

复杂度:时间 O(n),空间 O(w)。

复杂度分析

  • 解法:BFS 每层最后一个:时间 O(n),主循环中每个元素或节点只进入核心结构常数次。 空间 O(w),额外空间来自代码中显式维护的辅助结构;返回结果本身通常不计入额外空间。
114二叉树展开为链表 Flatten Binary Tree to Linked List中等

题目规格

官方题意(改写):把二叉树原地展开为先序遍历顺序的单链表,使用 right 指针连接。 完整原题、样例和英文表述以本题官方链接为准。

输入:判题入口是 Solution.flatten(...),输入参数为 root(Optional[TreeNode])。

输出:函数原地修改输入对象,不需要返回新对象;修改后的数据应满足:把二叉树原地展开为先序遍历顺序的单链表,使用 right 指针连接。

约束要点
  • 树中结点数在范围 [0, 2000] 内
  • -100 <= Node.val <= 100

题目分析

后序递归先展开左右子树,再把左链插到右链前面。也可以用前序遍历栈模拟。

  • 建模:二叉树展开为链表 可以先理解为“把二叉树原地展开为先序遍历顺序的单链表,使用 right 指针连接。”。先确定答案依赖的是下标、值、路径、窗口、集合还是 DP 状态,再决定数据结构。
  • 不变量:二叉树题先定义递归函数返回什么:高度、是否合法、路径贡献、构造出的子树,还是目标节点;返回值要能服务父节点。
  • 边界:重点检查空树、单节点、左右子树缺失、重复值限制和递归返回 None 的处理。
  • 正确性:返回子树展开后的尾节点,便于父节点连接。 只要每次更新后不变量仍成立,最终保留下来的状态就对应题目要求的结果。

解法:后序原地拼接

返回子树展开后的尾节点,便于父节点连接。

class Solution:
    def flatten(self, root: Optional[TreeNode]) -> None:
        def dfs(node):
            if not node:
                return None
            left_tail = dfs(node.left)
            right_tail = dfs(node.right)
            if left_tail:
                left_tail.right = node.right
                node.right = node.left
                node.left = None
            return right_tail or left_tail or node
        dfs(root)

复杂度:时间 O(n),空间 O(h)。

复杂度分析

  • 解法:后序原地拼接:时间 O(n),主循环中每个元素或节点只进入核心结构常数次。 空间 O(h),主要来自树高相关的递归调用栈或显式栈。
105从前序与中序遍历序列构造二叉树 Construct Binary Tree from Preorder and Inorder Traversal中等

题目规格

官方题意(改写):根据前序遍历和中序遍历构造二叉树。 完整原题、样例和英文表述以本题官方链接为准。

输入:判题入口是 Solution.buildTree(...),输入参数为 preorder(List[int])、inorder(List[int])。

输出:返回 Optional[TreeNode];返回值含义是:根据前序遍历和中序遍历构造二叉树。

约束要点
  • 1 <= preorder.length <= 3000
  • inorder.length == preorder.length
  • -3000 <= preorder[i], inorder[i] <= 3000
  • preorder 和 inorder 均 无重复 元素
  • inorder 均出现在 preorder
  • preorder 保证 为二叉树的前序遍历序列

题目分析

前序第一个值是根。根在中序中的位置把左右子树分开。用哈希表记录中序位置,递归时只传区间,避免切片。

  • 建模:从前序与中序遍历序列构造二叉树 可以先理解为“根据前序遍历和中序遍历构造二叉树。”。先确定答案依赖的是下标、值、路径、窗口、集合还是 DP 状态,再决定数据结构。
  • 不变量:二叉树题先定义递归函数返回什么:高度、是否合法、路径贡献、构造出的子树,还是目标节点;返回值要能服务父节点。
  • 边界:重点检查空树、单节点、左右子树缺失、重复值限制和递归返回 None 的处理。
  • 正确性:pre_idx 按前序顺序消费根节点。 只要每次更新后不变量仍成立,最终保留下来的状态就对应题目要求的结果。

解法:前序根 + 中序分割

pre_idx 按前序顺序消费根节点。

class Solution:
    def buildTree(self, preorder: List[int], inorder: List[int]) -> Optional[TreeNode]:
        pos = {v: i for i, v in enumerate(inorder)}
        pre_idx = 0

        def build(l, r):
            nonlocal pre_idx
            if l > r:
                return None
            root_val = preorder[pre_idx]
            pre_idx += 1
            root = TreeNode(root_val)
            mid = pos[root_val]
            root.left = build(l, mid - 1)
            root.right = build(mid + 1, r)
            return root

        return build(0, len(inorder) - 1)

复杂度:时间 O(n),空间 O(n)。

复杂度分析

  • 解法:前序根 + 中序分割:时间 O(n),主循环中每个元素或节点只进入核心结构常数次。 空间 O(n),主要来自辅助数组、队列、哈希集合或递归栈。
437路径总和 III Path Sum III中等

题目规格

官方题意(改写):统计二叉树中路径和等于 targetSum 的路径数量;路径必须向下,但不一定从根开始。 完整原题、样例和英文表述以本题官方链接为准。

输入:判题入口是 Solution.pathSum(...),输入参数为 root(Optional[TreeNode])、targetSum(int)。

输出:返回 int;返回值含义是:统计二叉树中路径和等于 targetSum 的路径数量;路径必须向下,但不一定从根开始。

约束要点
  • 二叉树的节点个数的范围是 [0,1000]
  • -10^9
  • -1000

题目分析

从根到当前节点的前缀和为 cur。若之前存在前缀和 cur-target,则中间这段路径和为 target。DFS 进入节点时加入当前前缀,回溯离开时撤销。

  • 建模:路径总和 III 可以先理解为“统计二叉树中路径和等于 targetSum 的路径数量;路径必须向下,但不一定从根开始。”。先确定答案依赖的是下标、值、路径、窗口、集合还是 DP 状态,再决定数据结构。
  • 不变量:二叉树题先定义递归函数返回什么:高度、是否合法、路径贡献、构造出的子树,还是目标节点;返回值要能服务父节点。
  • 边界:重点检查空树、单节点、左右子树缺失、重复值限制和递归返回 None 的处理。
  • 正确性:把任意向下路径和转化为两段根路径前缀和之差。 只要每次更新后不变量仍成立,最终保留下来的状态就对应题目要求的结果。

解法:树上前缀和 + 回溯

把任意向下路径和转化为两段根路径前缀和之差。

from collections import defaultdict

class Solution:
    def pathSum(self, root: Optional[TreeNode], targetSum: int) -> int:
        count = defaultdict(int)
        count[0] = 1
        ans = 0

        def dfs(node, cur):
            nonlocal ans
            if not node:
                return
            cur += node.val
            ans += count[cur - targetSum]
            count[cur] += 1
            dfs(node.left, cur)
            dfs(node.right, cur)
            count[cur] -= 1

        dfs(root, 0)
        return ans

复杂度:时间 O(n),空间 O(h)。

复杂度分析

  • 解法:树上前缀和 + 回溯:时间 O(n),主循环中每个元素或节点只进入核心结构常数次。 空间 O(h),主要来自树高相关的递归调用栈或显式栈。
236二叉树的最近公共祖先 Lowest Common Ancestor of a Binary Tree中等

题目规格

官方题意(改写):在普通二叉树中寻找两个给定节点的最近公共祖先。 完整原题、样例和英文表述以本题官方链接为准。

输入:判题入口是 Solution.lowestCommonAncestor(...),输入参数为 root('TreeNode')、p('TreeNode')、q('TreeNode')。

输出:返回 'TreeNode';返回值含义是:在普通二叉树中寻找两个给定节点的最近公共祖先。

约束要点
  • 树中节点数目在范围 [2, 10^5] 内
  • -10^9
  • 所有 Node.val 互不相同
  • p != q
  • p 和 q 均存在于给定的二叉树中

题目分析

递归搜索左右子树。如果当前节点是 p/q,返回当前节点;如果左右子树都找到目标,当前节点就是 LCA;否则返回非空的一侧。

  • 建模:二叉树的最近公共祖先 可以先理解为“在普通二叉树中寻找两个给定节点的最近公共祖先。”。先确定答案依赖的是下标、值、路径、窗口、集合还是 DP 状态,再决定数据结构。
  • 不变量:二叉树题先定义递归函数返回什么:高度、是否合法、路径贡献、构造出的子树,还是目标节点;返回值要能服务父节点。
  • 边界:重点检查空树、单节点、左右子树缺失、重复值限制和递归返回 None 的处理。
  • 正确性:左右两侧同时命中时,当前节点是最深汇合点。 只要每次更新后不变量仍成立,最终保留下来的状态就对应题目要求的结果。

解法:后序递归返回命中节点

左右两侧同时命中时,当前节点是最深汇合点。

class Solution:
    def lowestCommonAncestor(self, root: 'TreeNode', p: 'TreeNode', q: 'TreeNode') -> 'TreeNode':
        if not root or root is p or root is q:
            return root
        left = self.lowestCommonAncestor(root.left, p, q)
        right = self.lowestCommonAncestor(root.right, p, q)
        if left and right:
            return root
        return left or right

复杂度:时间 O(n),空间 O(h)。

复杂度分析

  • 解法:后序递归返回命中节点:时间 O(n),主循环中每个元素或节点只进入核心结构常数次。 空间 O(h),主要来自树高相关的递归调用栈或显式栈。
124二叉树中的最大路径和 Binary Tree Maximum Path Sum困难

题目规格

官方题意(改写):求二叉树中任意非空路径的最大节点值之和,路径不必经过根。 完整原题、样例和英文表述以本题官方链接为准。

输入:判题入口是 Solution.maxPathSum(...),输入参数为 root(Optional[TreeNode])。

输出:返回 int;返回值含义是:求二叉树中任意非空路径的最大节点值之和,路径不必经过根。

约束要点
  • 树中节点数目范围是 [1, 3 * 10^4]
  • -1000 <= Node.val <= 1000

题目分析

对父节点可贡献的只能是一条向下单链,因此返回 max(0, node.val + max(left_gain, right_gain))。但以当前节点为拐点的答案可同时使用左右贡献。

  • 建模:二叉树中的最大路径和 可以先理解为“求二叉树中任意非空路径的最大节点值之和,路径不必经过根。”。先确定答案依赖的是下标、值、路径、窗口、集合还是 DP 状态,再决定数据结构。
  • 不变量:二叉树题先定义递归函数返回什么:高度、是否合法、路径贡献、构造出的子树,还是目标节点;返回值要能服务父节点。
  • 边界:重点检查空树、单节点、左右子树缺失、重复值限制和递归返回 None 的处理。
  • 正确性:返回给父节点的是单边最大增益,更新答案时可用双边。 只要每次更新后不变量仍成立,最终保留下来的状态就对应题目要求的结果。

解法:后序贡献值

返回给父节点的是单边最大增益,更新答案时可用双边。

class Solution:
    def maxPathSum(self, root: Optional[TreeNode]) -> int:
        best = float('-inf')
        def gain(node):
            nonlocal best
            if not node:
                return 0
            left = max(gain(node.left), 0)
            right = max(gain(node.right), 0)
            best = max(best, node.val + left + right)
            return node.val + max(left, right)
        gain(root)
        return best

复杂度:时间 O(n),空间 O(h)。

复杂度分析

  • 解法:后序贡献值:时间 O(n),主循环中每个元素或节点只进入核心结构常数次。 空间 O(h),主要来自树高相关的递归调用栈或显式栈。

图论

本组 4 题,围绕 图论 的核心模式展开。先识别不变量,再选择哈希、指针、递归、队列、堆或 DP 状态。

200岛屿数量 Number of Islands中等

题目规格

官方题意(改写):给定由 0/1 组成的网格,统计岛屿数量;上下左右相邻的陆地属于同一岛。 完整原题、样例和英文表述以本题官方链接为准。

输入:判题入口是 Solution.numIslands(...),输入参数为 grid(List[List[str]])。

输出:返回 int;返回值含义是:给定由 0/1 组成的网格,统计岛屿数量;上下左右相邻的陆地属于同一岛。

约束要点
  • m == grid.length
  • n == grid[i].length
  • 1 <= m, n <= 300
  • grid[i][j] 的值为 '0' 或 '1'

题目分析

扫描网格,遇到未访问陆地就启动 DFS/BFS,把整座岛标记为水或 visited,岛屿数加一。

  • 建模:岛屿数量 可以先理解为“给定由 0/1 组成的网格,统计岛屿数量;上下左右相邻的陆地属于同一岛。”。先确定答案依赖的是下标、值、路径、窗口、集合还是 DP 状态,再决定数据结构。
  • 不变量:图题先把状态建成节点和边,再选择 DFS/BFS/拓扑排序;visited、入度或队列层数就是核心不变量。
  • 边界:重点检查孤立节点、重复边、无法到达的节点、层数是否多加一分钟或一步。
  • 正确性:一次搜索消去一个连通块。 只要每次更新后不变量仍成立,最终保留下来的状态就对应题目要求的结果。

解法:DFS flood fill

一次搜索消去一个连通块。

class Solution:
    def numIslands(self, grid: List[List[str]]) -> int:
        m, n = len(grid), len(grid[0])
        def dfs(i, j):
            if i < 0 or i >= m or j < 0 or j >= n or grid[i][j] != '1':
                return
            grid[i][j] = '0'
            dfs(i + 1, j); dfs(i - 1, j)
            dfs(i, j + 1); dfs(i, j - 1)

        ans = 0
        for i in range(m):
            for j in range(n):
                if grid[i][j] == '1':
                    ans += 1
                    dfs(i, j)
        return ans

复杂度:时间 O(mn),空间 O(mn) 最坏递归栈。

复杂度分析

  • 解法:DFS flood fill:时间 O(mn),矩阵或二维 DP 中每个格子会被访问或更新常数次。 空间 O(mn) 最坏递归栈,主要来自 visited 标记、二维 DP 表或递归搜索的最坏栈空间。
994腐烂的橘子 Rotting Oranges中等

题目规格

官方题意(改写):网格中腐烂橘子每分钟感染相邻新鲜橘子,求腐烂全部新鲜橘子的最短时间。 完整原题、样例和英文表述以本题官方链接为准。

输入:判题入口是 Solution.orangesRotting(...),输入参数为 grid(List[List[int]])。

输出:返回 int;返回值含义是:网格中腐烂橘子每分钟感染相邻新鲜橘子,求腐烂全部新鲜橘子的最短时间。

约束要点
  • m == grid.length
  • n == grid[i].length
  • 1 <= m, n <= 10
  • grid[i][j] 仅为 0 、 1 或 2

题目分析

多源 BFS:所有初始腐烂橘子同时入队,每层代表一分钟。记录新鲜橘子数量,BFS 后若仍有新鲜则返回 -1。

  • 建模:腐烂的橘子 可以先理解为“网格中腐烂橘子每分钟感染相邻新鲜橘子,求腐烂全部新鲜橘子的最短时间。”。先确定答案依赖的是下标、值、路径、窗口、集合还是 DP 状态,再决定数据结构。
  • 不变量:图题先把状态建成节点和边,再选择 DFS/BFS/拓扑排序;visited、入度或队列层数就是核心不变量。
  • 边界:重点检查孤立节点、重复边、无法到达的节点、层数是否多加一分钟或一步。
  • 正确性:一开始所有腐烂橘子都是同一时间层的源点。 只要每次更新后不变量仍成立,最终保留下来的状态就对应题目要求的结果。

解法:多源 BFS

一开始所有腐烂橘子都是同一时间层的源点。

from collections import deque

class Solution:
    def orangesRotting(self, grid: List[List[int]]) -> int:
        m, n = len(grid), len(grid[0])
        q = deque()
        fresh = 0
        for i in range(m):
            for j in range(n):
                if grid[i][j] == 2:
                    q.append((i, j))
                elif grid[i][j] == 1:
                    fresh += 1
        minutes = 0
        dirs = [(1,0), (-1,0), (0,1), (0,-1)]
        while q and fresh:
            for _ in range(len(q)):
                i, j = q.popleft()
                for di, dj in dirs:
                    ni, nj = i + di, j + dj
                    if 0 <= ni < m and 0 <= nj < n and grid[ni][nj] == 1:
                        grid[ni][nj] = 2
                        fresh -= 1
                        q.append((ni, nj))
            minutes += 1
        return minutes if fresh == 0 else -1

复杂度:时间 O(mn),空间 O(mn)。

复杂度分析

  • 解法:多源 BFS:时间 O(mn),矩阵或二维 DP 中每个格子会被访问或更新常数次。 空间 O(mn),主要来自 visited 标记、二维 DP 表或递归搜索的最坏栈空间。
207课程表 Course Schedule中等

题目规格

官方题意(改写):给定课程先修关系,判断是否能完成所有课程。 完整原题、样例和英文表述以本题官方链接为准。

输入:判题入口是 Solution.canFinish(...),输入参数为 numCourses(int)、prerequisites(List[List[int]])。

输出:返回 bool;返回值含义是:给定课程先修关系,判断是否能完成所有课程。

约束要点
  • 1 <= numCourses <= 2000
  • 0 <= prerequisites.length <= 5000
  • prerequisites[i].length == 2
  • 0 <= a i , b i < numCourses
  • prerequisites[i] 中的所有课程对 互不相同

题目分析

这是有向图判环。用入度表做拓扑排序,能弹出所有节点说明无环;若剩余节点无法入队,则存在循环依赖。

  • 建模:课程表 可以先理解为“给定课程先修关系,判断是否能完成所有课程。”。先确定答案依赖的是下标、值、路径、窗口、集合还是 DP 状态,再决定数据结构。
  • 不变量:图题先把状态建成节点和边,再选择 DFS/BFS/拓扑排序;visited、入度或队列层数就是核心不变量。
  • 边界:重点检查孤立节点、重复边、无法到达的节点、层数是否多加一分钟或一步。
  • 正确性:每学完一门入度为 0 的课,就减少它指向课程的入度。 只要每次更新后不变量仍成立,最终保留下来的状态就对应题目要求的结果。

解法:拓扑排序 Kahn 算法

每学完一门入度为 0 的课,就减少它指向课程的入度。

from collections import deque

class Solution:
    def canFinish(self, numCourses: int, prerequisites: List[List[int]]) -> bool:
        graph = [[] for _ in range(numCourses)]
        indeg = [0] * numCourses
        for a, b in prerequisites:
            graph[b].append(a)
            indeg[a] += 1
        q = deque(i for i in range(numCourses) if indeg[i] == 0)
        learned = 0
        while q:
            cur = q.popleft()
            learned += 1
            for nxt in graph[cur]:
                indeg[nxt] -= 1
                if indeg[nxt] == 0:
                    q.append(nxt)
        return learned == numCourses

复杂度:时间 O(V+E),空间 O(V+E)。

复杂度分析

  • 解法:拓扑排序 Kahn 算法:时间 O(V+E),每个节点和每条边只会在建图或遍历时处理常数次。 空间 O(V+E),额外空间来自代码中显式维护的辅助结构;返回结果本身通常不计入额外空间。
208实现 Trie (前缀树) Implement Trie (Prefix Tree)中等

题目规格

官方题意(改写):实现前缀树,支持插入单词、查询完整单词和查询前缀。 完整原题、样例和英文表述以本题官方链接为准。

输入:设计类 TrieNode,判题器会按照官方操作序列调用构造器和公开方法。

输出:每次查询方法返回题目要求的布尔值、整数或浮点数;更新类方法只改变对象内部状态。核心目标是:实现前缀树,支持插入单词、查询完整单词和查询前缀。

约束要点
  • 1 <= word.length, prefix.length <= 2000
  • word 和 prefix 仅由小写英文字母组成
  • insert 、 search 和 startsWith 调用次数 总计 不超过 3 * 10^4 次

题目分析

Trie 的每条边代表一个字符。从根沿字符逐步创建或查找节点,终止标记用于区分完整单词和仅作为前缀出现的路径。

  • 建模:实现 Trie (前缀树) 可以先理解为“实现前缀树,支持插入单词、查询完整单词和查询前缀。”。先确定答案依赖的是下标、值、路径、窗口、集合还是 DP 状态,再决定数据结构。
  • 不变量:图题先把状态建成节点和边,再选择 DFS/BFS/拓扑排序;visited、入度或队列层数就是核心不变量。
  • 边界:重点检查孤立节点、重复边、无法到达的节点、层数是否多加一分钟或一步。
  • 正确性:children 存后续字符,end 标记单词结束。 只要每次更新后不变量仍成立,最终保留下来的状态就对应题目要求的结果。

解法:嵌套字典节点

children 存后续字符,end 标记单词结束。

class TrieNode:
    def __init__(self):
        self.children = {}
        self.end = False

class Trie:
    def __init__(self):
        self.root = TrieNode()

    def insert(self, word: str) -> None:
        node = self.root
        for ch in word:
            node = node.children.setdefault(ch, TrieNode())
        node.end = True

    def search(self, word: str) -> bool:
        node = self._find(word)
        return bool(node and node.end)

    def startsWith(self, prefix: str) -> bool:
        return self._find(prefix) is not None

    def _find(self, s: str):
        node = self.root
        for ch in s:
            if ch not in node.children:
                return None
            node = node.children[ch]
        return node

复杂度:时间 每次操作 O(L),空间 O(总字符数)。

复杂度分析

  • 解法:嵌套字典节点:时间 每次操作 O(L),图题先把状态建成节点和边,再选择 DFS/BFS/拓扑排序;visited、入度或队列层数就是核心不变量。 空间 O(总字符数),额外空间来自代码中显式维护的辅助结构;返回结果本身通常不计入额外空间。

回溯

本组 8 题,围绕 回溯 的核心模式展开。先识别不变量,再选择哈希、指针、递归、队列、堆或 DP 状态。

46全排列 Permutations中等

题目规格

官方题意(改写):返回数组中所有元素的全排列。 完整原题、样例和英文表述以本题官方链接为准。

输入:判题入口是 Solution.permute(...),输入参数为 nums(List[int])。

输出:返回 List[List[int]];返回值含义是:返回数组中所有元素的全排列。

约束要点
  • 1 <= nums.length <= 6
  • -10 <= nums[i] <= 10
  • nums 中的所有整数 互不相同

题目分析

回溯维护当前路径 path 和 used 标记。路径长度达到 n 时得到一个排列;递归退出时撤销选择。

  • 建模:全排列 可以先理解为“返回数组中所有元素的全排列。”。先确定答案依赖的是下标、值、路径、窗口、集合还是 DP 状态,再决定数据结构。
  • 不变量:回溯题的状态由 path、start/index 和可用集合组成;每次选择后必须撤销,剪枝只能基于确定不会产生合法解的条件。
  • 边界:重点检查撤销现场、重复解去重、剩余目标为 0、路径长度达到上限。
  • 正确性:每层选择一个还没放入 path 的数。 只要每次更新后不变量仍成立,最终保留下来的状态就对应题目要求的结果。

解法:回溯选择未使用元素

每层选择一个还没放入 path 的数。

class Solution:
    def permute(self, nums: List[int]) -> List[List[int]]:
        ans, path = [], []
        used = [False] * len(nums)
        def dfs():
            if len(path) == len(nums):
                ans.append(path[:])
                return
            for i, x in enumerate(nums):
                if used[i]:
                    continue
                used[i] = True
                path.append(x)
                dfs()
                path.pop()
                used[i] = False
        dfs()
        return ans

复杂度:时间 O(n * n!),空间 O(n)。

复杂度分析

  • 解法:回溯选择未使用元素:时间 O(n * n!),递归搜索树会枚举合法选择或组合,复杂度由结果规模和每条路径的构造成本决定。 空间 O(n),主要来自辅助数组、队列、哈希集合或递归栈。
78子集 Subsets中等

题目规格

官方题意(改写):返回数组所有可能子集。 完整原题、样例和英文表述以本题官方链接为准。

输入:判题入口是 Solution.subsets(...),输入参数为 nums(List[int])。

输出:返回 List[List[int]];返回值含义是:返回数组所有可能子集。

约束要点
  • 1 <= nums.length <= 10
  • -10 <= nums[i] <= 10
  • nums 中的所有元素 互不相同

题目分析

子集问题每个元素有选或不选两种状态。可以在每个递归节点记录当前 path,再继续选择后面的元素。

  • 建模:子集 可以先理解为“返回数组所有可能子集。”。先确定答案依赖的是下标、值、路径、窗口、集合还是 DP 状态,再决定数据结构。
  • 不变量:回溯题的状态由 path、start/index 和可用集合组成;每次选择后必须撤销,剪枝只能基于确定不会产生合法解的条件。
  • 边界:重点检查撤销现场、重复解去重、剩余目标为 0、路径长度达到上限。
  • 正确性:从 start 往后选择,天然避免重复顺序。 只要每次更新后不变量仍成立,最终保留下来的状态就对应题目要求的结果。

解法:增量构造子集

从 start 往后选择,天然避免重复顺序。

class Solution:
    def subsets(self, nums: List[int]) -> List[List[int]]:
        ans, path = [], []
        def dfs(start):
            ans.append(path[:])
            for i in range(start, len(nums)):
                path.append(nums[i])
                dfs(i + 1)
                path.pop()
        dfs(0)
        return ans

复杂度:时间 O(n * 2^n),空间 O(n)。

复杂度分析

  • 解法:增量构造子集:时间 O(n * 2^n),递归搜索树会枚举合法选择或组合,复杂度由结果规模和每条路径的构造成本决定。 空间 O(n),主要来自辅助数组、队列、哈希集合或递归栈。
17电话号码的字母组合 Letter Combinations of a Phone Number中等

题目规格

官方题意(改写):给定数字字符串,返回它能映射出的所有字母组合。 完整原题、样例和英文表述以本题官方链接为准。

输入:判题入口是 Solution.letterCombinations(...),输入参数为 digits(str)。

输出:返回 List[str];返回值含义是:给定数字字符串,返回它能映射出的所有字母组合。

约束要点
  • 1 <= digits.length <= 4
  • digits[i] 是范围 ['2', '9'] 的一个数字

题目分析

每一位数字对应一组选项。回溯按位置选择字符,位置走到末尾时得到一个组合。

  • 建模:电话号码的字母组合 可以先理解为“给定数字字符串,返回它能映射出的所有字母组合。”。先确定答案依赖的是下标、值、路径、窗口、集合还是 DP 状态,再决定数据结构。
  • 不变量:回溯题的状态由 path、start/index 和可用集合组成;每次选择后必须撤销,剪枝只能基于确定不会产生合法解的条件。
  • 边界:重点检查撤销现场、重复解去重、剩余目标为 0、路径长度达到上限。
  • 正确性:第 idx 层只处理 digits[idx] 的候选字母。 只要每次更新后不变量仍成立,最终保留下来的状态就对应题目要求的结果。

解法:按数字位置回溯

第 idx 层只处理 digits[idx] 的候选字母。

class Solution:
    def letterCombinations(self, digits: str) -> List[str]:
        if not digits:
            return []
        mp = {'2':'abc','3':'def','4':'ghi','5':'jkl','6':'mno','7':'pqrs','8':'tuv','9':'wxyz'}
        ans, path = [], []
        def dfs(idx):
            if idx == len(digits):
                ans.append(''.join(path))
                return
            for ch in mp[digits[idx]]:
                path.append(ch)
                dfs(idx + 1)
                path.pop()
        dfs(0)
        return ans

复杂度:时间 O(4^n),空间 O(n)。

复杂度分析

  • 解法:按数字位置回溯:时间 O(4^n),递归搜索树会枚举合法选择或组合,复杂度由结果规模和每条路径的构造成本决定。 空间 O(n),主要来自辅助数组、队列、哈希集合或递归栈。
39组合总和 Combination Sum中等

题目规格

官方题意(改写):从候选数中选择若干数使和等于 target,同一个数可以重复使用。 完整原题、样例和英文表述以本题官方链接为准。

输入:判题入口是 Solution.combinationSum(...),输入参数为 candidates(List[int])、target(int)。

输出:返回 List[List[int]];返回值含义是:从候选数中选择若干数使和等于 target,同一个数可以重复使用。

约束要点
  • 1 <= candidates.length <= 30
  • 2 <= candidates[i] <= 40
  • candidates 的所有元素 互不相同
  • 1 <= target <= 40

题目分析

排序后回溯。参数 start 保证组合按非降序生成,避免排列重复;选择 candidates[i] 后递归仍从 i 开始,因为可以重复使用。

  • 建模:组合总和 可以先理解为“从候选数中选择若干数使和等于 target,同一个数可以重复使用。”。先确定答案依赖的是下标、值、路径、窗口、集合还是 DP 状态,再决定数据结构。
  • 不变量:回溯题的状态由 path、start/index 和可用集合组成;每次选择后必须撤销,剪枝只能基于确定不会产生合法解的条件。
  • 边界:重点检查撤销现场、重复解去重、剩余目标为 0、路径长度达到上限。
  • 正确性:剩余目标小于当前数时后面更大,可以停止循环。 只要每次更新后不变量仍成立,最终保留下来的状态就对应题目要求的结果。

解法:排序剪枝回溯

剩余目标小于当前数时后面更大,可以停止循环。

class Solution:
    def combinationSum(self, candidates: List[int], target: int) -> List[List[int]]:
        candidates.sort()
        ans, path = [], []
        def dfs(start, remain):
            if remain == 0:
                ans.append(path[:])
                return
            for i in range(start, len(candidates)):
                x = candidates[i]
                if x > remain:
                    break
                path.append(x)
                dfs(i, remain - x)
                path.pop()
        dfs(0, target)
        return ans

复杂度:时间 指数级,取决于 target 和候选数,空间 O(target/min(candidates))。

复杂度分析

  • 解法:排序剪枝回溯:时间 指数级,取决于 target 和候选数,递归搜索树会枚举合法选择或组合,复杂度由结果规模和每条路径的构造成本决定。 空间 O(target/min(candidates)),额外空间来自代码中显式维护的辅助结构;返回结果本身通常不计入额外空间。
22括号生成 Generate Parentheses中等

题目规格

官方题意(改写):生成所有由 n 对括号组成的合法括号序列。 完整原题、样例和英文表述以本题官方链接为准。

输入:判题入口是 Solution.generateParenthesis(...),输入参数为 n(int)。

输出:返回 List[str];返回值含义是:生成所有由 n 对括号组成的合法括号序列。

约束要点
  • 1 <= n <= 8

题目分析

任何前缀中右括号数量不能超过左括号。回溯维护已用 left/right 数量,左括号未满可放左括号,右括号少于左括号时可放右括号。

  • 建模:括号生成 可以先理解为“生成所有由 n 对括号组成的合法括号序列。”。先确定答案依赖的是下标、值、路径、窗口、集合还是 DP 状态,再决定数据结构。
  • 不变量:回溯题的状态由 path、start/index 和可用集合组成;每次选择后必须撤销,剪枝只能基于确定不会产生合法解的条件。
  • 边界:重点检查撤销现场、重复解去重、剩余目标为 0、路径长度达到上限。
  • 正确性:只生成始终可能合法的前缀。 只要每次更新后不变量仍成立,最终保留下来的状态就对应题目要求的结果。

解法:合法前缀回溯

只生成始终可能合法的前缀。

class Solution:
    def generateParenthesis(self, n: int) -> List[str]:
        ans, path = [], []
        def dfs(left, right):
            if len(path) == 2 * n:
                ans.append(''.join(path))
                return
            if left < n:
                path.append('(')
                dfs(left + 1, right)
                path.pop()
            if right < left:
                path.append(')')
                dfs(left, right + 1)
                path.pop()
        dfs(0, 0)
        return ans

复杂度:时间 O(C_n * n),空间 O(n)。

复杂度分析

  • 解法:合法前缀回溯:时间 O(C_n * n),递归搜索树会枚举合法选择或组合,复杂度由结果规模和每条路径的构造成本决定。 空间 O(n),主要来自辅助数组、队列、哈希集合或递归栈。
79单词搜索 Word Search中等

题目规格

官方题意(改写):判断单词是否可以由网格中上下左右相邻的字符组成,同一格不能重复使用。 完整原题、样例和英文表述以本题官方链接为准。

输入:判题入口是 Solution.exist(...),输入参数为 board(List[List[str]])、word(str)。

输出:返回 bool;返回值含义是:判断单词是否可以由网格中上下左右相邻的字符组成,同一格不能重复使用。

约束要点
  • m == board.length
  • n = board[i].length
  • 1 <= m, n <= 6
  • 1 <= word.length <= 15
  • board 和 word 仅由大小写英文字母组成

题目分析

从每个匹配首字母的格子开始 DFS。访问过的格子临时标记,递归结束后恢复,以支持其他路径使用。

  • 建模:单词搜索 可以先理解为“判断单词是否可以由网格中上下左右相邻的字符组成,同一格不能重复使用。”。先确定答案依赖的是下标、值、路径、窗口、集合还是 DP 状态,再决定数据结构。
  • 不变量:回溯题的状态由 path、start/index 和可用集合组成;每次选择后必须撤销,剪枝只能基于确定不会产生合法解的条件。
  • 边界:重点检查撤销现场、重复解去重、剩余目标为 0、路径长度达到上限。
  • 正确性:idx 表示当前要匹配 word[idx],访问标记防止重复用格子。 只要每次更新后不变量仍成立,最终保留下来的状态就对应题目要求的结果。

解法:DFS 回溯搜索路径

idx 表示当前要匹配 word[idx],访问标记防止重复用格子。

class Solution:
    def exist(self, board: List[List[str]], word: str) -> bool:
        m, n = len(board), len(board[0])
        def dfs(i, j, idx):
            if idx == len(word):
                return True
            if i < 0 or i >= m or j < 0 or j >= n or board[i][j] != word[idx]:
                return False
            tmp = board[i][j]
            board[i][j] = '#'
            ok = (dfs(i + 1, j, idx + 1) or dfs(i - 1, j, idx + 1) or
                  dfs(i, j + 1, idx + 1) or dfs(i, j - 1, idx + 1))
            board[i][j] = tmp
            return ok
        return any(dfs(i, j, 0) for i in range(m) for j in range(n))

复杂度:时间 O(mn * 3^L),空间 O(L)。

复杂度分析

  • 解法:DFS 回溯搜索路径:时间 O(mn * 3^L),矩阵或二维 DP 中每个格子会被访问或更新常数次。 空间 O(L),额外空间来自代码中显式维护的辅助结构;返回结果本身通常不计入额外空间。
131分割回文串 Palindrome Partitioning中等

题目规格

官方题意(改写):把字符串切分成若干子串,要求每个子串都是回文,返回所有切分方案。 完整原题、样例和英文表述以本题官方链接为准。

输入:判题入口是 Solution.partition(...),输入参数为 s(str)。

输出:返回 List[List[str]];返回值含义是:把字符串切分成若干子串,要求每个子串都是回文,返回所有切分方案。

约束要点
  • 1 <= s.length <= 16
  • s 仅由小写英文字母组成

题目分析

先预处理 is_pal[i][j] 表示 s[i:j+1] 是否回文,再回溯枚举每个切分终点。预处理能避免重复判断子串。

  • 建模:分割回文串 可以先理解为“把字符串切分成若干子串,要求每个子串都是回文,返回所有切分方案。”。先确定答案依赖的是下标、值、路径、窗口、集合还是 DP 状态,再决定数据结构。
  • 不变量:回溯题的状态由 path、start/index 和可用集合组成;每次选择后必须撤销,剪枝只能基于确定不会产生合法解的条件。
  • 边界:重点检查撤销现场、重复解去重、剩余目标为 0、路径长度达到上限。
  • 正确性:每层从 start 开始选择一个回文前缀。 只要每次更新后不变量仍成立,最终保留下来的状态就对应题目要求的结果。

解法:回文 DP + 回溯切分

每层从 start 开始选择一个回文前缀。

class Solution:
    def partition(self, s: str) -> List[List[str]]:
        n = len(s)
        is_pal = [[False] * n for _ in range(n)]
        for i in range(n - 1, -1, -1):
            for j in range(i, n):
                is_pal[i][j] = s[i] == s[j] and (j - i < 2 or is_pal[i + 1][j - 1])
        ans, path = [], []
        def dfs(start):
            if start == n:
                ans.append(path[:])
                return
            for end in range(start, n):
                if is_pal[start][end]:
                    path.append(s[start:end + 1])
                    dfs(end + 1)
                    path.pop()
        dfs(0)
        return ans

复杂度:时间 O(n^2 + n * 2^n),空间 O(n^2)。

复杂度分析

  • 解法:回文 DP + 回溯切分:时间 O(n^2 + n * 2^n),递归搜索树会枚举合法选择或组合,复杂度由结果规模和每条路径的构造成本决定。 空间 O(n^2),主要来自 visited 标记、二维 DP 表或递归搜索的最坏栈空间。
51N 皇后 N-Queens困难

题目规格

官方题意(改写):在 n x n 棋盘放置 n 个皇后,使任意两个皇后都不能互相攻击。 完整原题、样例和英文表述以本题官方链接为准。

输入:判题入口是 Solution.solveNQueens(...),输入参数为 n(int)。

输出:返回 List[List[str]];返回值含义是:在 n x n 棋盘放置 n 个皇后,使任意两个皇后都不能互相攻击。

约束要点
  • 1 <= n <= 9

题目分析

按行放皇后。列、主对角线 r-c、副对角线 r+c 分别用集合记录占用,确保每次选择合法位置。

  • 建模:N 皇后 可以先理解为“在 n x n 棋盘放置 n 个皇后,使任意两个皇后都不能互相攻击。”。先确定答案依赖的是下标、值、路径、窗口、集合还是 DP 状态,再决定数据结构。
  • 不变量:回溯题的状态由 path、start/index 和可用集合组成;每次选择后必须撤销,剪枝只能基于确定不会产生合法解的条件。
  • 边界:重点检查撤销现场、重复解去重、剩余目标为 0、路径长度达到上限。
  • 正确性:每行放一个皇后,冲突检查 O(1)。 只要每次更新后不变量仍成立,最终保留下来的状态就对应题目要求的结果。

解法:行级回溯 + 三集合剪枝

每行放一个皇后,冲突检查 O(1)。

class Solution:
    def solveNQueens(self, n: int) -> List[List[str]]:
        ans = []
        cols, diag1, diag2 = set(), set(), set()
        board = [['.'] * n for _ in range(n)]
        def dfs(r):
            if r == n:
                ans.append([''.join(row) for row in board])
                return
            for c in range(n):
                if c in cols or r - c in diag1 or r + c in diag2:
                    continue
                cols.add(c); diag1.add(r - c); diag2.add(r + c)
                board[r][c] = 'Q'
                dfs(r + 1)
                board[r][c] = '.'
                cols.remove(c); diag1.remove(r - c); diag2.remove(r + c)
        dfs(0)
        return ans

复杂度:时间 O(n!) 量级,空间 O(n)。

复杂度分析

  • 解法:行级回溯 + 三集合剪枝:时间 O(n!) 量级,递归搜索树会枚举合法选择或组合,复杂度由结果规模和每条路径的构造成本决定。 空间 O(n),主要来自辅助数组、队列、哈希集合或递归栈。

本组 5 题,围绕 栈 的核心模式展开。先识别不变量,再选择哈希、指针、递归、队列、堆或 DP 状态。

20有效的括号 Valid Parentheses简单

题目规格

官方题意(改写):判断括号字符串是否有效:括号必须按正确顺序闭合。 完整原题、样例和英文表述以本题官方链接为准。

输入:判题入口是 Solution.isValid(...),输入参数为 s(str)。

输出:返回 bool;返回值含义是:判断括号字符串是否有效:括号必须按正确顺序闭合。

约束要点
  • 1 <= s.length <= 10^4
  • s 仅由括号 '()[]{}' 组成

题目分析

遇到左括号入栈,遇到右括号时栈顶必须是匹配的左括号。最后栈为空才有效。

  • 建模:有效的括号 可以先理解为“判断括号字符串是否有效:括号必须按正确顺序闭合。”。先确定答案依赖的是下标、值、路径、窗口、集合还是 DP 状态,再决定数据结构。
  • 不变量:栈题利用“最近的未匹配/未解决元素”性质;单调栈还要求栈内元素按某种顺序保持单调。
  • 边界:重点检查空栈弹出、哨兵下标、相等元素是否弹出、遍历结束后是否需要清算。
  • 正确性:最近打开的括号必须最先闭合。 只要每次更新后不变量仍成立,最终保留下来的状态就对应题目要求的结果。

解法:栈匹配括号

最近打开的括号必须最先闭合。

class Solution:
    def isValid(self, s: str) -> bool:
        match = {')': '(', ']': '[', '}': '{'}
        stack = []
        for ch in s:
            if ch in match.values():
                stack.append(ch)
            else:
                if not stack or stack.pop() != match[ch]:
                    return False
        return not stack

复杂度:时间 O(n),空间 O(n)。

复杂度分析

  • 解法:栈匹配括号:时间 O(n),主循环中每个元素或节点只进入核心结构常数次。 空间 O(n),主要来自显式栈、单调栈或堆中保存的候选元素。
155最小栈 Min Stack中等

题目规格

官方题意(改写):设计一个支持 push、pop、top 和 getMin 的栈,所有操作都要 O(1)。 完整原题、样例和英文表述以本题官方链接为准。

输入:设计类 MinStack,判题器会按照官方操作序列调用构造器和公开方法。

输出:每次查询方法返回题目要求的布尔值、整数或浮点数;更新类方法只改变对象内部状态。核心目标是:设计一个支持 push、pop、top 和 getMin 的栈,所有操作都要 O(1)。

约束要点
  • -2^31 <= val <= 2^31 - 1
  • pop 、 top 和 getMin 操作总是在 非空栈 上调用
  • push , pop , top , and getMin 最多被调用 3 * 10^4 次

题目分析

普通栈存值,辅助栈存当前最小值。每次 push 时把 min(x, 当前最小值) 同步压入辅助栈。

  • 建模:最小栈 可以先理解为“设计一个支持 push、pop、top 和 getMin 的栈,所有操作都要 O(1)。”。先确定答案依赖的是下标、值、路径、窗口、集合还是 DP 状态,再决定数据结构。
  • 不变量:栈题利用“最近的未匹配/未解决元素”性质;单调栈还要求栈内元素按某种顺序保持单调。
  • 边界:重点检查空栈弹出、哨兵下标、相等元素是否弹出、遍历结束后是否需要清算。
  • 正确性:min_stack[i] 表示 data[0..i] 的最小值。 只要每次更新后不变量仍成立,最终保留下来的状态就对应题目要求的结果。

解法:同步最小值栈

min_stack[i] 表示 data[0..i] 的最小值。

class MinStack:
    def __init__(self):
        self.data = []
        self.mins = []

    def push(self, val: int) -> None:
        self.data.append(val)
        self.mins.append(val if not self.mins else min(val, self.mins[-1]))

    def pop(self) -> None:
        self.data.pop()
        self.mins.pop()

    def top(self) -> int:
        return self.data[-1]

    def getMin(self) -> int:
        return self.mins[-1]

复杂度:时间 每次操作 O(1),空间 O(n)。

复杂度分析

  • 解法:同步最小值栈:时间 每次操作 O(1),操作只读取或更新固定数量的变量/指针。 空间 O(n),主要来自显式栈、单调栈或堆中保存的候选元素。
394字符串解码 Decode String中等

题目规格

官方题意(改写):解码形如 k[encoded_string] 的字符串,嵌套结构需要展开。 完整原题、样例和英文表述以本题官方链接为准。

输入:判题入口是 Solution.decodeString(...),输入参数为 s(str)。

输出:返回 str;返回值含义是:解码形如 k[encoded_string] 的字符串,嵌套结构需要展开。

约束要点
  • 1 <= s.length <= 30
  • s 由小写英文字母、数字和方括号 '[]' 组成
  • s 保证是一个 有效 的输入
  • s 中所有整数的取值范围为 [1, 300]

题目分析

用栈保存进入 `[` 前的字符串和重复次数。遇到 `]` 时弹出上下文,把当前片段重复后拼接回上一层。

  • 建模:字符串解码 可以先理解为“解码形如 k[encoded_string] 的字符串,嵌套结构需要展开。”。先确定答案依赖的是下标、值、路径、窗口、集合还是 DP 状态,再决定数据结构。
  • 不变量:栈题利用“最近的未匹配/未解决元素”性质;单调栈还要求栈内元素按某种顺序保持单调。
  • 边界:重点检查空栈弹出、哨兵下标、相等元素是否弹出、遍历结束后是否需要清算。
  • 正确性:每个左括号开启一个新的局部字符串环境。 只要每次更新后不变量仍成立,最终保留下来的状态就对应题目要求的结果。

解法:栈保存上下文

每个左括号开启一个新的局部字符串环境。

class Solution:
    def decodeString(self, s: str) -> str:
        stack = []
        cur = []
        num = 0
        for ch in s:
            if ch.isdigit():
                num = num * 10 + int(ch)
            elif ch == '[':
                stack.append((''.join(cur), num))
                cur = []
                num = 0
            elif ch == ']':
                prev, repeat = stack.pop()
                cur = [prev + ''.join(cur) * repeat]
            else:
                cur.append(ch)
        return ''.join(cur)

复杂度:时间 O(输出长度),空间 O(嵌套深度 + 输出长度)。

复杂度分析

  • 解法:栈保存上下文:时间 O(输出长度),栈题利用“最近的未匹配/未解决元素”性质;单调栈还要求栈内元素按某种顺序保持单调。 空间 O(嵌套深度 + 输出长度),额外空间来自代码中显式维护的辅助结构;返回结果本身通常不计入额外空间。
739每日温度 Daily Temperatures中等

题目规格

官方题意(改写):对每天温度,返回还要等多少天才会遇到更高温度;没有则为 0。 完整原题、样例和英文表述以本题官方链接为准。

输入:判题入口是 Solution.dailyTemperatures(...),输入参数为 temperatures(List[int])。

输出:返回 List[int];返回值含义是:对每天温度,返回还要等多少天才会遇到更高温度;没有则为 0。

约束要点
  • 1 <= temperatures.length <= 10^5
  • 30 <= temperatures[i] <= 100

题目分析

单调递减栈存还没找到更高温的下标。当前温度更高时,不断弹栈并计算等待天数。

  • 建模:每日温度 可以先理解为“对每天温度,返回还要等多少天才会遇到更高温度;没有则为 0。”。先确定答案依赖的是下标、值、路径、窗口、集合还是 DP 状态,再决定数据结构。
  • 不变量:栈题利用“最近的未匹配/未解决元素”性质;单调栈还要求栈内元素按某种顺序保持单调。
  • 边界:重点检查空栈弹出、哨兵下标、相等元素是否弹出、遍历结束后是否需要清算。
  • 正确性:栈内下标对应温度从底到顶递减。 只要每次更新后不变量仍成立,最终保留下来的状态就对应题目要求的结果。

解法:单调递减栈

栈内下标对应温度从底到顶递减。

class Solution:
    def dailyTemperatures(self, temperatures: List[int]) -> List[int]:
        ans = [0] * len(temperatures)
        stack = []
        for i, t in enumerate(temperatures):
            while stack and temperatures[stack[-1]] < t:
                j = stack.pop()
                ans[j] = i - j
            stack.append(i)
        return ans

复杂度:时间 O(n),空间 O(n)。

复杂度分析

  • 解法:单调递减栈:时间 O(n),主循环中每个元素或节点只进入核心结构常数次。 空间 O(n),主要来自显式栈、单调栈或堆中保存的候选元素。
84柱状图中最大的矩形 Largest Rectangle in Histogram困难

题目规格

官方题意(改写):给定柱状图高度,求能组成的最大矩形面积。 完整原题、样例和英文表述以本题官方链接为准。

输入:判题入口是 Solution.largestRectangleArea(...),输入参数为 heights(List[int])。

输出:返回 int;返回值含义是:给定柱状图高度,求能组成的最大矩形面积。

约束要点
  • 1 <= heights.length <= 10^5
  • 0 <= heights[i] <= 10^4

题目分析

对每根柱子,若知道左右第一个比它矮的位置,就能确定以它为最低高度的最大宽度。单调递增栈在遇到更矮柱子时结算被弹出的柱子。

  • 建模:柱状图中最大的矩形 可以先理解为“给定柱状图高度,求能组成的最大矩形面积。”。先确定答案依赖的是下标、值、路径、窗口、集合还是 DP 状态,再决定数据结构。
  • 不变量:栈题利用“最近的未匹配/未解决元素”性质;单调栈还要求栈内元素按某种顺序保持单调。
  • 边界:重点检查空栈弹出、哨兵下标、相等元素是否弹出、遍历结束后是否需要清算。
  • 正确性:末尾加 0 强制清空栈,栈中下标对应高度递增。 只要每次更新后不变量仍成立,最终保留下来的状态就对应题目要求的结果。

解法:单调递增栈 + 哨兵

末尾加 0 强制清空栈,栈中下标对应高度递增。

class Solution:
    def largestRectangleArea(self, heights: List[int]) -> int:
        stack = []
        best = 0
        for i, h in enumerate(heights + [0]):
            while stack and heights[stack[-1]] > h:
                height = heights[stack.pop()]
                left = stack[-1] if stack else -1
                best = max(best, height * (i - left - 1))
            stack.append(i)
        return best

复杂度:时间 O(n),空间 O(n)。

复杂度分析

  • 解法:单调递增栈 + 哨兵:时间 O(n),主循环中每个元素或节点只进入核心结构常数次。 空间 O(n),主要来自显式栈、单调栈或堆中保存的候选元素。

本组 3 题,围绕 堆 的核心模式展开。先识别不变量,再选择哈希、指针、递归、队列、堆或 DP 状态。

215数组中的第K个最大元素 Kth Largest Element in an Array中等

题目规格

官方题意(改写):找到数组中第 k 大的元素。 完整原题、样例和英文表述以本题官方链接为准。

输入:判题入口是 Solution.findKthLargest(...),输入参数为 nums(List[int])、k(int)。

输出:返回 int;返回值含义是:找到数组中第 k 大的元素。

约束要点
  • 1 <= k <= nums.length <= 10^5
  • -10^4 <= nums[i] <= 10^4

题目分析

最直接可用大小为 k 的小根堆;也可以用 Quickselect 平均线性时间。堆写法稳定、易实现。

  • 建模:数组中的第K个最大元素 可以先理解为“找到数组中第 k 大的元素。”。先确定答案依赖的是下标、值、路径、窗口、集合还是 DP 状态,再决定数据结构。
  • 不变量:堆题关注动态 top-k 或流式中位数;堆内只保留当前最有竞争力的候选,弹出规则必须和目标排序一致。
  • 边界:重点检查堆大小、相同优先级的 tie-breaker、数据流为空或 k 等于元素种类数。
  • 正确性:堆顶始终是当前前 k 大中的最小值。 只要每次更新后不变量仍成立,最终保留下来的状态就对应题目要求的结果。

解法:大小为 k 的小根堆

堆顶始终是当前前 k 大中的最小值。

import heapq

class Solution:
    def findKthLargest(self, nums: List[int], k: int) -> int:
        heap = []
        for x in nums:
            heapq.heappush(heap, x)
            if len(heap) > k:
                heapq.heappop(heap)
        return heap[0]

复杂度:时间 O(n log k),空间 O(k)。

复杂度分析

  • 解法:大小为 k 的小根堆:时间 O(n log k),每个元素最多触发一次堆插入/弹出,堆大小受 k 控制。 空间 O(k),主要来自大小受 k 控制的堆、窗口或候选集合。
347前 K 个高频元素 Top K Frequent Elements中等

题目规格

官方题意(改写):返回数组中出现频率最高的 k 个元素。 完整原题、样例和英文表述以本题官方链接为准。

输入:判题入口是 Solution.topKFrequent(...),输入参数为 nums(List[int])、k(int)。

输出:返回 List[int];返回值含义是:返回数组中出现频率最高的 k 个元素。

约束要点
  • 1 <= nums.length <= 10^5
  • -10^4 <= nums[i] <= 10^4
  • k 的取值范围是 [1, 数组中不相同的元素的个数]
  • 题目数据保证答案唯一,换句话说,数组中前 k 个高频元素的集合是唯一的

题目分析

先统计频率,再用小根堆保留频率最高的 k 个元素。由于只关心 top k,堆大小不必超过 k。

  • 建模:前 K 个高频元素 可以先理解为“返回数组中出现频率最高的 k 个元素。”。先确定答案依赖的是下标、值、路径、窗口、集合还是 DP 状态,再决定数据结构。
  • 不变量:堆题关注动态 top-k 或流式中位数;堆内只保留当前最有竞争力的候选,弹出规则必须和目标排序一致。
  • 边界:重点检查堆大小、相同优先级的 tie-breaker、数据流为空或 k 等于元素种类数。
  • 正确性:堆元素为 (freq, value),超出 k 时弹出最低频。 只要每次更新后不变量仍成立,最终保留下来的状态就对应题目要求的结果。

解法:计数 + 小根堆

堆元素为 (freq, value),超出 k 时弹出最低频。

from collections import Counter
import heapq

class Solution:
    def topKFrequent(self, nums: List[int], k: int) -> List[int]:
        heap = []
        for x, freq in Counter(nums).items():
            heapq.heappush(heap, (freq, x))
            if len(heap) > k:
                heapq.heappop(heap)
        return [x for _, x in heap]

复杂度:时间 O(n log k),空间 O(n)。

复杂度分析

  • 解法:计数 + 小根堆:时间 O(n log k),每个元素最多触发一次堆插入/弹出,堆大小受 k 控制。 空间 O(n),主要来自显式栈、单调栈或堆中保存的候选元素。
295数据流的中位数 Find Median from Data Stream困难

题目规格

官方题意(改写):设计数据结构,持续加入数字并能返回当前中位数。 完整原题、样例和英文表述以本题官方链接为准。

输入:设计类 MedianFinder,判题器会按照官方操作序列调用构造器和公开方法。

输出:每次查询方法返回题目要求的布尔值、整数或浮点数;更新类方法只改变对象内部状态。核心目标是:设计数据结构,持续加入数字并能返回当前中位数。

约束要点
  • -10^5 <= num <= 10^5
  • 在调用 findMedian 之前,数据结构中至少有一个元素
  • 最多 5 * 10^4 次调用 addNum 和 findMedian

题目分析

用两个堆维护较小一半和较大一半。左边用最大堆(取负数实现),右边用最小堆,并保持两堆大小差不超过 1。

  • 建模:数据流的中位数 可以先理解为“设计数据结构,持续加入数字并能返回当前中位数。”。先确定答案依赖的是下标、值、路径、窗口、集合还是 DP 状态,再决定数据结构。
  • 不变量:堆题关注动态 top-k 或流式中位数;堆内只保留当前最有竞争力的候选,弹出规则必须和目标排序一致。
  • 边界:重点检查堆大小、相同优先级的 tie-breaker、数据流为空或 k 等于元素种类数。
  • 正确性:small 存较小一半,large 存较大一半;奇数时 small 多一个。 只要每次更新后不变量仍成立,最终保留下来的状态就对应题目要求的结果。

解法:双堆平衡

small 存较小一半,large 存较大一半;奇数时 small 多一个。

import heapq

class MedianFinder:
    def __init__(self):
        self.small = []  # 最大堆,存负数
        self.large = []  # 最小堆

    def addNum(self, num: int) -> None:
        heapq.heappush(self.small, -num)
        heapq.heappush(self.large, -heapq.heappop(self.small))
        if len(self.large) > len(self.small):
            heapq.heappush(self.small, -heapq.heappop(self.large))

    def findMedian(self) -> float:
        if len(self.small) > len(self.large):
            return -self.small[0]
        return (-self.small[0] + self.large[0]) / 2

复杂度:时间 addNum O(log n),findMedian O(1),空间 O(n)。

复杂度分析

  • 解法:双堆平衡:时间 addNum O(log n),findMedian O(1),主要成本来自排序、堆操作或二分搜索中的对数级收缩。 空间 O(n),主要来自显式栈、单调栈或堆中保存的候选元素。

贪心算法

本组 4 题,围绕 贪心算法 的核心模式展开。先识别不变量,再选择哈希、指针、递归、队列、堆或 DP 状态。

121买卖股票的最佳时机 Best Time to Buy and Sell Stock简单

题目规格

官方题意(改写):给定每天股价,只允许买卖一次,求最大利润。 完整原题、样例和英文表述以本题官方链接为准。

输入:判题入口是 Solution.maxProfit(...),输入参数为 prices(List[int])。

输出:返回 int;返回值含义是:给定每天股价,只允许买卖一次,求最大利润。

约束要点
  • 1 <= prices.length <= 10^5
  • 0 <= prices[i] <= 10^4

题目分析

卖出日确定时,最佳买入价是此前最低价。遍历价格时维护 min_price,并更新当前卖出的最大利润。

  • 建模:买卖股票的最佳时机 可以先理解为“给定每天股价,只允许买卖一次,求最大利润。”。先确定答案依赖的是下标、值、路径、窗口、集合还是 DP 状态,再决定数据结构。
  • 不变量:贪心题要证明局部选择不会破坏全局最优:通常通过最远边界、最低成本、最后出现位置或历史最优状态来表达。
  • 边界:重点检查不可达、利润为 0、最后一个区间/片段是否结算。
  • 正确性:每一天只作为卖出日尝试一次。 只要每次更新后不变量仍成立,最终保留下来的状态就对应题目要求的结果。

解法:维护历史最低买入价

每一天只作为卖出日尝试一次。

class Solution:
    def maxProfit(self, prices: List[int]) -> int:
        min_price = float('inf')
        best = 0
        for price in prices:
            min_price = min(min_price, price)
            best = max(best, price - min_price)
        return best

复杂度:时间 O(n),空间 O(1)。

复杂度分析

  • 解法:维护历史最低买入价:时间 O(n),主循环中每个元素或节点只进入核心结构常数次。 空间 O(1),只使用常数个指针、计数器或滚动变量;题目要求的输出不计入额外空间。
55跳跃游戏 Jump Game中等

题目规格

官方题意(改写):判断从数组第一个位置出发,是否能跳到最后一个位置。 完整原题、样例和英文表述以本题官方链接为准。

输入:判题入口是 Solution.canJump(...),输入参数为 nums(List[int])。

输出:返回 bool;返回值含义是:判断从数组第一个位置出发,是否能跳到最后一个位置。

约束要点
  • 1 <= nums.length <= 10^4
  • 0 <= nums[i] <= 10^5

题目分析

贪心维护当前能到达的最远位置 farthest。若遍历到 i 时 i 已超过 farthest,说明这个位置不可达。

  • 建模:跳跃游戏 可以先理解为“判断从数组第一个位置出发,是否能跳到最后一个位置。”。先确定答案依赖的是下标、值、路径、窗口、集合还是 DP 状态,再决定数据结构。
  • 不变量:贪心题要证明局部选择不会破坏全局最优:通常通过最远边界、最低成本、最后出现位置或历史最优状态来表达。
  • 边界:重点检查不可达、利润为 0、最后一个区间/片段是否结算。
  • 正确性:只要每个经过的位置都可达,就持续扩展边界。 只要每次更新后不变量仍成立,最终保留下来的状态就对应题目要求的结果。

解法:最远可达位置

只要每个经过的位置都可达,就持续扩展边界。

class Solution:
    def canJump(self, nums: List[int]) -> bool:
        farthest = 0
        for i, jump in enumerate(nums):
            if i > farthest:
                return False
            farthest = max(farthest, i + jump)
            if farthest >= len(nums) - 1:
                return True
        return True

复杂度:时间 O(n),空间 O(1)。

复杂度分析

  • 解法:最远可达位置:时间 O(n),主循环中每个元素或节点只进入核心结构常数次。 空间 O(1),只使用常数个指针、计数器或滚动变量;题目要求的输出不计入额外空间。
45跳跃游戏 II Jump Game II中等

题目规格

官方题意(改写):求从第一个位置跳到最后一个位置的最少跳跃次数。 完整原题、样例和英文表述以本题官方链接为准。

输入:判题入口是 Solution.jump(...),输入参数为 nums(List[int])。

输出:返回 int;返回值含义是:求从第一个位置跳到最后一个位置的最少跳跃次数。

约束要点
  • 1 <= nums.length <= 10^4
  • 0 <= nums[i] <= 1000
  • 题目保证可以到达 n - 1

题目分析

把当前跳数能覆盖的范围看成一层 BFS。遍历当前层时维护下一层最远边界;到达当前层末尾就必须跳一次。

  • 建模:跳跃游戏 II 可以先理解为“求从第一个位置跳到最后一个位置的最少跳跃次数。”。先确定答案依赖的是下标、值、路径、窗口、集合还是 DP 状态,再决定数据结构。
  • 不变量:贪心题要证明局部选择不会破坏全局最优:通常通过最远边界、最低成本、最后出现位置或历史最优状态来表达。
  • 边界:重点检查不可达、利润为 0、最后一个区间/片段是否结算。
  • 正确性:end 是当前跳数能到的最远位置,farthest 是下一跳能到的最远位置。 只要每次更新后不变量仍成立,最终保留下来的状态就对应题目要求的结果。

解法:贪心层边界

end 是当前跳数能到的最远位置,farthest 是下一跳能到的最远位置。

class Solution:
    def jump(self, nums: List[int]) -> int:
        steps = 0
        end = farthest = 0
        for i in range(len(nums) - 1):
            farthest = max(farthest, i + nums[i])
            if i == end:
                steps += 1
                end = farthest
        return steps

复杂度:时间 O(n),空间 O(1)。

复杂度分析

  • 解法:贪心层边界:时间 O(n),主循环中每个元素或节点只进入核心结构常数次。 空间 O(1),只使用常数个指针、计数器或滚动变量;题目要求的输出不计入额外空间。
763划分字母区间 Partition Labels中等

题目规格

官方题意(改写):把字符串划分为尽可能多的片段,使每个字母最多只出现在一个片段中。 完整原题、样例和英文表述以本题官方链接为准。

输入:判题入口是 Solution.partitionLabels(...),输入参数为 s(str)。

输出:返回 List[int];返回值含义是:把字符串划分为尽可能多的片段,使每个字母最多只出现在一个片段中。

约束要点
  • 1 <= s.length <= 500
  • s 仅由小写英文字母组成

题目分析

先记录每个字符最后出现位置。扫描时维护当前片段必须覆盖到的最远位置 end,当 i == end 时可以切分。

  • 建模:划分字母区间 可以先理解为“把字符串划分为尽可能多的片段,使每个字母最多只出现在一个片段中。”。先确定答案依赖的是下标、值、路径、窗口、集合还是 DP 状态,再决定数据结构。
  • 不变量:贪心题要证明局部选择不会破坏全局最优:通常通过最远边界、最低成本、最后出现位置或历史最优状态来表达。
  • 边界:重点检查不可达、利润为 0、最后一个区间/片段是否结算。
  • 正确性:片段必须覆盖其中所有字符的最后位置。 只要每次更新后不变量仍成立,最终保留下来的状态就对应题目要求的结果。

解法:最后出现位置贪心

片段必须覆盖其中所有字符的最后位置。

class Solution:
    def partitionLabels(self, s: str) -> List[int]:
        last = {ch: i for i, ch in enumerate(s)}
        ans = []
        start = end = 0
        for i, ch in enumerate(s):
            end = max(end, last[ch])
            if i == end:
                ans.append(end - start + 1)
                start = i + 1
        return ans

复杂度:时间 O(n),空间 O(字符集)。

复杂度分析

  • 解法:最后出现位置贪心:时间 O(n),主循环中每个元素或节点只进入核心结构常数次。 空间 O(字符集),额外空间来自代码中显式维护的辅助结构;返回结果本身通常不计入额外空间。

动态规划

本组 10 题,围绕 动态规划 的核心模式展开。先识别不变量,再选择哈希、指针、递归、队列、堆或 DP 状态。

70爬楼梯 Climbing Stairs简单

题目规格

官方题意(改写):每次可以爬 1 或 2 阶,求到达第 n 阶的方法数。 完整原题、样例和英文表述以本题官方链接为准。

输入:判题入口是 Solution.climbStairs(...),输入参数为 n(int)。

输出:返回 int;返回值含义是:每次可以爬 1 或 2 阶,求到达第 n 阶的方法数。

约束要点
  • 1 <= n <= 45

题目分析

到达第 i 阶的最后一步来自 i-1 或 i-2,因此 dp[i] = dp[i-1] + dp[i-2]。这就是 Fibonacci 结构。

  • 建模:爬楼梯 可以先理解为“每次可以爬 1 或 2 阶,求到达第 n 阶的方法数。”。先确定答案依赖的是下标、值、路径、窗口、集合还是 DP 状态,再决定数据结构。
  • 不变量:DP 题先定义状态覆盖的前缀或容量,再写出最后一步来自哪里;滚动数组只能在依赖方向明确后使用。
  • 边界:重点检查 dp[0] 含义、倒序/正序更新、目标无法达到和只有一个元素。
  • 正确性:只保留前两项即可。 只要每次更新后不变量仍成立,最终保留下来的状态就对应题目要求的结果。

解法:滚动 DP

只保留前两项即可。

class Solution:
    def climbStairs(self, n: int) -> int:
        a, b = 1, 1
        for _ in range(n):
            a, b = b, a + b
        return a

复杂度:时间 O(n),空间 O(1)。

复杂度分析

  • 解法:滚动 DP:时间 O(n),主循环中每个元素或节点只进入核心结构常数次。 空间 O(1),只使用常数个指针、计数器或滚动变量;题目要求的输出不计入额外空间。
118杨辉三角 Pascal's Triangle简单

题目规格

官方题意(改写):生成杨辉三角前 numRows 行。 完整原题、样例和英文表述以本题官方链接为准。

输入:判题入口是 Solution.generate(...),输入参数为 numRows(int)。

输出:返回 List[List[int]];返回值含义是:生成杨辉三角前 numRows 行。

约束要点
  • 1 <= numRows <= 30

题目分析

每行首尾是 1,中间元素等于上一行相邻两个元素之和。逐行构造即可。

  • 建模:杨辉三角 可以先理解为“生成杨辉三角前 numRows 行。”。先确定答案依赖的是下标、值、路径、窗口、集合还是 DP 状态,再决定数据结构。
  • 不变量:DP 题先定义状态覆盖的前缀或容量,再写出最后一步来自哪里;滚动数组只能在依赖方向明确后使用。
  • 边界:重点检查 dp[0] 含义、倒序/正序更新、目标无法达到和只有一个元素。
  • 正确性:第 i 行只依赖第 i-1 行。 只要每次更新后不变量仍成立,最终保留下来的状态就对应题目要求的结果。

解法:逐行递推

第 i 行只依赖第 i-1 行。

class Solution:
    def generate(self, numRows: int) -> List[List[int]]:
        ans = []
        for i in range(numRows):
            row = [1] * (i + 1)
            for j in range(1, i):
                row[j] = ans[i - 1][j - 1] + ans[i - 1][j]
            ans.append(row)
        return ans

复杂度:时间 O(numRows^2),空间 O(numRows^2) 输出空间。

复杂度分析

  • 解法:逐行递推:时间 O(numRows^2),主循环中每个元素或节点只进入核心结构常数次。 空间 O(numRows^2) 输出空间,额外空间来自代码中显式维护的辅助结构;返回结果本身通常不计入额外空间。
198打家劫舍 House Robber中等

题目规格

官方题意(改写):不能偷相邻房屋,求能偷到的最大金额。 完整原题、样例和英文表述以本题官方链接为准。

输入:判题入口是 Solution.rob(...),输入参数为 nums(List[int])。

输出:返回 int;返回值含义是:不能偷相邻房屋,求能偷到的最大金额。

约束要点
  • 1 <= nums.length <= 100
  • 0 <= nums[i] <= 400

题目分析

到第 i 间房的最优解要么不偷它,继承前一间最优;要么偷它,加上 i-2 的最优。

  • 建模:打家劫舍 可以先理解为“不能偷相邻房屋,求能偷到的最大金额。”。先确定答案依赖的是下标、值、路径、窗口、集合还是 DP 状态,再决定数据结构。
  • 不变量:DP 题先定义状态覆盖的前缀或容量,再写出最后一步来自哪里;滚动数组只能在依赖方向明确后使用。
  • 边界:重点检查 dp[0] 含义、倒序/正序更新、目标无法达到和只有一个元素。
  • 正确性:prev2 表示 i-2 最优,prev1 表示 i-1 最优。 只要每次更新后不变量仍成立,最终保留下来的状态就对应题目要求的结果。

解法:相邻约束 DP

prev2 表示 i-2 最优,prev1 表示 i-1 最优。

class Solution:
    def rob(self, nums: List[int]) -> int:
        prev2 = prev1 = 0
        for x in nums:
            prev2, prev1 = prev1, max(prev1, prev2 + x)
        return prev1

复杂度:时间 O(n),空间 O(1)。

复杂度分析

  • 解法:相邻约束 DP:时间 O(n),主循环中每个元素或节点只进入核心结构常数次。 空间 O(1),只使用常数个指针、计数器或滚动变量;题目要求的输出不计入额外空间。
279完全平方数 Perfect Squares中等

题目规格

官方题意(改写):求组成 n 的完全平方数的最少数量。 完整原题、样例和英文表述以本题官方链接为准。

输入:判题入口是 Solution.numSquares(...),输入参数为 n(int)。

输出:返回 int;返回值含义是:求组成 n 的完全平方数的最少数量。

约束要点
  • 1 <= n <= 10^4

题目分析

这是完全背包最少物品数。dp[i] 表示凑出 i 的最少平方数数量,枚举每个平方数更新。

  • 建模:完全平方数 可以先理解为“求组成 n 的完全平方数的最少数量。”。先确定答案依赖的是下标、值、路径、窗口、集合还是 DP 状态,再决定数据结构。
  • 不变量:DP 题先定义状态覆盖的前缀或容量,再写出最后一步来自哪里;滚动数组只能在依赖方向明确后使用。
  • 边界:重点检查 dp[0] 含义、倒序/正序更新、目标无法达到和只有一个元素。
  • 正确性:每个平方数可以重复使用。 只要每次更新后不变量仍成立,最终保留下来的状态就对应题目要求的结果。

解法:完全背包 DP

每个平方数可以重复使用。

class Solution:
    def numSquares(self, n: int) -> int:
        dp = [0] + [float('inf')] * n
        squares = [i * i for i in range(1, int(n ** 0.5) + 1)]
        for i in range(1, n + 1):
            for sq in squares:
                if sq > i:
                    break
                dp[i] = min(dp[i], dp[i - sq] + 1)
        return dp[n]

复杂度:时间 O(n sqrt n),空间 O(n)。

复杂度分析

  • 解法:完全背包 DP:时间 O(n sqrt n),每个状态会枚举不超过平方根数量的候选平方数。 空间 O(n),主要来自 DP 数组或滚动状态;若返回所有结果,输出本身不计入额外空间。
322零钱兑换 Coin Change中等

题目规格

官方题意(改写):给定硬币面额和总金额,求凑出金额所需的最少硬币数,不能凑出返回 -1。 完整原题、样例和英文表述以本题官方链接为准。

输入:判题入口是 Solution.coinChange(...),输入参数为 coins(List[int])、amount(int)。

输出:返回 int;返回值含义是:给定硬币面额和总金额,求凑出金额所需的最少硬币数,不能凑出返回 -1。

约束要点
  • 1 <= coins.length <= 12
  • 1 <= coins[i] <= 2^31 - 1
  • 0 <= amount <= 10^4

题目分析

完全背包最小值。dp[x] 表示凑出金额 x 的最少硬币数,转移为 dp[x] = min(dp[x-coin] + 1)。

  • 建模:零钱兑换 可以先理解为“给定硬币面额和总金额,求凑出金额所需的最少硬币数,不能凑出返回 -1。”。先确定答案依赖的是下标、值、路径、窗口、集合还是 DP 状态,再决定数据结构。
  • 不变量:DP 题先定义状态覆盖的前缀或容量,再写出最后一步来自哪里;滚动数组只能在依赖方向明确后使用。
  • 边界:重点检查 dp[0] 含义、倒序/正序更新、目标无法达到和只有一个元素。
  • 正确性:外层枚举金额,内层枚举最后一枚硬币。 只要每次更新后不变量仍成立,最终保留下来的状态就对应题目要求的结果。

解法:完全背包最少硬币

外层枚举金额,内层枚举最后一枚硬币。

class Solution:
    def coinChange(self, coins: List[int], amount: int) -> int:
        dp = [0] + [float('inf')] * amount
        for x in range(1, amount + 1):
            for coin in coins:
                if x >= coin:
                    dp[x] = min(dp[x], dp[x - coin] + 1)
        return -1 if dp[amount] == float('inf') else dp[amount]

复杂度:时间 O(amount * len(coins)),空间 O(amount)。

复杂度分析

  • 解法:完全背包最少硬币:时间 O(amount * len(coins)),DP 题先定义状态覆盖的前缀或容量,再写出最后一步来自哪里;滚动数组只能在依赖方向明确后使用。 空间 O(amount),额外空间来自代码中显式维护的辅助结构;返回结果本身通常不计入额外空间。
139单词拆分 Word Break中等

题目规格

官方题意(改写):判断字符串是否可以被字典中的一个或多个单词拼接而成。 完整原题、样例和英文表述以本题官方链接为准。

输入:判题入口是 Solution.wordBreak(...),输入参数为 s(str)、wordDict(List[str])。

输出:返回 bool;返回值含义是:判断字符串是否可以被字典中的一个或多个单词拼接而成。

约束要点
  • 1 <= s.length <= 300
  • 1 <= wordDict.length <= 1000
  • 1 <= wordDict[i].length <= 20
  • s 和 wordDict[i] 仅由小写英文字母组成
  • wordDict 中的所有字符串 互不相同

题目分析

dp[i] 表示 s[:i] 是否可拆分。若存在 j

  • 建模:单词拆分 可以先理解为“判断字符串是否可以被字典中的一个或多个单词拼接而成。”。先确定答案依赖的是下标、值、路径、窗口、集合还是 DP 状态,再决定数据结构。
  • 不变量:DP 题先定义状态覆盖的前缀或容量,再写出最后一步来自哪里;滚动数组只能在依赖方向明确后使用。
  • 边界:重点检查 dp[0] 含义、倒序/正序更新、目标无法达到和只有一个元素。
  • 正确性:每个切分点 j 把问题拆成已可达前缀和一个字典单词。 只要每次更新后不变量仍成立,最终保留下来的状态就对应题目要求的结果。

解法:前缀可达 DP

每个切分点 j 把问题拆成已可达前缀和一个字典单词。

class Solution:
    def wordBreak(self, s: str, wordDict: List[str]) -> bool:
        words = set(wordDict)
        dp = [False] * (len(s) + 1)
        dp[0] = True
        for i in range(1, len(s) + 1):
            for j in range(i):
                if dp[j] and s[j:i] in words:
                    dp[i] = True
                    break
        return dp[-1]

复杂度:时间 O(n^2),空间 O(n)。

复杂度分析

  • 解法:前缀可达 DP:时间 O(n^2),需要枚举两个维度、中心扩展或区间端点,形成二次级状态/比较。 空间 O(n),主要来自 DP 数组或滚动状态;若返回所有结果,输出本身不计入额外空间。
300最长递增子序列 Longest Increasing Subsequence中等

题目规格

官方题意(改写):求数组中最长严格递增子序列的长度。 完整原题、样例和英文表述以本题官方链接为准。

输入:判题入口是 Solution.lengthOfLIS(...),输入参数为 nums(List[int])。

输出:返回 int;返回值含义是:求数组中最长严格递增子序列的长度。

约束要点
  • 1 <= nums.length <= 2500
  • -10^4 <= nums[i] <= 10^4

题目分析

tails[len-1] 表示长度为 len 的递增子序列可能拥有的最小结尾。对每个数用二分找到替换位置,保持 tails 尽量小。

  • 建模:最长递增子序列 可以先理解为“求数组中最长严格递增子序列的长度。”。先确定答案依赖的是下标、值、路径、窗口、集合还是 DP 状态,再决定数据结构。
  • 不变量:DP 题先定义状态覆盖的前缀或容量,再写出最后一步来自哪里;滚动数组只能在依赖方向明确后使用。
  • 边界:重点检查 dp[0] 含义、倒序/正序更新、目标无法达到和只有一个元素。
  • 正确性:更小结尾给后续元素留下更多扩展空间。 只要每次更新后不变量仍成立,最终保留下来的状态就对应题目要求的结果。

解法:贪心 + 二分 tails

更小结尾给后续元素留下更多扩展空间。

from bisect import bisect_left

class Solution:
    def lengthOfLIS(self, nums: List[int]) -> int:
        tails = []
        for x in nums:
            i = bisect_left(tails, x)
            if i == len(tails):
                tails.append(x)
            else:
                tails[i] = x
        return len(tails)

复杂度:时间 O(n log n),空间 O(n)。

复杂度分析

  • 解法:贪心 + 二分 tails:时间 O(n log n),二分每轮都会丢弃一半搜索区间,直到边界收敛。 空间 O(n),主要来自 DP 数组或滚动状态;若返回所有结果,输出本身不计入额外空间。
152乘积最大子数组 Maximum Product Subarray中等

题目规格

官方题意(改写):求数组中乘积最大的连续子数组。 完整原题、样例和英文表述以本题官方链接为准。

输入:判题入口是 Solution.maxProduct(...),输入参数为 nums(List[int])。

输出:返回 int;返回值含义是:求数组中乘积最大的连续子数组。

约束要点
  • 1 <= nums.length <= 2 * 10^4
  • -10 <= nums[i] <= 10
  • nums 的任何子数组的乘积都 保证 是一个 32-位 整数

题目分析

负数会让最大值和最小值互换。维护以当前位置结尾的最大乘积和最小乘积,遇到新数 x 时从三种候选中取值。

  • 建模:乘积最大子数组 可以先理解为“求数组中乘积最大的连续子数组。”。先确定答案依赖的是下标、值、路径、窗口、集合还是 DP 状态,再决定数据结构。
  • 不变量:DP 题先定义状态覆盖的前缀或容量,再写出最后一步来自哪里;滚动数组只能在依赖方向明确后使用。
  • 边界:重点检查 dp[0] 含义、倒序/正序更新、目标无法达到和只有一个元素。
  • 正确性:最小负乘积乘以负数可能变成最大正乘积。 只要每次更新后不变量仍成立,最终保留下来的状态就对应题目要求的结果。

解法:同时维护最大和最小乘积

最小负乘积乘以负数可能变成最大正乘积。

class Solution:
    def maxProduct(self, nums: List[int]) -> int:
        cur_max = cur_min = best = nums[0]
        for x in nums[1:]:
            a, b = cur_max * x, cur_min * x
            cur_max = max(x, a, b)
            cur_min = min(x, a, b)
            best = max(best, cur_max)
        return best

复杂度:时间 O(n),空间 O(1)。

复杂度分析

  • 解法:同时维护最大和最小乘积:时间 O(n),主循环中每个元素或节点只进入核心结构常数次。 空间 O(1),只使用常数个指针、计数器或滚动变量;题目要求的输出不计入额外空间。
416分割等和子集 Partition Equal Subset Sum中等

题目规格

官方题意(改写):判断数组是否能分成两个和相等的子集。 完整原题、样例和英文表述以本题官方链接为准。

输入:判题入口是 Solution.canPartition(...),输入参数为 nums(List[int])。

输出:返回 bool;返回值含义是:判断数组是否能分成两个和相等的子集。

约束要点
  • 1 <= nums.length <= 200
  • 1 <= nums[i] <= 100

题目分析

总和必须为偶数。问题转化为能否从数组中选出若干数使和为 total/2,这是 0/1 背包可达性问题。

  • 建模:分割等和子集 可以先理解为“判断数组是否能分成两个和相等的子集。”。先确定答案依赖的是下标、值、路径、窗口、集合还是 DP 状态,再决定数据结构。
  • 不变量:DP 题先定义状态覆盖的前缀或容量,再写出最后一步来自哪里;滚动数组只能在依赖方向明确后使用。
  • 边界:重点检查 dp[0] 含义、倒序/正序更新、目标无法达到和只有一个元素。
  • 正确性:倒序更新,保证每个数只使用一次。 只要每次更新后不变量仍成立,最终保留下来的状态就对应题目要求的结果。

解法:0/1 背包可达性

倒序更新,保证每个数只使用一次。

class Solution:
    def canPartition(self, nums: List[int]) -> bool:
        total = sum(nums)
        if total % 2:
            return False
        target = total // 2
        dp = [False] * (target + 1)
        dp[0] = True
        for x in nums:
            for s in range(target, x - 1, -1):
                dp[s] = dp[s] or dp[s - x]
        return dp[target]

复杂度:时间 O(n * target),空间 O(target)。

复杂度分析

  • 解法:0/1 背包可达性:时间 O(n * target),主循环中每个元素或节点只进入核心结构常数次。 空间 O(target),额外空间来自代码中显式维护的辅助结构;返回结果本身通常不计入额外空间。
32最长有效括号 Longest Valid Parentheses困难

题目规格

官方题意(改写):求最长有效括号子串长度。 完整原题、样例和英文表述以本题官方链接为准。

输入:判题入口是 Solution.longestValidParentheses(...),输入参数为 s(str)。

输出:返回 int;返回值含义是:求最长有效括号子串长度。

约束要点
  • 0 <= s.length <= 3 * 10^4
  • s[i] 为 '(' 或 ')'

题目分析

栈存还未匹配的下标,并用 -1 作为最近非法位置哨兵。遇到右括号匹配后,当前有效长度是 i - stack[-1]。

  • 建模:最长有效括号 可以先理解为“求最长有效括号子串长度。”。先确定答案依赖的是下标、值、路径、窗口、集合还是 DP 状态,再决定数据结构。
  • 不变量:DP 题先定义状态覆盖的前缀或容量,再写出最后一步来自哪里;滚动数组只能在依赖方向明确后使用。
  • 边界:重点检查 dp[0] 含义、倒序/正序更新、目标无法达到和只有一个元素。
  • 正确性:栈底记录当前有效区间左边界之前的位置。 只要每次更新后不变量仍成立,最终保留下来的状态就对应题目要求的结果。

解法:栈 + 非法位置哨兵

栈底记录当前有效区间左边界之前的位置。

class Solution:
    def longestValidParentheses(self, s: str) -> int:
        stack = [-1]
        best = 0
        for i, ch in enumerate(s):
            if ch == '(':
                stack.append(i)
            else:
                stack.pop()
                if not stack:
                    stack.append(i)
                else:
                    best = max(best, i - stack[-1])
        return best

复杂度:时间 O(n),空间 O(n)。

复杂度分析

  • 解法:栈 + 非法位置哨兵:时间 O(n),主循环中每个元素或节点只进入核心结构常数次。 空间 O(n),主要来自 DP 数组或滚动状态;若返回所有结果,输出本身不计入额外空间。

多维动态规划

本组 5 题,围绕 多维动态规划 的核心模式展开。先识别不变量,再选择哈希、指针、递归、队列、堆或 DP 状态。

62不同路径 Unique Paths中等

题目规格

官方题意(改写):机器人只能向右或向下移动,求从左上到右下的不同路径数。 完整原题、样例和英文表述以本题官方链接为准。

输入:判题入口是 Solution.uniquePaths(...),输入参数为 m(int)、n(int)。

输出:返回 int;返回值含义是:机器人只能向右或向下移动,求从左上到右下的不同路径数。

约束要点
  • 1 <= m, n <= 100
  • 题目数据保证答案小于等于 2 * 10^9

题目分析

到达每个格子的路径数等于上方和左方路径数之和。用一维 dp 按行滚动即可。

  • 建模:不同路径 可以先理解为“机器人只能向右或向下移动,求从左上到右下的不同路径数。”。先确定答案依赖的是下标、值、路径、窗口、集合还是 DP 状态,再决定数据结构。
  • 不变量:多维 DP 的状态通常同时绑定两个下标、二维位置或区间端点;初始化边界比转移本身更容易出错。
  • 边界:重点检查第一行/第一列、空字符串、长度为 1 的回文或编辑距离。
  • 正确性:dp[j] 更新后表示当前行第 j 列路径数。 只要每次更新后不变量仍成立,最终保留下来的状态就对应题目要求的结果。

解法:网格 DP 一维滚动

dp[j] 更新后表示当前行第 j 列路径数。

class Solution:
    def uniquePaths(self, m: int, n: int) -> int:
        dp = [1] * n
        for _ in range(1, m):
            for j in range(1, n):
                dp[j] += dp[j - 1]
        return dp[-1]

复杂度:时间 O(mn),空间 O(n)。

复杂度分析

  • 解法:网格 DP 一维滚动:时间 O(mn),矩阵或二维 DP 中每个格子会被访问或更新常数次。 空间 O(n),主要来自 DP 数组或滚动状态;若返回所有结果,输出本身不计入额外空间。
64最小路径和 Minimum Path Sum中等

题目规格

官方题意(改写):在非负网格中,只能向右或向下,求从左上到右下的最小路径和。 完整原题、样例和英文表述以本题官方链接为准。

输入:判题入口是 Solution.minPathSum(...),输入参数为 grid(List[List[int]])。

输出:返回 int;返回值含义是:在非负网格中,只能向右或向下,求从左上到右下的最小路径和。

约束要点
  • m == grid.length
  • n == grid[i].length
  • 1 <= m, n <= 200
  • 0 <= grid[i][j] <= 200

题目分析

dp[j] 表示当前行第 j 列的最小路径和,转移来自上方 dp[j] 和左方 dp[j-1] 的较小值。

  • 建模:最小路径和 可以先理解为“在非负网格中,只能向右或向下,求从左上到右下的最小路径和。”。先确定答案依赖的是下标、值、路径、窗口、集合还是 DP 状态,再决定数据结构。
  • 不变量:多维 DP 的状态通常同时绑定两个下标、二维位置或区间端点;初始化边界比转移本身更容易出错。
  • 边界:重点检查第一行/第一列、空字符串、长度为 1 的回文或编辑距离。
  • 正确性:先初始化第一行,再逐行更新。 只要每次更新后不变量仍成立,最终保留下来的状态就对应题目要求的结果。

解法:一维滚动 DP

先初始化第一行,再逐行更新。

class Solution:
    def minPathSum(self, grid: List[List[int]]) -> int:
        m, n = len(grid), len(grid[0])
        dp = [0] * n
        dp[0] = grid[0][0]
        for j in range(1, n):
            dp[j] = dp[j - 1] + grid[0][j]
        for i in range(1, m):
            dp[0] += grid[i][0]
            for j in range(1, n):
                dp[j] = min(dp[j], dp[j - 1]) + grid[i][j]
        return dp[-1]

复杂度:时间 O(mn),空间 O(n)。

复杂度分析

  • 解法:一维滚动 DP:时间 O(mn),矩阵或二维 DP 中每个格子会被访问或更新常数次。 空间 O(n),主要来自 DP 数组或滚动状态;若返回所有结果,输出本身不计入额外空间。
5最长回文子串 Longest Palindromic Substring中等

题目规格

官方题意(改写):求字符串中最长回文子串。 完整原题、样例和英文表述以本题官方链接为准。

输入:判题入口是 Solution.longestPalindrome(...),输入参数为 s(str)。

输出:返回 str;返回值含义是:求字符串中最长回文子串。

约束要点
  • 1 <= s.length <= 1000
  • s 仅由数字和英文字母组成

题目分析

回文中心可以是一个字符或两个字符之间的间隙。枚举中心向两侧扩展,记录最长区间。

  • 建模:最长回文子串 可以先理解为“求字符串中最长回文子串。”。先确定答案依赖的是下标、值、路径、窗口、集合还是 DP 状态,再决定数据结构。
  • 不变量:多维 DP 的状态通常同时绑定两个下标、二维位置或区间端点;初始化边界比转移本身更容易出错。
  • 边界:重点检查第一行/第一列、空字符串、长度为 1 的回文或编辑距离。
  • 正确性:分别处理奇数长度和偶数长度回文。 只要每次更新后不变量仍成立,最终保留下来的状态就对应题目要求的结果。

解法:中心扩展

分别处理奇数长度和偶数长度回文。

class Solution:
    def longestPalindrome(self, s: str) -> str:
        best_l = best_r = 0
        def expand(l, r):
            while l >= 0 and r < len(s) and s[l] == s[r]:
                l -= 1
                r += 1
            return l + 1, r - 1
        for i in range(len(s)):
            for l, r in (expand(i, i), expand(i, i + 1)):
                if r - l > best_r - best_l:
                    best_l, best_r = l, r
        return s[best_l:best_r + 1]

复杂度:时间 O(n^2),空间 O(1)。

复杂度分析

  • 解法:中心扩展:时间 O(n^2),需要枚举两个维度、中心扩展或区间端点,形成二次级状态/比较。 空间 O(1),只使用常数个指针、计数器或滚动变量;题目要求的输出不计入额外空间。
1143最长公共子序列 Longest Common Subsequence中等

题目规格

官方题意(改写):求两个字符串的最长公共子序列长度,子序列不要求连续。 完整原题、样例和英文表述以本题官方链接为准。

输入:判题入口是 Solution.longestCommonSubsequence(...),输入参数为 text1(str)、text2(str)。

输出:返回 int;返回值含义是:求两个字符串的最长公共子序列长度,子序列不要求连续。

约束要点
  • 1 <= text1.length, text2.length <= 1000
  • text1 和 text2 仅由小写英文字符组成

题目分析

dp[i][j] 表示 text1 前 i 个字符和 text2 前 j 个字符的 LCS 长度。若末字符相等,来自 dp[i-1][j-1]+1;否则取上方或左方最大值。

  • 建模:最长公共子序列 可以先理解为“求两个字符串的最长公共子序列长度,子序列不要求连续。”。先确定答案依赖的是下标、值、路径、窗口、集合还是 DP 状态,再决定数据结构。
  • 不变量:多维 DP 的状态通常同时绑定两个下标、二维位置或区间端点;初始化边界比转移本身更容易出错。
  • 边界:重点检查第一行/第一列、空字符串、长度为 1 的回文或编辑距离。
  • 正确性:prev 保存上一行左上角值。 只要每次更新后不变量仍成立,最终保留下来的状态就对应题目要求的结果。

解法:二维 DP 一维滚动

prev 保存上一行左上角值。

class Solution:
    def longestCommonSubsequence(self, text1: str, text2: str) -> int:
        n = len(text2)
        dp = [0] * (n + 1)
        for ch1 in text1:
            prev = 0
            for j, ch2 in enumerate(text2, 1):
                tmp = dp[j]
                if ch1 == ch2:
                    dp[j] = prev + 1
                else:
                    dp[j] = max(dp[j], dp[j - 1])
                prev = tmp
        return dp[n]

复杂度:时间 O(mn),空间 O(n)。

复杂度分析

  • 解法:二维 DP 一维滚动:时间 O(mn),矩阵或二维 DP 中每个格子会被访问或更新常数次。 空间 O(n),主要来自 DP 数组或滚动状态;若返回所有结果,输出本身不计入额外空间。
72编辑距离 Edit Distance中等

题目规格

官方题意(改写):求把 word1 转换成 word2 所需的最少插入、删除、替换操作数。 完整原题、样例和英文表述以本题官方链接为准。

输入:判题入口是 Solution.minDistance(...),输入参数为 word1(str)、word2(str)。

输出:返回 int;返回值含义是:求把 word1 转换成 word2 所需的最少插入、删除、替换操作数。

约束要点
  • 0 <= word1.length, word2.length <= 500
  • word1 和 word2 由小写英文字母组成

题目分析

dp[i][j] 表示 word1[:i] 到 word2[:j] 的编辑距离。末字符相同则继承左上;不同则从删除、插入、替换三种操作中取最小。

  • 建模:编辑距离 可以先理解为“求把 word1 转换成 word2 所需的最少插入、删除、替换操作数。”。先确定答案依赖的是下标、值、路径、窗口、集合还是 DP 状态,再决定数据结构。
  • 不变量:多维 DP 的状态通常同时绑定两个下标、二维位置或区间端点;初始化边界比转移本身更容易出错。
  • 边界:重点检查第一行/第一列、空字符串、长度为 1 的回文或编辑距离。
  • 正确性:一维滚动时 prev 保存左上角旧值。 只要每次更新后不变量仍成立,最终保留下来的状态就对应题目要求的结果。

解法:经典编辑距离 DP

一维滚动时 prev 保存左上角旧值。

class Solution:
    def minDistance(self, word1: str, word2: str) -> int:
        n = len(word2)
        dp = list(range(n + 1))
        for i, c1 in enumerate(word1, 1):
            prev = dp[0]
            dp[0] = i
            for j, c2 in enumerate(word2, 1):
                tmp = dp[j]
                if c1 == c2:
                    dp[j] = prev
                else:
                    dp[j] = 1 + min(dp[j], dp[j - 1], prev)
                prev = tmp
        return dp[n]

复杂度:时间 O(mn),空间 O(n)。

复杂度分析

  • 解法:经典编辑距离 DP:时间 O(mn),矩阵或二维 DP 中每个格子会被访问或更新常数次。 空间 O(n),主要来自 DP 数组或滚动状态;若返回所有结果,输出本身不计入额外空间。

技巧

本组 5 题,围绕 技巧 的核心模式展开。先识别不变量,再选择哈希、指针、递归、队列、堆或 DP 状态。

136只出现一次的数字 Single Number简单

题目规格

官方题意(改写):数组中除一个元素只出现一次外,其余每个元素都出现两次,找出只出现一次的元素。 完整原题、样例和英文表述以本题官方链接为准。

输入:判题入口是 Solution.singleNumber(...),输入参数为 nums(List[int])。

输出:返回 int;返回值含义是:数组中除一个元素只出现一次外,其余每个元素都出现两次,找出只出现一次的元素。

约束要点
  • 1 <= nums.length <= 3 * 10^4
  • -3 * 10^4 <= nums[i] <= 3 * 10^4
  • 除了某个元素只出现一次以外,其余每个元素均出现两次

题目分析

异或满足 x^x=0、x^0=x,并且交换结合。把所有数异或起来,成对元素抵消,剩下唯一元素。

  • 建模:只出现一次的数字 可以先理解为“数组中除一个元素只出现一次外,其余每个元素都出现两次,找出只出现一次的元素。”。先确定答案依赖的是下标、值、路径、窗口、集合还是 DP 状态,再决定数据结构。
  • 不变量:技巧题通常依赖位运算、投票、三路划分、排列字典序或函数图建模;关键是识别题目隐藏的不变量。
  • 边界:重点检查题目给定的特殊保证是否成立,例如多数元素一定存在、重复数唯一或数组值范围固定。
  • 正确性:不需要额外空间,顺序无关。 只要每次更新后不变量仍成立,最终保留下来的状态就对应题目要求的结果。

解法:异或抵消

不需要额外空间,顺序无关。

class Solution:
    def singleNumber(self, nums: List[int]) -> int:
        ans = 0
        for x in nums:
            ans ^= x
        return ans

复杂度:时间 O(n),空间 O(1)。

复杂度分析

  • 解法:异或抵消:时间 O(n),主循环中每个元素或节点只进入核心结构常数次。 空间 O(1),只使用常数个指针、计数器或滚动变量;题目要求的输出不计入额外空间。
169多数元素 Majority Element简单

题目规格

官方题意(改写):找出数组中出现次数超过一半的多数元素。 完整原题、样例和英文表述以本题官方链接为准。

输入:判题入口是 Solution.majorityElement(...),输入参数为 nums(List[int])。

输出:返回 int;返回值含义是:找出数组中出现次数超过一半的多数元素。

约束要点
  • n == nums.length
  • 1 <= n <= 5 * 10^4
  • -10^9 <= nums[i] <= 10^9
  • 输入保证数组中一定有一个多数元素

题目分析

Boyer-Moore 投票:把多数元素和其他元素两两抵消,最后留下的候选一定是多数元素。

  • 建模:多数元素 可以先理解为“找出数组中出现次数超过一半的多数元素。”。先确定答案依赖的是下标、值、路径、窗口、集合还是 DP 状态,再决定数据结构。
  • 不变量:技巧题通常依赖位运算、投票、三路划分、排列字典序或函数图建模;关键是识别题目隐藏的不变量。
  • 边界:重点检查题目给定的特殊保证是否成立,例如多数元素一定存在、重复数唯一或数组值范围固定。
  • 正确性:计数为 0 时更换候选,相同加一,不同减一。 只要每次更新后不变量仍成立,最终保留下来的状态就对应题目要求的结果。

解法:Boyer-Moore 投票

计数为 0 时更换候选,相同加一,不同减一。

class Solution:
    def majorityElement(self, nums: List[int]) -> int:
        candidate = None
        count = 0
        for x in nums:
            if count == 0:
                candidate = x
            count += 1 if x == candidate else -1
        return candidate

复杂度:时间 O(n),空间 O(1)。

复杂度分析

  • 解法:Boyer-Moore 投票:时间 O(n),主循环中每个元素或节点只进入核心结构常数次。 空间 O(1),只使用常数个指针、计数器或滚动变量;题目要求的输出不计入额外空间。
75颜色分类 Sort Colors中等

题目规格

官方题意(改写):原地排序只包含 0、1、2 的数组,使相同颜色相邻。 完整原题、样例和英文表述以本题官方链接为准。

输入:判题入口是 Solution.sortColors(...),输入参数为 nums(List[int])。

输出:函数原地修改输入对象,不需要返回新对象;修改后的数据应满足:原地排序只包含 0、1、2 的数组,使相同颜色相邻。

约束要点
  • n == nums.length
  • 1 <= n <= 300
  • nums[i] 为 0 、 1 或 2

题目分析

荷兰国旗问题。left 左侧全是 0,right 右侧全是 2,i 扫描未知区间。遇 0 换到左侧,遇 2 换到右侧且 i 不前进。

  • 建模:颜色分类 可以先理解为“原地排序只包含 0、1、2 的数组,使相同颜色相邻。”。先确定答案依赖的是下标、值、路径、窗口、集合还是 DP 状态,再决定数据结构。
  • 不变量:技巧题通常依赖位运算、投票、三路划分、排列字典序或函数图建模;关键是识别题目隐藏的不变量。
  • 边界:重点检查题目给定的特殊保证是否成立,例如多数元素一定存在、重复数唯一或数组值范围固定。
  • 正确性:维护 [0,left)、[left,i)、(right,n) 三个已知区域。 只要每次更新后不变量仍成立,最终保留下来的状态就对应题目要求的结果。

解法:三指针荷兰国旗

维护 [0,left)、[left,i)、(right,n) 三个已知区域。

class Solution:
    def sortColors(self, nums: List[int]) -> None:
        left, i, right = 0, 0, len(nums) - 1
        while i <= right:
            if nums[i] == 0:
                nums[left], nums[i] = nums[i], nums[left]
                left += 1
                i += 1
            elif nums[i] == 2:
                nums[i], nums[right] = nums[right], nums[i]
                right -= 1
            else:
                i += 1

复杂度:时间 O(n),空间 O(1)。

复杂度分析

  • 解法:三指针荷兰国旗:时间 O(n),主循环中每个元素或节点只进入核心结构常数次。 空间 O(1),只使用常数个指针、计数器或滚动变量;题目要求的输出不计入额外空间。
31下一个排列 Next Permutation中等

题目规格

官方题意(改写):将数组原地变成字典序中的下一个排列;若已经最大,则变成最小排列。 完整原题、样例和英文表述以本题官方链接为准。

输入:判题入口是 Solution.nextPermutation(...),输入参数为 nums(List[int])。

输出:函数原地修改输入对象,不需要返回新对象;修改后的数据应满足:将数组原地变成字典序中的下一个排列;若已经最大,则变成最小排列。

约束要点
  • 1 <= nums.length <= 100
  • 0 <= nums[i] <= 100

题目分析

从右找第一个下降位置 i,使 nums[i] < nums[i+1]。再从右找第一个大于 nums[i] 的数交换,最后反转 i+1 后缀使其最小。

  • 建模:下一个排列 可以先理解为“将数组原地变成字典序中的下一个排列;若已经最大,则变成最小排列。”。先确定答案依赖的是下标、值、路径、窗口、集合还是 DP 状态,再决定数据结构。
  • 不变量:技巧题通常依赖位运算、投票、三路划分、排列字典序或函数图建模;关键是识别题目隐藏的不变量。
  • 边界:重点检查题目给定的特殊保证是否成立,例如多数元素一定存在、重复数唯一或数组值范围固定。
  • 正确性:后缀原本是非递增,交换后反转即可得到最小后缀。 只要每次更新后不变量仍成立,最终保留下来的状态就对应题目要求的结果。

解法:字典序后缀调整

后缀原本是非递增,交换后反转即可得到最小后缀。

class Solution:
    def nextPermutation(self, nums: List[int]) -> None:
        i = len(nums) - 2
        while i >= 0 and nums[i] >= nums[i + 1]:
            i -= 1
        if i >= 0:
            j = len(nums) - 1
            while nums[j] <= nums[i]:
                j -= 1
            nums[i], nums[j] = nums[j], nums[i]
        left, right = i + 1, len(nums) - 1
        while left < right:
            nums[left], nums[right] = nums[right], nums[left]
            left += 1
            right -= 1

复杂度:时间 O(n),空间 O(1)。

复杂度分析

  • 解法:字典序后缀调整:时间 O(n),主循环中每个元素或节点只进入核心结构常数次。 空间 O(1),只使用常数个指针、计数器或滚动变量;题目要求的输出不计入额外空间。
287寻找重复数 Find the Duplicate Number中等

题目规格

官方题意(改写):长度为 n+1 的数组中每个数在 1..n 范围内,找出唯一重复的数,不能修改数组。 完整原题、样例和英文表述以本题官方链接为准。

输入:判题入口是 Solution.findDuplicate(...),输入参数为 nums(List[int])。

输出:返回 int;返回值含义是:长度为 n+1 的数组中每个数在 1..n 范围内,找出唯一重复的数,不能修改数组。

约束要点
  • 1 <= n <= 10^5
  • nums.length == n + 1
  • 1 <= nums[i] <= n
  • nums 中 只有一个整数 出现 两次或多次 ,其余整数均只出现 一次

题目分析

把 nums[i] 看成从 i 指向 nums[i] 的边,因为有重复值,函数图中存在环。重复数就是环入口,用 Floyd 算法寻找。

  • 建模:寻找重复数 可以先理解为“长度为 n+1 的数组中每个数在 1..n 范围内,找出唯一重复的数,不能修改数组。”。先确定答案依赖的是下标、值、路径、窗口、集合还是 DP 状态,再决定数据结构。
  • 不变量:技巧题通常依赖位运算、投票、三路划分、排列字典序或函数图建模;关键是识别题目隐藏的不变量。
  • 边界:重点检查题目给定的特殊保证是否成立,例如多数元素一定存在、重复数唯一或数组值范围固定。
  • 正确性:数组下标跳转构成链表,重复值对应入环点。 只要每次更新后不变量仍成立,最终保留下来的状态就对应题目要求的结果。

解法:Floyd 环入口

数组下标跳转构成链表,重复值对应入环点。

class Solution:
    def findDuplicate(self, nums: List[int]) -> int:
        slow = fast = nums[0]
        while True:
            slow = nums[slow]
            fast = nums[nums[fast]]
            if slow == fast:
                break
        slow = nums[0]
        while slow != fast:
            slow = nums[slow]
            fast = nums[fast]
        return slow

复杂度:时间 O(n),空间 O(1)。

复杂度分析

  • 解法:Floyd 环入口:时间 O(n),主循环中每个元素或节点只进入核心结构常数次。 空间 O(1),只使用常数个指针、计数器或滚动变量;题目要求的输出不计入额外空间。

复盘:把题目压缩成不变量

刷完一组后,回头复述每题的不变量:哈希表里存什么,指针为什么移动,递归返回什么,DP 状态表示什么。

检查清单

  • 哈希题先问:key 是值、下标、前缀和,还是结构签名。
  • 双指针题先问:移动哪一端可以安全丢弃状态。
  • 树和链表题先问:函数返回节点、布尔值、路径和,还是子树高度。
  • DP 题先问:状态覆盖的前缀、容量、区间或两个序列边界是什么。