跳过正文
重生之我还在做 HOT100
  1. posts/

重生之我还在做 HOT100

·20321 字·41 分钟·
陈驰水
作者
陈驰水
感谢您看到这里,祝您生活愉快
目录

HOT100 复习笔记:题干 + 思路 + 代码

HOT100 从 23 年开始零零散散做过很多遍。

但其中 trick 与脑筋急转弯比较多,还是很容易忘。

因此特意花了一周的时间重新全部做了一边,用了同一的思路和代码风格方便复习。

此外,还有些面试常见题也会写在后面。

和 ‘算法题 - 分类索引’ 有一定的重复。

HOT 100 的分类其实有点问题,暂时先按顺序写了,比如很多题的知识点和类别就关系不大。


此文章全部用 python,我也建议面试全部使用 python,原因如下:

  1. 现在很多面试会搓 torch,直接用 python 写代码题避免切换麻烦
  2. python 功能更强大且写的快,比如切片和 @cache 等
  3. 面试一般不要求大样本,python 的速度劣势体现不出来
  4. python 语法不严格,避免了面试看到编译错误紧张

哈希
#

1. 两数之和

题干:给一个数组,找的任意两数之和等于目标值

思路:一轮遍历,用哈希记录当前遍历过哪些数,然后当前元素查哈希表即可

class Solution:
    def twoSum(self, nums: List[int], target: int) -> List[int]:
        m = defaultdict(lambda : -1) # 哈希表缺省值
        n = len(nums)
        for i in range(n):
            if m[target - nums[i]] != -1:
                return [i, m[target - nums[i]]]
            m[nums[i]] = i
        return []

49. 字母异位词分组

题干:有字符串数组,对其中每个字符串按其中出现的元素分类

输入: strs = [“eat”, “tea”, “tan”, “ate”, “nat”, “bat”]

输出: [[“bat”],[“nat”,“tan”],[“ate”,“eat”,“tea”]]

思路:对每个字符串排序后,哈希 key 为排序后的值

class Solution:
    def groupAnagrams(self, strs: List[str]) -> List[List[str]]:
        n = len(strs)
        m = defaultdict(list)
        for i in range(n):
            tmp_s = ''.join(sorted(strs[i])) # 注意下 str 的 join 用法
            m[tmp_s].append(strs[i])
        ans = []
        for i, v in m.items():
            ans.append(v)
        return ans

128. 最长连续序列

题干:给一个数组,找的其中所有元素排序后最长的连续序列

输入:nums = [100,4,200,1,3,2] 输出:4 解释:最长数字连续序列是 [1, 2, 3, 4]。它的长度为 4。

思路:不能使用排序。用桶重构数组,遍历每个连续序列的首值,记录每个序列的长度。最终使数组中每个元素最多遍历一边。

# 桶 + 技巧
class Solution:
    def longestConsecutive(self, nums: List[int]) -> int:
        m = set(nums)
        ans = 0
        for x in m: # !注意此处是在 set 中遍历,在 nums 中遍历最差是 On2
            if x - 1 in m:
                continue
            xx = x
            length = 0
            while xx in m:
                length += 1
                xx += 1
            ans = max(ans, length)
        return ans

双指针
#

283. 移动零

题干:不改变数组顺序去情况下,将零全部移动到末尾,要求操作次数最少

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

思路:将一个指针视作栈顶,即目前有多少非零的数,栈顶一定指向零,当遍历到非零元素交换。

注意:和 荷兰国旗问题比较接近,但此题要求有序,因此解法不同。

class Solution:
    def moveZeroes(self, nums: List[int]) -> None:
        """
        Do not return anything, modify nums in-place instead.
        """
        n = len(nums)
        stack_size = 0
        for i in range(n):
            if nums[i] != 0:
                nums[i], nums[stack_size] = nums[stack_size], nums[i]
                stack_size += 1

11. 盛最多水的容器

题干:找数组两个“隔板”之间能容纳最多的水,注意不同与接雨水,不用考虑“隔板”本身的体积。

输入:[1,8,6,2,5,4,8,3,7] 输出:49

思路:双指针从两边向中间走,哪边数小就缩哪边

class Solution:
    def maxArea(self, height: List[int]) -> int:
        n = len(height)
        l = 0
        r = n - 1
        ans = 0
        while l < r:
            ans = max(ans, (r - l) * min(height[l], height[r]))
            if height[l] < height[r]:
                l += 1
            else:
                r -= 1
        return ans

15. 三数之和

题干:记录数组中所有 x + y + z == 0 的值,需要去重。

思路:排序后三指针,遍历 x 作为基数,由于 y 和 z 一定有序,还是从两边缩小到中间的思路。此外可以遍历过程中去重,也可以用 tuple 做 hash 的 key,后者思维量小。

class Solution:
    def threeSum(self, nums: List[int]) -> List[List[int]]:
        n = len(nums)
        nums.sort()
        ans = set()
        for i, x in enumerate(nums):
            l = i + 1
            r = n - 1
            while l < r:
                if nums[l] + nums[r] == -x:
                    ans.add((nums[i], nums[l], nums[r]))
                    # break 
                    # 注意下此处不能直接 break,因为同一个 i,可以对应不同的 l r 做答案
                    l += 1 
                    r -= 1
                elif nums[l] + nums[r] < -x:
                    l += 1
                else:
                    r -= 1
        res = []
        for x in ans:
            res.append([x[0], x[1], x[2]])
        return res

下面是循环中去重的代码,注意是在 i 循环 和 当满足条件的时候去跳过重复的,这样更好写

class Solution:
    def threeSum(self, nums: List[int]) -> List[List[int]]:
        nums.sort()
        ans = []
        n = len(nums)
        for i in range(n):
            if i != 0 and nums[i] == nums[i - 1]:
                continue
            j = i + 1
            k = n - 1
            x = nums[i]
            while j < k:
                # print(i, j, k)
                if x + nums[j] + nums[k] == 0:
                    ans.append([x, nums[j], nums[k]])
                    j += 1
                    k -= 1
                    while 0 < j < k and nums[j] == nums[j - 1]:
                        j += 1
                    while j < k < n - 1 and nums[k] == nums[k + 1]:
                        k -= 1
                if x + nums[j] + nums[k] < 0:
                    j += 1
                if x + nums[j] + nums[k] > 0:
                    k -= 1
        return ans

42. 接雨水

题干:过于经典,不赘述了

思路:

  1. 前缀后缀法:通法,思维量较小。统计每个数的最大前后缀
  2. 双指针实际上就说前缀后缀的优化版,核心就是哪侧前或后缀更小,就缩那测
  3. 单调递减栈,找上一个更大的,然后填坑
# 前缀后缀
class Solution:
    def trap(self, height: List[int]) -> int:
        n = len(height)
        pre = [0] * n
        pre[0] = height[0]
        suf = [0] * n
        suf[-1] = height[-1]
        for i in range(1, n):
            pre[i] = max(pre[i - 1], height[i])
        
        for i in range(n - 2, -1, -1):
            suf[i] = max(suf[i + 1], height[i])
        ans = 0
        for i in range(n):
            ans += (min(pre[i], suf[i]) - height[i])
        return ans
# 双指针,谁小谁移动
class Solution:
    def trap(self, height: List[int]) -> int:
        n = len(height)
        l = 0
        r = n - 1
        pre_max = height[l]
        suf_max = height[r]
        ans = 0
        while l < r:
            if pre_max < suf_max:
                ans += (pre_max - height[l])
                l += 1
            else:
                ans += (suf_max - height[r])
                r -= 1
            pre_max = max(pre_max, height[l]) 
            suf_max = max(suf_max, height[r])
        return ans
# 单调栈做法
class Solution:
    def trap(self, height: List[int]) -> int:
        stack = []
        n = len(height)
        ans = 0
        for i, x in enumerate(height):
            while stack and x >= height[stack[-1]]:
                bottom = height[stack[-1]]
                stack.pop()
                if not stack:
                    break
                l = stack[-1]
                h = min(height[l], x) - bottom
                ans += h * (i - l - 1)
            stack.append(i)
        return ans

滑动窗口
#

3. 无重复字符的最长子串

题干:找到数组中无重复字符的最长子串

思路:滑动窗口 + 哈希的模板题

class Solution:
    def lengthOfLongestSubstring(self, s: str) -> int:
        n = len(s)
        m = defaultdict(int)
        l = r = ans = 0
        while l <= r and r < n:
            m[s[r]] += 1
            # print(m)
            while l <= r and m[s[r]] != 1:
                # print(l, r, m)
                m[s[l]] -= 1
                l += 1
            ans = max(ans, r - l + 1)
            r += 1
        return ans

438. 找到字符串中所有字母异位词

题干:找到数组中所有 “目标字符 cnt ”等于”字串字符 cnt“ 的字串下标

思路:稍微变形的滑窗板子,注意当缩减左窗口后,字串长度与目标相同时,一定两者 cnt 相同。

# 滑动窗口板子
class Solution:
    def findAnagrams(self, s: str, p: str) -> List[int]:
        n = len(s)
        l = r = 0
        t = defaultdict(int)
        for i in list(p):
            t[i] += 1
        m = defaultdict(int)
        ans = []
        while l <= r and r < n:
            m[s[r]] += 1
            while l <= r and m[s[r]] > t[s[r]]: # 注意下缩左窗口的条件
                m[s[l]] -= 1
                l += 1
            # print(r, l, m)
            if r - l + 1 == len(p):
                ans.append(l)
            r += 1
        return ans

字串
#

560. 和为 K 的子数组

题干:找子串和为 K 的数量

思路:O1 求区间和 -> 需要前缀和 -> 哈希记录出现的指定前缀数量 -> 和两数之和思路一样

# 求区间和 -> 前缀和 -> 哈希记录出现的指定前缀数量
class Solution:
    def subarraySum(self, nums: List[int], k: int) -> int:
        n = len(nums)
        prefix = [0] * (n + 1)
        m = defaultdict(int)
        m[0] = 1
        ans = 0
        # prefix[x] - prefix[y] == k
        # 即找 m[prefix[i] - k] 的数量
        for i in range(1, n + 1):
            prefix[i] = prefix[i - 1] + nums[i - 1]
            ans += m[prefix[i] - k]
            m[prefix[i]] += 1
        return ans
         

239. 滑动窗口最大值

题干:找所有定长字串的最大值,要求 On

思路:经典单调队列,仅字串中保持递减的一个队列,右侧插入与单调栈一致。当左侧和首元素下标相同时,队列进行 O1 移除。从而使每个队列的首元素一定是当前字串的最大值。

单调队列相比单调栈出现的更少,注意其基本都是在栈中存下标,然后注意栈中的严格单调性。

此外,此题还有个 ST 表 做法,但复杂度不比单调队列好。

# 单调队列
class Solution:
    def maxSlidingWindow(self, nums: List[int], k: int) -> List[int]:
        arr = deque()
        n = len(nums)
        r = 0
        ans = []
        while r < n:
            # print(r, arr)
            if r >= k:
                ans.append(nums[arr[0]])
                if arr[0] == r - k: # 维护左下标
                    arr.popleft() 
            
            while arr and nums[r] >= nums[arr[-1]]: # 入队列操作和单调栈一样
                arr.pop()
            arr.append(r)
            r += 1
        ans.append(nums[arr[0]])
        return ans

76. 最小覆盖子串

题干:找到所有字串中,“目标串元素 cnt “ 能覆盖的”长度最小字串元素 cnt “是什么?

输入:s = “ADOBECODEBANC”, t = “ABC” 输出:“BANC”

思路:如果只要 O(52*n) 即滑窗的过程每次遍历单词表,那就是滑窗板子,和438. 找到字符串中所有字母异位词 完全一样。

但如果想 O(n) 还是有些难度,新提出一个 less 字符标志,判断单词表中有多少词仍未被覆盖,从而代替遍历单词表。

但要注意 less 的维护方法。

class Solution:
    def minWindow(self, s: str, t: str) -> str:
        n = len(s)
        l = r = 0
        target = defaultdict(int)
        for i in list(t):
            target[i] += 1
        m = defaultdict(int)

        less = len(target) # 开始时 target 中每个词都没被满足
        ans_l, ans_r = 0, inf
        while l <= r and r < n:
            m[s[r]] += 1
            if m[s[r]] == target[s[r]]: # 注意此处一定是等于,只想让 less 更新一次
                less -= 1

            while l <= r and less == 0: 
                # print(l, r, less, ans_l, ans_r)
                if (ans_r - ans_l) > (r - l): # 注意此时内层循环才满足答案吗要求
                    ans_l = l
                    ans_r = r
                m[s[l]] -= 1
                if m[s[l]] == target[s[l]] - 1:
                    less += 1 # 同样的,保证 less 只更新一次
                l += 1
            r += 1
        return s[ans_l:ans_r + 1] if ans_r != inf else ""

普通数组
#

53. 最大子数组和

题干:找字串中最大的 sum 是多少?

思路:前缀和 + 空间优化(也可以简单 DP)。记录前缀和的最小值即可,简单优化下就能空间 O1。

# 找个前缀就可以
class Solution:
    def maxSubArray(self, nums: List[int]) -> int:
        n = len(nums)
        prefix = 0
        pre_min = inf
        ans = -inf
        for i in range(1, n + 1):
            pre_min = min(prefix, pre_min)
            prefix = prefix + nums[i - 1]
            ans = max(ans, prefix - pre_min)
        return ans

56. 合并区间

题干:合并所有有重叠的区间

思路:对原始区间排序后,比较当前区间的右侧和下一个区间的左侧。大部分语言之间排就可以,小部分要写 lambda。

class Solution:
    def merge(self, intervals: List[List[int]]) -> List[List[int]]:
        intervals.sort()
        ans = []
        l = intervals[0][0]
        r = intervals[0][1]
        for x, y in intervals:
            if x <= r:
                r = max(y, r)
            else:
                ans.append([l, r])
                l = x
                r = y
        ans.append([l, r])
        return ans

189. 轮转数组

题干:将数组右移动 k 位

思路:三次反转,先转最大区间,再转两个小区间

提示:如果是左移,其实就等于右移 -k 位

class Solution:
    def rotate(self, nums: List[int], k: int) -> None:
        """
        Do not return anything, modify nums in-place instead.
        """
        # 自带的 reverse 会产生多余空间,自己用双指针搓更合适
        def reverse(l, r):
            while l < r:
                nums[l], nums[r] = nums[r], nums[l]
                l += 1
                r -= 1
        n = len(nums)
        k %= n
        reverse(0, n - 1)
        reverse(0, k - 1)
        reverse(k, n - 1)

238. 除自身以外数组的乘积

题干:获得每个数对应的除了他自己以外全部元素的乘积。

思路:明显的前后缀,题目要求只使用一个额外数组,先后缀后前缀直接得到答案就可以

# 前缀后缀积,最好用一个数组解决
class Solution:
    def productExceptSelf(self, nums: List[int]) -> List[int]:
        n = len(nums)
        suf = [1] * n
        for i in range(n - 1, 0, -1):
            suf[i - 1] = suf[i] * nums[i]
        # print(suf)
        
        pre = 1
        for i in range(n):
            suf[i] *= pre
            pre *= nums[i]
        return suf

41. 缺失的第一个正数

题干:找数组中没出现的最小正数,要求 O(n) 时间,O1 空间。

思路: 经典技巧题,想让每个符合条件的位置满足 nums[i] == nums[nums[i]]。

这样就相当于让位置 i 的 x 一定回到 x 的位置上,此外交换过程中要考虑 swap 依赖问题。

class Solution:
    def firstMissingPositive(self, nums: List[int]) -> int:
        n = len(nums)
        for i in range(n): # 对齐下标
            nums[i] -= 1
        for i in range(n):
            while 0 <= nums[i] < n and nums[i] != nums[nums[i]]:
                j = nums[i] # 要让 swap 不被依赖
                nums[i], nums[j] = nums[j], nums[i]
        # print(nums)
        for i in range(n):
            if nums[i] != i:
                return i + 1
        return n + 1

矩阵
#

73. 矩阵置零

题干:如果出现零,则将该 行/列 全部置零,要求 O1 空间复杂度。

思路:用原本的第一行和第一列的来记录本行或列是否要归零,用 bool 记录第一行或列自己是否要归零。

class Solution:
    def setZeroes(self, matrix: List[List[int]]) -> None:
        """
        Do not return anything, modify matrix in-place instead.
        """
        n = len(matrix)
        m = len(matrix[0])
        col_0 = False
        raw_0 = False
        # 用 bool 记录第一行或列自己是否要归零
        for i in range(n):
            if matrix[i][0] == 0:
                raw_0 = True
                break
        for i in range(m):
            if matrix[0][i] == 0:
                col_0 = True
                break
        # 用第一行和第一列的来记录本行或列是否要归零
        for i in range(n):
            for j in range(m):
                if matrix[i][j] == 0:
                    matrix[0][j] = 0
                    matrix[i][0] = 0
        # 从第二行列开始进行实际归零
        for i in range(1, n):
            for j in range(1, m):
                if matrix[i][0] == 0:
                    matrix[i][j] = 0
                if matrix[0][j] == 0:
                    matrix[i][j] = 0
        # 修改第一行列
        for i in range(n):
            if raw_0:
                matrix[i][0] = 0
        for i in range(m):
            if col_0:
                matrix[0][i] = 0

54. 螺旋矩阵

题干:顺时针向内旋转的遍历矩阵,输出遍历顺序。

思路:模拟题,记录每行列的起始和方向,特别是循环后要把位置挪到下一个的起始点。

# 走格子模拟,要注意些细节。注意要记录每行列的起始和方向。
class Solution:
    def spiralOrder(self, matrix: List[List[int]]) -> List[int]:
        direction = [[0, 1], [1, 0], [0, -1], [-1, 0]]
        index = 0
        ans = []
        m = len(matrix)
        n = len(matrix[0])
        px = py = 0
        left_m, right_m = 0, m
        left_n, right_n = 0, n
        while len(ans) != n * m:
            # print(ans, px, py, left_m, right_m, left_n, right_n)
            dx = direction[index][0]
            dy = direction[index][1]
            while left_m <= px < right_m and left_n <= py < right_n:
                # print(ans, px, py, left_m, right_m, left_n, right_n)
                ans.append(matrix[px][py])
                px += dx
                py += dy
            px -= dx # 循环后要把位置挪到下一个的起始点
            py -= dy
            ne = (index + 1) % 4
            px += direction[ne][0]
            py += direction[ne][1]
            if index == 0: # 维护边界
                left_m += 1
            if index == 1:
                right_n -= 1
            if index == 3:
                left_n += 1
            if index == 2:
                right_m -= 1
            index = (index + 1) % 4

        return ans

48. 旋转图像

题干:矩阵原地顺时针旋转 90°

思路:

顺时针旋转90°,等于 先沿主对角线反转 + 后左右反转。

逆时针选择90°,等于 先沿主对角线反转 + 后上下反转。

旋转 180°,等于 左右反转 + 上下反转

class Solution:
    def rotate(self, matrix: List[List[int]]) -> None:
        """
        Do not return anything, modify matrix in-place instead.
        """
        m, n = len(matrix), len(matrix[0])
        for i in range(m):
            for j in range(i + 1, n):
                matrix[i][j], matrix[j][i] = matrix[j][i], matrix[i][j]
        # print(matrix)

        mid = n // 2
        for i in range(m):
            for j in range(mid):
                matrix[i][j], matrix[i][n-j-1] = matrix[i][n-j-1], matrix[i][j]

240. 搜索二维矩阵 II

题干:矩阵每行从左到右升序,每列从上到下升序,找目标元素

思路:和二分查找树一样,注意右上角是根

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

链表
#

160. 相交链表

题干:找两条链表相交的第一个节点(相交后一直相交),注意其不一定相交。要求时间 O(m+n) 空间 O(1)。

思路:同时遍历两条链表,如果遍历到空,则从另一条头开始遍历,直到两者相遇。(和经典的 O1 空间判断环入口的思路其实一样)

原理:设两条链表的重复部分为 Z,则 链表a 为 X+Z;链表b 为 Y+Z。让两者都走 X+Y+Z,相撞一定在 Z 的入口。不相交就是 Z == 0 的情况。

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

206. 反转链表

思路:分成递归和迭代,建议直接记住吧

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

234. 回文链表

题干:空间 O1 的判断一个链表是否是回文链表

思路:快慢指针判断中点,对后半段反转,之后双指针判断是否前后一致

# Definition for singly-linked list.
# class ListNode:
#     def __init__(self, val=0, next=None):
#         self.val = val
#         self.next = next
class Solution:
    def isPalindrome(self, head: Optional[ListNode]) -> bool:
        # 快慢指针找中点
        fast = slow = head
        while fast and fast.next:
            slow = slow.next
            fast = fast.next.next
        def reverse(head):
            cur = head
            pre = None
            while cur:
                tmp = cur.next
                cur.next = pre
                pre = cur
                cur = tmp
            return pre
        # 反转后半段
        right = reverse(slow)
        left = head
        # 判断前后是否相同
        while right:
            if left.val != right.val:
                return False
            left = left.next
            right = right.next
        return True

141. 环形链表

题干:链表判断有环,典中典

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

更加优雅的版本

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 fast is slow:  # 兔子追上乌龟(套圈),说明有环
                return True
        return False  # 访问到了链表末尾,无环

142. 环形链表 II

题干:空间 O1 找环入口

思路:先找快慢指针相撞的位置,之后将快指针挪到头,之后和慢指针一步步走,再次相撞就是环入口

原理:假设进环前的路程为 a,环长为 b。设慢指针走了 x 步时,快慢指针相遇,此时快指针走了 2x 步。显然 2x-x=kb(快指针比慢指针多走了 k 圈),即 x=kb。也就是说慢指针总共走过的路程是 kb,但这 kb 当中,实际上包含了进环前的一个小 a,因此慢指针在环中只走了 kb-a 步,再往前走 a 步,就得到完整的 k 圈,即环入口。

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

        if not fast or not fast.next:
            return
        
        fast = head
        while fast != slow:
            fast = fast.next
            slow = slow.next

        return fast

21. 合并两个有序链表

题干:两个有序链表合成一个有序链表

思路:用一个 pre 节点来连整个链表,两条链表的节点谁小谁移动。注意最后剩下的也要连到后面。

class Solution:
    def mergeTwoLists(self, list1: Optional[ListNode], list2: Optional[ListNode]) -> Optional[ListNode]:
        prehead = ListNode(-1)
        pre = prehead # 用一个 pre 来链接链表
        cur1 = list1
        cur2 = list2
        while cur1 and cur2:
            if cur1.val <= cur2.val: # 谁小谁移动
                pre.next = cur1
                cur1 = cur1.next
                pre = pre.next
            else:
                pre.next = cur2
                cur2 = cur2.next
                pre = pre.next
        if cur1:
            pre.next = cur1
        else:
            pre.next = cur2
        return prehead.next

2. 两数相加

题干:将两个链表诸位相加,大于10则进一位,返回相加完的最终链表。

思路:以其中一条链表为基准,另一条逐位加到上面,当出现长度不同和进位问题时,延长该链表。

class Solution:
    def addTwoNumbers(self, l1: Optional[ListNode], l2: Optional[ListNode]) -> Optional[ListNode]:
        cur1 = l1
        cur2 = l2
        # 将 cur1 的值加到 cur2 中,注意如果 cur1 更长或者有进位,需要加节点
        while cur1 and cur2: 
            if not cur2.next and cur1.next:
                cur2.next = ListNode(0)
            cur2.val += cur1.val
            if cur2.val >= 10:
                cur2.val %= 10
                if cur2.next:
                    cur2.next.val += 1 
                else:
                    node = ListNode(1)
                    cur2.next = node
            cur2 = cur2.next
            cur1 = cur1.next
        # 如果 cur2 最后还是大于 10,则反复加节点
        while cur2 and cur2.val >= 10:
            cur2.val %= 10
            if cur2.next:
                cur2.next.val += 1 
            else:
                node = ListNode(1)
                cur2.next = node
            cur2 = cur2.next
        return l2

19. 删除链表的倒数第 N 个结点

题干:一次遍历删除倒数第 N 个结点。

思路:快指针先走 N 步,然后慢指针启动,当快指针到头,慢指针就是倒数第 N 个。

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

24. 两两交换链表中的节点

题干:将每相邻的两个节点反转,后续不足两个的不变。

思路:用三个指针模拟 pre, cur, nx ,其中反转 cur 和 nx 即可。

class Solution:
    def swapPairs(self, head: Optional[ListNode]) -> Optional[ListNode]:
        prehead = ListNode(-1)
        pre = prehead
        pre.next = head
        cur = head
        while cur:
            nx = cur.next
            if not nx:
                return prehead.next
            pre.next = nx
            tmp = nx.next
            nx.next = cur
            cur.next = tmp
            
            pre = cur
            cur = cur.next
        return prehead.next

25. K 个一组翻转链表

题干:将连续的 k 个链表反转,后续不足 k 的不变。

思路:

  1. 实现 start 到 tail 的链表反转,实现如上述 206. 反转链表
  2. 用 pre 指针和 nx 指针维护反转后的链表连接,参考 24. 两两交换链表中的节点
class Solution:
    def reverseKGroup(self, head: Optional[ListNode], k: int) -> Optional[ListNode]:
        guard = ListNode(-1)
        guard.next = head
        def reverse(head: Optional[ListNode], k: int):
            cur = head
            pre = None
            kk = k
            while cur and k:
                k -= 1
                tmp = cur.next
                cur.next = pre
                pre = cur
                cur = tmp
            # print(pre, head)
            return pre, head

        cur = head
        pre = guard
        while cur:
            tail = cur
            for i in range(k - 1):
                tail = tail.next
                if not tail:
                    return guard.next
            tail = tail.next
            l, r = reverse(cur, k)
            pre.next = l
            r.next = tail
            # print(tail)
            pre = r
            cur = tail

        return guard.next

138. 随机链表的复制

题干:有一条链表在 next 之外,每个节点多存一个指针 random,其可能指向该链表的任意位置或空。要求深拷贝这条链表。

思路:

  1. 由于新链表在创建过程中,random 可能指向还没创建的链表,因此需要想办法存新链表的每个节点,然后至少两轮遍历。
  2. 如果用哈希表,空间复杂度 O(N),即一轮创建新节点,第二轮连接新节点。哈希表的 key 是 node 地址,value 也是 node 地址。
  3. 也可以用另一种方法,在每个 cur 后面存对应的新 cur,相当于用 链表 模拟出了一种哈希,如果返回值不记录复杂度,则可以视作 空间复杂度 O1。
class Solution:
    def copyRandomList(self, head: 'Optional[Node]') -> 'Optional[Node]':
        m = defaultdict(Node)
        cur = head
        pre = Node(-1)
        pre.next = cur
        while cur:
            new_node = Node(cur.val, None, None)
            m[cur] = new_node
            cur = cur.next
        cur = head
        print(m[head])
        while cur:
            if cur.next:
                m[cur].next = m[cur.next]
            if cur.random:
                m[cur].random = m[cur.random]
            cur = cur.next
        return m[head] if head else None

148. 排序链表

题干:根据值对链表排序

思路:归并排序用链表实现,分成递归和迭代,递归空间复杂度 O(logN),迭代法是 O1

# 递归法更好写
class Solution:
    def sortList(self, head: Optional[ListNode]) -> Optional[ListNode]:
        # 合并链表
        def mergeList(head1, head2):
            prehead = ListNode(-1)
            pre = prehead
            cur1 = head1
            cur2 = head2
            while cur1 and cur2:
                if cur1.val <= cur2.val:
                    pre.next = cur1
                    cur1 = cur1.next
                else:
                    pre.next = cur2
                    cur2 = cur2.next
                pre = pre.next
            pre.next = cur1 if cur1 else cur2
            return prehead.next
        
        def merge_sort(head, tail):
            if not head:
                return head
            if head.next == tail:
                head.next = None # 注意遍历到 tail 时要断开链表
                return head
            
            fast = head
            slow = head
            while fast != tail and fast.next != tail:
                slow = slow.next
                fast = fast.next.next
            
            left = merge_sort(head, slow)
            right = merge_sort(slow, tail)
            return mergeList(left, right)

        return merge_sort(head, None)
# 细节还是很多的
class Solution:
    def sortList(self, head: Optional[ListNode]) -> Optional[ListNode]:
        # 合并链表,要返回合并后的 head 和 tail
        def mergeList(head1, head2):
            prehead = ListNode(-1)
            pre = prehead
            cur1 = head1
            cur2 = head2
            while cur1 and cur2:
                if cur1.val <= cur2.val:
                    pre.next = cur1
                    cur1 = cur1.next
                else:
                    pre.next = cur2
                    cur2 = cur2.next
                pre = pre.next
            pre.next = cur1 if cur1 else cur2
            while pre.next:
                pre = pre.next
            return prehead.next, pre 
        
        def getlenght(head) -> int:
            cur = head
            ans = 0
            while cur:
                cur = cur.next
                ans += 1
            return ans
        # 分割 head 链表的第 k 个节点,返回分割后的头
        def split(head, k) -> ListNode: 
            # 这里不能复用 getlength,否则会导致 logN 退化为 N2 
            # length = getlenght(head)
            cur = head        
            for _ in range(k - 1):
                if cur == None:
                    return None
                cur = cur.next
            if not cur or not cur.next:
                return None
            tmp = cur.next
            cur.next = None
            return tmp
        # 倍增法实现迭代归并排序
        prehead = ListNode(next=head)
        step = 1
        length = getlenght(head)
        while step < length:
            pre = prehead # 记录需要合并链表的 pre
            cur = prehead.next
            while cur:
                head1 = cur # 两个 head 是切下来的两个 list
                head2 = split(head1, step)
                cur = split(head2, step) # cur 是合并后的 next
                newhead, newtail = mergeList(head1, head2)
                # print(newhead, newtail, step, cur)
                pre.next = newhead 
                newtail.next = cur 
                pre = newtail # pre 编程 merge 后的 tail
            step *= 2 # 倍增
        return prehead.next

23. 合并 K 个升序链表

题干:之前是合并 两个链表,现在合并 k 个链表。共有 L 个元素,则时间复杂度最好 O(L log k)。

思路:

  1. 堆法,用 val 对做排序,把每个链表的头堆中,如果最小则弹出该节点,然后节点向后移动一次,由于原始列表有序,则包装栈中一定是最小的,空间复杂度 O(K)。
  2. 递归分治,和上面的归并排序差不多,不过这次的是对 k 进行分治。空间复杂度 O(logK)
  3. 倍增法分治,同样的用 step 对 k 分治,空间复杂度 O(1)
class Solution:
    def mergeKLists(self, lists: List[Optional[ListNode]]) -> Optional[ListNode]:
        def mergeList(head1, head2):
            prehead = ListNode(-1)
            pre = prehead
            cur1 = head1
            cur2 = head2
            while cur1 and cur2:
                if cur1.val <= cur2.val:
                    pre.next = cur1
                    cur1 = cur1.next
                else:
                    pre.next = cur2
                    cur2 = cur2.next
                pre = pre.next
            pre.next = cur1 if cur1 else cur2
            while pre.next:
                pre = pre.next
            return prehead.next
        # 倍增法
        n = len(lists)
        step = 1
        while step < n:
            for i in range(0, n - step, step * 2):
                lists[i] = mergeList(lists[i], lists[i + step])
            step *= 2
        return lists[0] if lists else None

146. LRU 缓存

题干:实现一个 LRU 缓存,需要有考虑 cache 大小,有 get 和 put 操作。其中如果 cache 满了,则删掉最久没用到的节点,如果 put 一个已有的 node 则相当于 update。此外,每个节点有 key 。

思路:

  1. 双向链表 + 哈希表。链表头代表最近使用,用 map 来 O1 找到节点的位置,并记录链表长度。

  2. 用哨兵来维护头和尾,哨兵的 next 是 head, 哨兵的 pre 是 tail。

  3. get 需要两步,先找到节点,再把节点挪到头。

  4. put 先判断是不是 update,如果不是,根据 cache 容量来创建新 node,并可能删除 tail

class Node:
    def __init__(self, key=0, value=0, pre=None, next=None):
        self.key = key
        self.value = value
        self.pre = pre
        self.next = next
class LRUCache:
    def __init__(self, capacity: int):
        self.capacity = capacity
        # 双向链表哑节点一定在固定的 head
        # 则 dummy.pre 就是 tail 节点
        self.dummy = Node()
        self.dummy.next = self.dummy
        self.dummy.pre = self.dummy
        self.map = defaultdict(lambda : -1)

    # 将 node 挪到链表头
    # 注意不管是挪节点还是 remove,都只对指针操作,创建节点在 put 中
    def node_front(self, node):
        if node == None:
            return
        node.pre = self.dummy
        node.next = self.dummy.next
        node.next.pre = node
        node.pre.next = node

    # 删去 node
    def remove(self, node):
        node.pre.next = node.next
        node.next.pre = node.pre

    # 用 map 查找节点是否在链表中,同时要注意维护 map
    # 如何有,则将其挪到最前
    def get_node(self, key):
        if self.map[key] == -1:
            del self.map[key]
            return None
        node = self.map[key]
        self.remove(node)
        self.node_front(node)
        return node
    # 用 get_node 返回 value
    def get(self, key: int) -> int:
        node = self.get_node(key)
        return node.value if node else -1
    # 如果当前节点已经在链表中,相当于查找更新并
    # 否则相当于添加节点,则需要维护 map
    def put(self, key: int, value: int) -> None:
        node = self.get_node(key)
        if node:
            node.value = value
        else:
            new_node = Node()
            new_node.value = value
            new_node.key = key
            self.map[key] = new_node
            self.node_front(new_node)
            if len(self.map) > self.capacity:
                last_node = self.dummy.pre
                del self.map[last_node.key]
                self.remove(last_node)

二叉树
#

94. 二叉树的中序遍历

# 别忘了怎么构建 TreeNode 就行
# class TreeNode:
#     def __init__(self, val=0, left=None, right=None):
#         self.val = val
#         self.left = left
#         self.right = right
class Solution:
    def inorderTraversal(self, root: Optional[TreeNode]) -> List[int]:
        ans = []
        def dfs(node):
            if node == None:
                return node
            dfs(node.left)
            ans.append(node.val)
            dfs(node.right)
        dfs(root)
        return ans

104. 二叉树的最大深度

思路:层级遍历

class Solution:
    def maxDepth(self, root: Optional[TreeNode]) -> int:
        if root == None:
            return 0
        queue = deque()
        queue.append(root)
        ans = 0
        while queue:
            ans += 1
            k = len(queue)
            for i in range(k):
                tmp = queue[0]
                queue.popleft()
                if tmp.left:
                    queue.append(tmp.left)
                if tmp.right:
                    queue.append(tmp.right)
        return ans

226. 翻转二叉树

题干:将二叉树沿中轴线 左右反转。

思路:dfs 时候交换左右指针即可。

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

101. 对称二叉树

题干:判断二叉树是否左右对称

思路:递归稍微有一点难度,检查左右节点的关系

class Solution:
    def isSymmetric(self, root: Optional[TreeNode]) -> bool:
        if not root:
            return True
        def check(l, r):
            if not l and not r: # 到叶子了
                return True
            if not l or not r or l.val != r.val:
                return False
            return check(l.left, r.right) and check(l.right, r.left)
        return check(root.left, root.right)

543. 二叉树的直径

题干:直径指的是 树中任意两个节点之间最长路径的长度,注意其未必经过 root

思路:遍历每个点深度,左右子树深度差的最大值就是直径

# 把下面两个写在一个函数就是一轮遍历
class Solution:
    def diameterOfBinaryTree(self, root: Optional[TreeNode]) -> int:
        m = defaultdict(int)
        def depth(node):
            if not node:
                m[node] = 0
                return 0
            de = max(depth(node.left), depth(node.right)) + 1
            m[node] = de
            return de   
        depth(root)
        # print(m)
        ans = 0
        def dfs(node):
            nonlocal ans
            if not node:
                return
            ans = max(ans, m[node.left] + m[node.right])
            dfs(node.left)
            dfs(node.right)
        dfs(root)
        return ans 

102. 二叉树的层序遍历

class Solution:
    def levelOrder(self, root: Optional[TreeNode]) -> List[List[int]]:
        if root == None:
            return []
        queue = deque()
        queue.append(root)
        ans = []
        while queue:
            k = len(queue)
            res = []
            for i in range(k):
                tmp = queue[0]
                res.append(tmp.val)
                queue.popleft()
                if tmp.left:
                    queue.append(tmp.left)
                if tmp.right:
                    queue.append(tmp.right)
            ans.append(res)
        return ans

108. 将有序数组转换为二叉搜索树

题干:用有序数组创建一颗 二叉查找树,要求平衡(左右深度不超过1)

思路:二分分治,每次用 mid 做根,左右二分做左右子树

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

98. 验证二叉搜索树

思路:注意只查看左右子树是否为满足二叉搜索性质是错的,因为二叉搜索树要求左边全部元素小于跟。

具体思路很多:

  1. 由于中跟遍历是有序数组,因此只需要维护中跟的前一个数,然后判断当前是否比前面大即可。
  2. 前跟或后跟 遍历同时记录最大最小值,判断是否满足。
# 中跟
class Solution:
    def isValidBST(self, root: Optional[TreeNode]) -> bool:
        if not root:
            return True
        pre = -inf # pre 是中跟遍历的上一个元素
        def dfs(node) -> bool:
            nonlocal pre
            if not node:
                return True
            l_ans = dfs(node.left) # 中跟遍历
            if pre >= node.val: # 如果当前小于上个元素就错了
                return False
            pre = node.val
            return l_ans and dfs(node.right)
        return dfs(root) 

230. 二叉搜索树中第 K 小的元素

思路:中跟遍历到的第 K 个就是答案

思考题:如果要频繁查找第 K 个,可以预处理每个节点下面的子节点数量,从而在一棵平衡树中做到查找 O(logN)。

class Solution:
    def kthSmallest(self, root: Optional[TreeNode], k: int) -> int:
        if not root:
            return -1
        ans = -1
        def dfs(node):
            nonlocal ans, k
            if not node:
                return
            dfs(node.left)
            k -= 1
            if k == 0:
                ans = node.val
                return # 提前返回
            dfs(node.right)
        dfs(root)
        return ans

199. 二叉树的右视图

题干:从右向左看一颗二叉树,返回看到的 list

思路:层级遍历记录最后一个值。

class Solution:
    def rightSideView(self, root: Optional[TreeNode]) -> List[int]:
        if root == None:
            return []
        queue = deque()
        queue.append(root)
        ans = []
        while queue:
            res = -1
            k = len(queue)
            for i in range(k):
                tmp = queue[0]
                res = tmp.val
                queue.popleft()
                if tmp.left:
                    queue.append(tmp.left)
                if tmp.right:
                    queue.append(tmp.right)
            ans.append(res)
        return ans

114. 二叉树展开为链表

题干:将链表用前跟遍历的顺序连接成链表,用树中左指针置空,右指针作为链表 next 来实现,要求原地修改。

思路:

  1. 按 右左跟 的后跟遍历是正常前跟遍历的 reverse,因此可以从后向前构建链表。
  2. 分治法,维护左右子链表的最后一个元素,用最后一个元素连接。前跟遍历即可。
# 逆序头插法
class Solution:
    def flatten(self, root: Optional[TreeNode]) -> None:
        """
        Do not return anything, modify root in-place instead.
        """
        head = None
        def dfs(node):
            nonlocal head
            if node == None:
                return
            dfs(node.right)
            dfs(node.left)
            node.left = None
            node.right = head
            head = node
        dfs(root)
        return head
# 分治法
class Solution:
    def flatten(self, root: Optional[TreeNode]) -> None:
        if not root:
            return None
        
        def dfs(node) -> TreeNode:
            if not node:
                return None
            l_tail = dfs(node.left)
            r_tail = dfs(node.right)
            if l_tail: # 连接左右两个子链表
                l_tail.right = node.right
                node.right = node.left
                node.left = None
            if r_tail:
                return r_tail
            elif l_tail:
                return l_tail
            else:
                return node
        return dfs(root)

105. 从前序与中序遍历序列构造二叉树

思路:递归构造二叉树,输入是前序和中序的左右边界(共四个边界),每轮前序的第一个元素是下一轮的根,用 hash O1 查到 inorder 中的位置,然后就能推出四个边界。

class Solution:
    def buildTree(self, preorder: List[int], inorder: List[int]) -> Optional[TreeNode]:
        m = defaultdict(int)
        n = len(preorder)
        for i in range(len(inorder)):
            m[inorder[i]] = i
        if len(preorder) == 0:
            return None
        # root = TreeNode(preorder[0])
        def dfs(lp, rp, li, ri):
            nonlocal m, preorder, inorder
            if lp > rp or li > ri:
                return None
            node = TreeNode(preorder[lp])
            x = m[node.val]
            length_l = x - li
            length_r = ri - x

            node.left = dfs(lp + 1, lp + length_l, li, x - 1)
            node.right = dfs(lp + length_l + 1, rp, x + 1, ri)
            return node
        
        return dfs(0, n - 1, 0, n - 1)

437. 路径总和 III

题干:二叉树里节点值之和等于 targetSum 的 路径 的数目。路径指任意两节点相连的过程,此题中只考虑从上往下的路径。

思路:和 560. 和为 K 的子数组 一样用前缀和,但此处由于树的递归操作,因此要在左右递归后恢复现场,比较综合的题。

class Solution:
    def pathSum(self, root: Optional[TreeNode], targetSum: int) -> int:
        prefix = defaultdict(int)
        prefix[0] = 1
        num = 0
        ans = 0
        def dfs(node):
            nonlocal prefix, num, targetSum, ans
            if not node:
                return None
            # print(node.val, prefix, num, ans)
            num += node.val
            ans += prefix[num - targetSum]
            prefix[num] += 1
            dfs(node.left) # 注意此题是要等左右都遍历完才恢复现场
            dfs(node.right)
            prefix[num] -= 1
            num -= node.val
        dfs(root)
        return ans

236. 二叉树的最近公共祖先

题干:找二叉树两点的最近祖先

思路:用后跟遍历 find 判断最近公共祖先。

由于后跟遍历到 node 时,已知其下面的情况,如果找不到两节点中的任意一个,则代表其不是祖先,返回找到的那个。

如果该节点左右都能找到节点,说明他就是答案,返回自己,而其父节点一定只能找到一边,最终返回该答案。

此题思路相当优雅,值得多看。

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

124. 二叉树中的最大路径和

题干:找路径中最大的,注意会出现负数。此题路径距离在 node 上的 val。

思路:depth 记录包含该点的最大值,第二轮遍历中,此点最大路径就是 左右 depth + 该点值。也可以用一轮遍历。

注意:存在大量负数,需要每次保证 max(depth, 0)

class Solution:
    def maxPathSum(self, root: Optional[TreeNode]) -> int:
        m = defaultdict(lambda : 0)
        def depth(node):
            nonlocal m
            if not node:
                return 0
            m[node] = max(max(depth(node.left), 0), max(depth(node.right), 0)) + node.val
            return m[node]
        depth(root)
        # print(m[root])   
        ans = -inf
        def dfs(node):
            nonlocal m, ans
            if not node:
                return
            ans = max(ans, max(m[node.left], 0) + max(m[node.right], 0) + node.val)
            dfs(node.left)
            dfs(node.right)
        dfs(root)
        return ans

图论
#

200. 岛屿数量

题干:

给你一个由 '1'(陆地)和 '0'(水)组成的的二维网格,请你计算网格中岛屿的数量。

岛屿总是被水包围,并且每座岛屿只能由水平方向和/或竖直方向上相邻的陆地连接形成。

样例:

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

思路:DFS,BFS 都可以,最简单的图

class Solution:
    def numIslands(self, grid: List[List[str]]) -> int:
        direction = [[0, 1], [0, -1], [1, 0], [-1, 0]]
        m = len(grid)
        n = len(grid[0])
        def dfs(i: int, j: int) -> None:
            if i < 0 or i >= m or j < 0 or j >= n or grid[i][j] != '1':
                return
            grid[i][j] = '0' # 淹了陆地
            dfs(i, j - 1)
            dfs(i, j + 1)
            dfs(i - 1, j)
            dfs(i + 1, j)
        ans = 0
        for i in range(m):
            for j in range(n):
                if grid[i][j] == '1':
                    dfs(i, j)
                    ans += 1
        return ans

994. 腐烂的橘子

题干:

在给定的 m x n 网格 grid 中,每个单元格可以有以下三个值之一:

  • 0 代表空单元格;
  • 1 代表新鲜橘子;
  • 2 代表腐烂的橘子。

每分钟,腐烂的橘子 周围 4 个方向上相邻 的新鲜橘子都会腐烂。

返回 直到单元格中没有新鲜橘子为止所必须经过的最小分钟数。如果不可能,返回 -1

# BFS 遍历
class Solution:
    def orangesRotting(self, grid: List[List[int]]) -> int:
        directionX = [1, 0, -1, 0]
        directionY = [0, 1, 0, -1]
        q = deque()
        m = len(grid)
        n = len(grid[0])
        for i in range(0, m):
            for j in range(0, n):
                if grid[i][j] == 2:
                    q.append([i, j])

        # 此题初始状态不算第一步
        ans = -1 if q else 0
        while q:
            k = len(q)
            ans += 1
            for _ in range(k):
                temp = q[0]
                q.popleft()
                for i in range(4):
                    dx = temp[0] + directionX[i]
                    dy = temp[1] + directionY[i]
                    if 0 <= dx < m and 0 <= dy < n and grid[dx][dy] == 1:
                        grid[dx][dy] = 2
                        q.append([dx, dy])
        # 如果还有走不到的橘子
        for i in range(0, m):
            for j in range(0, n):
                if grid[i][j] == 1:
                    return -1
        return ans

207. 课程表

题干:找图中是否由环

# 拓扑排序
class Solution:
    def canFinish(self, numCourses: int, prerequisites: List[List[int]]) -> bool:
        grid = [[] for _ in range(numCourses)]
        indegree = [0] * numCourses
        for x, y in prerequisites:
            grid[y].append(x)
            indegree[x] += 1
        # print(grid)
        queue = deque()
        for i in range(numCourses):
            if indegree[i] == 0:
                queue.append(i)
        while queue:
            node = queue[0]
            queue.popleft()
            for to in grid[node]:
                indegree[to] -= 1
                if indegree[to] == 0:
                    queue.append(to)
        for i in range(numCourses):
            if indegree[i] != 0:
                return False
        return True

208. 实现 Trie (前缀树)

思路:多维护一个 isEnd 即可,其他都是工程问题

N = 26 
class Trie:
    def __init__(self):
        # isEnd 判断每个单词的结尾
        # next 数组表示该字符可能的下一个字符
        self.isEnd = False
        self.next = [None] * N

    def insert(self, word: str) -> None:
        node = self
        for c in word:
            index = ord(c) - ord('a')
            if not node.next[index]:
                node.next[index] = Trie()
            node = node.next[index]
        # 标记每个单词的结尾
        node.isEnd = True

    def search(self, word: str) -> bool:
        node = self
        for c in word:
            # 先前进再判空
            index = ord(c) - ord('a')
            node = node.next[index]
            if not node:
                return False
        # 遍历到底 但不是结尾
        return node.isEnd

    def startsWith(self, prefix: str) -> bool:
        node = self
        for c in prefix:
            index = ord(c) - ord('a')
            node = node.next[index]
            if not node:
                return False
        # 判读前缀不用管 isEnd
        return True

回溯
#

46. 全排列

输入:nums = [1,2,3]
输出:[[1,2,3],[1,3,2],[2,1,3],[2,3,1],[3,1,2],[3,2,1]]
# 用 path 维护选了哪些,每轮遍历 nums
class Solution:
    def permute(self, nums: List[int]) -> List[List[int]]:
        ans = []
        n = len(nums)
        def dfs(i: int, path: list):
            nonlocal ans, n
            if i == n:
                ans.append(path.copy())
            for idx in range(n):
                if nums[idx] not in path:
                    path.append(nums[idx])
                    dfs(i + 1, path)
                    path.pop()
        dfs(0, [])
        return ans

78. 子集

输入:nums = [1,2,3]
输出:[[],[1],[2],[1,2],[3],[1,3],[2,3],[1,2,3]]
# 选或不选维护 arr 即可,注意深拷贝
class Solution:
    def subsets(self, nums: List[int]) -> List[List[int]]:
        ans = []
        n = len(nums)
        def dfs(index, arr):
            if index == n:
                ans.append(arr)
                return
            # 下面是深拷贝
            new_arr = copy.deepcopy(arr)
            new_arr.append(nums[index])
            dfs(index + 1, arr)
            dfs(index + 1, new_arr)
        dfs(0, [])
        return ans

17. 电话号码的字母组合

题干:分块遍历回溯,只能选而不能不选

有:
m = {'2':['a','b','c'],
'3':['d','e','f'],
'4':['g','h','i'],
'5':['j','k','l'],
'6':['m','n','o'],
'7':['p','q','r','s'],
'8':['t','u','v'],
'9':['w','x','y','z']}

输入:digits = "23"
输出:["ad","ae","af","bd","be","bf","cd","ce","cf"]
class Solution:
    def letterCombinations(self, digits: str) -> List[str]:
        n = len(digits)
        if n == 0:
            return []
        # 分块
        m = {'2':['a','b','c'],
                   '3':['d','e','f'],
                   '4':['g','h','i'],
                   '5':['j','k','l'],
                   '6':['m','n','o'],
                   '7':['p','q','r','s'],
                   '8':['t','u','v'],
                   '9':['w','x','y','z']}
        ans = []
        def dfs(index, myStr: str):
            if index == n:
                ans.append(myStr)
                return
            for letter in m[digits[index]]:
                dfs(index + 1, myStr + letter)
        dfs(0, "")
        return ans

39. 组合总和

题干:在 30 个数中选任意个数(可重复),使其综合等于 target,返回全部可能的序列(去重)

# 数据量小 能直接递归回溯
class Solution:
    def combinationSum(self, candidates: List[int], target: int) -> List[List[int]]:
        ans = set() # 去重用,注意其中不能装可变元素
        n = len(candidates)
        def dfs(array: List[int], sum: int):
            nonlocal ans
            if sum > target:
                return
            if sum == target:
                array.sort()
                ans.add(tuple(array)) # 使 array 转成元组并哈希
                return
            for i in range(n):
                newArray = array + [candidates[i]]
                dfs(newArray, sum + candidates[i])
        
        dfs([], 0)
        output = []
        for i in ans:
            output.append(i)
        return output

22. 括号生成

题干:向生成 n 组合法的括号(n < 9),可能有哪些组合方法

思路:暴力 + 剪枝,用省的左右括号数量做递归输入,如果剩下的右括号少于左括号,则不合法。

class Solution:
    def generateParenthesis(self, n: int) -> List[str]:
        ans = []
        path = []
        def dfs(l, r, path):
            nonlocal ans
            # print(l, r, path)
            if l < 0 or r < 0:
                return
            if l == 0 and r == 0:
                ans.append(deepcopy(path))
                return
            if l > r:
                return
            path.append('(')
            dfs(l - 1, r, path)
            path.pop()
            path.append(')')
            dfs(l, r - 1, path)
            path.pop()
            
        dfs(n, n, path)
        output = []
        for i in ans:
            s = ""
            for j in i:
                s += j
            output.append(s)
        return output

79. 单词搜索

题干:想在单词矩阵中找“贪吃蛇”形状的某单词,不能走回头路

输入
board = 
[['A','B','C','E'],
['S','F','C','S'],
['A','D','E','E']], 
word = "ABCCED"
输出true
提示
['*','*','*','E'],
['S','F','*','S'],
['A','*','*','E']

思路:剪枝 + 回溯 DFS

# 走格子回溯,注意优化和剪枝
class Solution:
    def exist(self, board: List[List[str]], word: str) -> bool:
        DIRS = [[0, 1], [1, 0], [-1, 0], [0, -1]]
        n = len(board)
        m = len(board[0])
        t = len(word)
        ans = False
        # 下面是两个小优化
        cnt = Counter(c for row in board for c in row)
        if not cnt >= Counter(word):  # 如果 word 中的词频大于 board 直接返回
            return False
        if cnt[word[-1]] < cnt[word[0]]:  # 将词频更大的元素作为头
            word = word[::-1]
        
        def dfs(i: int, x: int, y: int):
            nonlocal ans
            if ans: # 剪枝
                return 
            if i == t:
                ans = True
                return
            tmp = board[x][y] # 不走重复路,因此要回溯
            board[x][y] = "."
            for dx, dy in DIRS:
                nx = x + dx
                ny = y + dy
                if 0 <= nx < n and 0 <= ny < m:
                    if board[nx][ny] == word[i]:
                        dfs(i + 1, nx, ny)
            board[x][y] = tmp
        # 找字母的头
        for i in range(n):
            for j in range(m):
                if board[i][j] == word[0]:  
                    dfs(1, i, j)
        return ans

131. 分割回文串

题干:将一个字符串,切割为 N 个子字符串,保证每个子字符串是回文串,有多少种切割方法。

输入:s = "aab"
输出:[["a","a","b"],["aa","b"]]
# 枚举字串结束的位置
class Solution:
    def partition(self, s: str) -> List[List[str]]:
        ans = []
        path = []
        n = len(s)
        def dfs(index):
            nonlocal s, ans
            # print(index, path)
            if index == n:
                ans.append(deepcopy(path))
            for j in range(index + 1, n + 1):
                t = s[index:j] # 注意要先切片后再 reverse,否则切片值不一样
                if t[::-1] == t:
                    path.append(t)
                    dfs(j)
                    path.pop()
        dfs(0)
        return ans

51. N 皇后

题干:经典中的经典,N 最大值是 9,返回棋盘

思路:

  1. 状态简化为 path,每个皇后情况其实就是长度为 N 的一维数组,其中 i,x 表示第 i 行的皇后在第 j 列。
  2. 记录 visited 的皇后位置,保证下一行皇后不与列重复。
  3. 用 check 检查对角线是否满足,其中 abs(new_x - old_x) == abs(new_y - old_y) 表示对角线有重叠。
  4. 递归输入当前到第 i 行,回溯并找到满足条件 23 的 path 即可,最后转化为棋盘。
class Solution:
    def solveNQueens(self, n: int) -> List[List[str]]:
        ans = []
        path = []
        visit = set()
        def check(arr: List[int]):
            # print(arr)
            if len(arr) == 0:
                return True
            last = arr[-1]
            last_i = len(arr) - 1
            for i, x in enumerate(arr):
                if i == last_i:
                    continue
                if abs(last_i - i) == abs(last - x):
                    return False
            return True
        
        def dfs(i: int):
            if i == n:
                ans.append(path.copy())
            for idx in range(n):
                if idx not in visit:
                    # print(idx)
                    path.append(idx)
                    visit.add(idx)
                    if check(path):
                        # print(path, i)
                        dfs(i + 1)
                    path.pop()
                    visit.remove(idx)
        dfs(0)
        output = []
        for line in ans:
            line_output = []
            for idx in line:
                s = ""
                # print(line)
                for i in range(n):
                    if i != idx:
                        s += "."
                    else:
                        s += 'Q'
                line_output.append(s) 
            output.append(line_output)
        return output

二分查找
#

35. 搜索插入位置

题干:找第一个大于等于 target 的下标

class Solution:
    def searchInsert(self, nums: List[int], target: int) -> int:
        l = 0
        r = len(nums)
        while l < r:
            mid = l + r >> 1
            if nums[mid] < target: # 如果找第一个大于 target,就此处改为 <= 。
                l = mid + 1
            else:
                r = mid
        return l
        # 等价库函数
        # return bisect.bisect_left(nums, target)

74. 搜索二维矩阵

题干:

  • 每行中的整数从左到右按非严格递增顺序排列。
  • 每行的第一个整数大于前一行的最后一个整数。

找是否有一个数为 target

思路:

先根据每行最后一个数做二分查找,得到该数应该在那行,然后对该行二分查找。

class Solution:
    def searchMatrix(self, matrix: List[List[int]], target: int) -> bool:
        m = len(matrix)
        n = len(matrix[0])
        l = 0
        r = m
        while l < r:
            mid = l + r >> 1
            if matrix[mid][-1] < target:
                l = mid + 1
            else:
                r = mid
        
        line = l
        if line >= m:
            return False
            
        l = 0
        r = n
        while l < r:
            mid = l + r >> 1
            if matrix[line][mid] < target:
                l = mid + 1
            else:
                r = mid
        if matrix[line][l] == target:
            return True
        return False

34. 在排序数组中查找元素的第一个和最后一个位置

题干:找 bisect_left 和 bisect_right 的区间

# 此题用于区分四种二分板子 (yxc 模板)
class Solution:
    def searchRange(self, nums: List[int], target: int) -> List[int]:
        n = len(nums)
        if n == 0:
            return[-1, -1]

        # yxc 板子
        # 这种写法是第一个大于等于 target
        # 若写出 nums[mid] <= target 则为第一个大于 target
        # 下面这种是 lower_bound 写出 <= 是 upper_bound
        l = 0
        r = n - 1
        while l < r :
            mid = l + r >> 1
            if nums[mid] < target:
                l = mid + 1
            else:
                r = mid
        first = l
        if nums[first] != target:
            return [-1 , -1]

        # 这种写法是最后一个小于等于 target
        # 若写出 nums[mid] >= target 则为第一个小于 target
        l = 0
        r = n - 1
        while l < r :
            mid = l + r + 1 >> 1
            if nums[mid] > target:
                r = mid - 1
            else:
                l = mid
        return [first, l]

33. 搜索旋转排序数组

题干:一个有序数组,随机旋转了一次,O(logN) 找目标值

输入:nums = [4,5,6,7,0,1,2], target = 0
输出:4

思路:还是二分查找,但 check 的条件比较复杂,先判断左右哪边有序(一定有一边有序),然后在判断 target 应该在哪边。

# 需要考虑很多边界值的二分查找
class Solution:
    def search(self, nums: List[int], target: int) -> bool:
        l = 0
        r = len(nums) - 1
        while l < r:
            mid = l + r >> 1
            if target == nums[mid]:
                return mid
            # 若右半段有序
            if nums[mid] < nums[r]:
                if nums[mid] < target <= nums[r]:
                    l = mid + 1
                else:
                    r = mid
            else: 
                if nums[l] <= target < nums[mid]:
                    r = mid
                else:
                    l = mid + 1
        return l if nums[l] == target else -1          

153. 寻找旋转排序数组中的最小值

题干:有序数组旋转后,O(logN) 找最小值

思路:上面那题的简化版

class Solution:
    def findMin(self, nums: List[int]) -> int:
        n = len(nums)
        l = 0
        r = n - 1
        while l < r:
            mid = l + r >> 1
            if nums[mid] > nums[r]:
                l = mid + 1
            else:
                r = mid
        return nums[l]

4. 寻找两个正序数组的中位数

题干:找两个递增数组的中位数,要求O(log (m+n))

思路:

① 若 a=b,则a或b为所求中位数,算法结束。

② 若a<b,则舍弃序列A中较小的一半,同时舍弃序列 B中较大的一半。

③ 若a>b,则舍弃序列A中较大的一半,同时舍弃序列 B中较小的一半。

注意:下面代码实际上不是严格 O(log (m+n))。

# ① 若 a=b,则a或b为所求中位数,算法结束。
# ② 若a<b,则舍弃序列A中较小的一半,同时舍弃序列 B中较大的一半。
# ③ 若a>b,则舍弃序列A中较大的一半,同时舍弃序列 B中较小的一半。
class Solution:
    def findMedianSortedArrays(self, nums1: List[int], nums2: List[int]) -> float:
        def get_mid(arr, l, r):
            if (r - l + 1) % 2 == 0:
                return (arr[(l + r) // 2] + arr[(l + r) // 2 + 1]) / 2
            else:
                return arr[(l + r) // 2]

        n1 = len(nums1)
        n2 = len(nums2)
        l1, l2 = 0, 0
        r1, r2 = n1 - 1, n2 - 1

        # 只要有一个数组长度降到了 2 或更少 就停止
        while (r1 - l1 > 1) and (r2 - l2 > 1):
            m1 = get_mid(nums1, l1, r1)
            m2 = get_mid(nums2, l2, r2)
            
            mid1_index = (l1 + r1) // 2
            mid2_index = (l2 + r2) // 2
            
            # 步长必须由较短的那一半决定,防止长数组切多了,或者短数组切空了
            shift = min(mid1_index - l1, mid2_index - l2)
            # 步长至少为 1
            if shift == 0: shift = 1 
            if m1 == m2:
                return m1
            if m1 < m2:
                # 舍弃 nums1 左边,nums2 右边
                l1 = l1 + shift
                r2 = r2 - shift
            else:
                # 舍弃 nums1 右边,nums2 左边
                r1 = r1 - shift
                l2 = l2 + shift

        # 此时至少有一个数组长度 <= 2。
        # 由于剩余元素很少(对于 LeetCode 数据规模),直接合并排序是安全的。
        last = nums1[l1:r1+1] + nums2[l2:r2+1]
        last.sort()
        n = len(last)
        if n == 0: return 0
        if n % 2 == 1:
            return last[n // 2]
        else:
            return (last[n // 2 - 1] + last[n // 2]) / 2
        return -1

#

20. 有效的括号

题干:判断括号串有“({[”是否合法,其中不能出现交错

class Solution:
    def isValid(self, s: str) -> bool:
        stack = []
        for c in s:
            if c == '(':
                stack.append(0)
            if c == '[':
                stack.append(1)
            if c == "{":
                stack.append(2)
            if c == ')':
                if not stack or stack[-1] != 0:
                        return False
                stack.pop()
            if c == ']':
                if not stack or stack[-1] != 1:
                        return False
                stack.pop()
            if c == '}':
                if not stack or stack[-1] != 2:
                        return False
                stack.pop()
        if not stack:
            return True
        return False

155. 最小栈

题干:构造一个可以 O1 找到最小元素的栈

思路:用原始栈 + 单调栈模拟,此题单调栈是非严格单调递减。如果插入小于等于单调栈尾,插入两个栈。如果普通栈尾和单调栈尾相同,弹出两者。

# 用栈 + 单调栈模拟
# 此题单调栈是非严格单调递减,记录当前最小值
class MinStack:
    def __init__(self):
        self.stack = []
        self.min_stack = []
    # 如果插入小于等于单调栈尾,插入单调栈
    def push(self, val: int) -> None:
        if not self.min_stack:
            self.stack.append(val)
            self.min_stack.append(val)
            return 
        if val <= self.min_stack[-1]:
            self.stack.append(val)
            self.min_stack.append(val)
        else:
            self.stack.append(val)
    # 如果普通栈尾和单调栈尾相同,弹出两者
    def pop(self) -> None:
        if self.stack[-1] == self.min_stack[-1]:
            self.min_stack.pop()
            self.stack.pop()
        else:
            self.stack.pop()

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

    def getMin(self) -> int:
self.min_stack.append(self.stack[-1])
        return self.min_stack[-1]

394. 字符串解码

题干:

给定一个经过编码的字符串,返回它解码后的字符串。

编码规则为: k[encoded_string],表示其中方括号内部的 encoded_string 正好重复 k 次。注意 k 保证为正整数。

输入:s = "3[a]2[bc]"
输出:"aaabcbc"
输入:s = "3[a2[c]]"
输出:"accaccacc"
输入:s = "2[abc]3[cd]ef"
输出:"abcabccdcdcdef"

思路:维护当前字符串和 栈,栈里面维护之前的字符串和需要重复的数量,优雅写法如下

class Solution:
    def decodeString(self, s: str) -> str:
        stack = []
        current_num = 0
        current_str = ""
        for c in s:
            if '0' <= c <= '9':
                current_num = current_num * 10 + int(c)
            elif c == '[':
                # 数字和当前字符串入栈
                stack.append((current_str, current_num))
                current_str = ""
                current_num = 0
            elif c == ']':
                # 出栈并构造新的字符串
                last_str, repeat_num = stack.pop()
                current_str = last_str + current_str * repeat_num
            else:
                # 普通字符追加
                current_str += c
        return current_str

739. 每日温度

题干:找数组中 “下一个更大” 的位置,返回每个点和下一个更大的距离。

思路:标准单调栈,记录一个单调递减栈,当有大于顶的元素时,弹出栈并维护答案。

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

84. 柱状图中最大的矩形

题干:获取一个柱状图中的最大矩阵

输入heights = [2,1,5,6,2,3]
输出10
解释最大的矩形为图中*区域面积为 10
高度
 6 |       
 5 |     * * 
 4 |     * * 
 3 |     * *     
 2 |    * *    
 1 |   * *   
     0 1 2 3 4 5

思路:

和接雨水思路一样用前缀后缀,但由于需要记录第一个小于 X 的位置,不能和接雨水一样直接双指针。

记录每个点前第一个和后第一个比他小的位置,对每个元素,由于左右区间内一定大于等于 height[i],因此则 heights[i] * (right[i] - left[i] - 1) 就是以改点为基准的最大值。遍历所有的最大值的就是答案。

想找到每个点前第一个和后第一个比他小的位置,就用到单调递增栈。

class Solution:
    def largestRectangleArea(self, heights: List[int]) -> int:
        stack = []
        n = len(heights)
        left = [-1] * n # 用单调栈找左边第一个比自身小的
        # ans = 0
        for i in range(n):
            while stack and heights[stack[-1]] >= heights[i]:
                stack.pop()
            if stack:
                left[i] = stack[-1]
            stack.append(i)

        right = [n] * n # 用单调栈找右边第一个比自身小的
        stack.clear()
        for i in range(n - 1, -1, -1):
            while stack and heights[stack[-1]] >= heights[i]:
                stack.pop()
            if stack:
                right[i] = stack[-1]
            stack.append(i)

        # 对每个元素,由于左右区间内一定大于等于 height[i]
        # 则 heights[i] * (right[i] - left[i] - 1) 就是包含区间的答案
        ans = 0
        for i in range(n):
            ans = max(ans, heights[i] * (right[i] - left[i] - 1))
        return ans

#

215. 数组中的第K个最大元素

topK 问题,长度 N 的数组中找到第 K 个元素。

  1. 堆:时间 O(N*logK),空间 O(K)
  2. 快速选择:时间平均 ON,最坏 ON2,空间 O1
  3. BFPRT:用五分中位数的中位数做 pivot,最坏 ON
# 第 K 大用 小跟堆
class Solution:
    def findKthLargest(self, nums: List[int], k: int) -> int:
        pq = []
        for i, x in enumerate(nums):
            if len(pq) >= k:
                heapq.heappush(pq, x)
                heapq.heappop(pq)
            else:
                heapq.heappush(pq, x)
        return pq[0]

347. 前 K 个高频元素

题干:找数组前 K 个频率的元素。

思路:Counter 后找 TopK 即可,注意 heapq 中用 cnt 做 key。

class Solution:
    def topKFrequent(self, nums: List[int], k: int) -> List[int]:
        m = Counter(nums)
        pq = []
        for i, j in m.items():
            if len(pq) >= k:
                heapq.heappush(pq, (j, i))
                heapq.heappop(pq)
            else:
                heapq.heappush(pq, (j, i))
        ans = []
        while pq:
            ans.append(pq[0][1])
            heapq.heappop(pq)
        return ans

295. 数据流的中位数

题干:想让 O logN 插入,能 O1 找到当前数据流的中位数

思路:一半大根堆,一半小根堆。想让两者长度相同,在遇到一个新元素时候,现在对面的堆里面洗一下,然后弹出到另一边。

class MedianFinder:
    def __init__(self):
        # 小顶堆(存较大的一半)
        self.smallPQ = []
        # 大顶堆(存较小的一半,存入负数以模拟大顶堆)
        self.bigPQ = []

    def addNum(self, num: int) -> None:
        if len(self.smallPQ) != len(self.bigPQ):
            # 先将 num 放入小顶堆,然后将堆顶移到大顶堆
            heapq.heappush(self.smallPQ, num)
            heapq.heappush(self.bigPQ, -heapq.heappop(self.smallPQ))
        else:
            # 先将 num 放入大顶堆,然后将堆顶移到小顶堆
            heapq.heappush(self.bigPQ, -num)
            heapq.heappush(self.smallPQ, -heapq.heappop(self.bigPQ))

    def findMedian(self) -> float:
        if len(self.smallPQ) != len(self.bigPQ):
            return self.smallPQ[0]
        else:
            return (self.smallPQ[0] - self.bigPQ[0]) / 2.0

贪心
#

121. 买卖股票的最佳时机

题干:

给定一个数组 prices ,它的第 i 个元素 prices[i] 表示一支给定股票第 i 天的价格。

你只能选择 某一天 买入这只股票,并选择在 未来的某一个不同的日子 卖出该股票。设计一个算法来计算你所能获取的最大利润。

返回你可以从这笔交易中获取的最大利润。

输入[7,1,5,3,6,4]
输出5
解释在第 2 股票价格 = 1的时候买入在第 5 股票价格 = 6的时候卖出最大利润 = 6-1 = 5 
     注意利润不能是 7-1 = 6, 因为卖出价格需要大于买入价格同时你不能在买入前卖出股票

思路:直接找前缀最小即可,或者如果递增直接卖。这道题应该和股票买卖234一起看。

class Solution:
    def maxProfit(self, prices: List[int]) -> int:
        pre_min = inf
        n = len(prices)
        ans = 0
        for i in range(n):
            pre_min = min(pre_min, prices[i])
            ans = max(ans, prices[i] - pre_min)
        return ans

55. 跳跃游戏

题干:数组中的每个元素代表你在该位置可以跳跃的最大长度。判断你是否能够从第一个下标到达最后一个下标。

思路:遍历并维护当前能跳到的最远下标,如果当前下标大于之前能跳到的最远下标,说明不能完成任务。注意不要遍历到最后一个点。

class Solution:
    def canJump(self, nums: List[int]) -> bool:
        fast = 0
        n = len(nums)
        # 遍历到 n - 2
        for i in range(0, n - 1):
            fast = max(fast, i + nums[i])
            # 如果到最后一个之前就卡住了
            if fast <= i:
                return False
        return True

45. 跳跃游戏 II

题干:包装上面情景下一定能走到最后下标,想得到最短的跳跃次数。

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

思路:同样维护当前能走到的最远位置 fast 和上一次跳跃到的最远位置 slow,如果 i 走到上一次跳跃的最短位置 slow,则说明需要再跳一次,就跳到当前可以跳到的最远位置 fast。

class Solution:
    def jump(self, nums: List[int]) -> int:
        fast = 0
        slow = 0
        ans = 0
        n = len(nums)
        # 遍历到 n - 2
        for i in range(0, n - 1):
            fast = max(fast, i + nums[i])
            if i == slow:
                ans += 1
                slow = fast
        return ans

763. 划分字母区间

题干:想让尽可能多的切割一个字符串,让同一字母最多出现在一个子字符串中。

思路:记录每个字母的第一次出现和最后一次出现,就是一个字母的最小区间,之后思路和合并区间完全一样。

class Solution:
    def partitionLabels(self, s: str) -> List[int]:
        m = defaultdict(lambda : [-1, -1])
        for i, c in enumerate(list(s)):
            if m[c][0] == -1:
                m[c][0] = i
                m[c][1] = i
            else:
                m[c][1] = i
        intervals = list(m.values())
        # print(intervals)
        
        def merge(intervals: List[List[int]]) -> List[List[int]]:
            intervals.sort()
            ans = []
            l = intervals[0][0]
            r = intervals[0][1]
            for x, y in intervals:
                if x == -1 or y == -1:
                    continue
                if x <= r:
                    r = max(y, r)
                else:
                    ans.append([l, r])
                    l = x
                    r = y
            ans.append([l, r])
            return ans
        ans = []
        res = merge(intervals)
        for i, j in res:
            ans.append(j - i + 1)
        return ans

动态规划
#

70. 爬楼梯

题干:求斐波那契数列

思路:最简单的 DP,如果用通式或者矩阵快速幂可以做到 OlogN

class Solution:
    def climbStairs(self, n: int) -> int:
        dp = [0] * (n + 1)
        dp[0] = 1
        dp[1] = 1
        for i in range(2, n + 1):
            dp[i] = dp[i - 1] + dp[i - 2]
        return dp[n]

118. 杨辉三角

题干:模拟一个杨辉三角

class Solution:
    def generate(self, numRows: int) -> List[List[int]]:
        ans = [[] for _ in range(numRows)]
        ans[0].append(1)
        for i in range(1, numRows):
            ans[i].append(1)
            for y in range(1, len(ans[i - 1])):
                ans[i].append(ans[i - 1][y - 1] + ans[i - 1][y])
            ans[i].append(1)
        return ans

198. 打家劫舍

题干:典中典的 打家劫舍,即不能偷两个相邻的房子,问最大能偷多少。

输入:[1,2,3,1]
输出:4
解释:偷窃 1 号房屋 (金额 = 1) ,然后偷窃 3 号房屋 (金额 = 3)。
     偷窃到的最高金额 = 1 + 3 = 4 。

思路:DP 维护 “到该点最多偷多少”

状态转移:dp[i] = max(dp[i - 1], dp[i - 2] + nums[i])

class Solution:
    def rob(self, nums: List[int]) -> int:
        ans = 0
        n = len(nums)
        if n == 1:
            return nums[0]
        if n == 2:
            return max(nums[0], nums[1])
        dp = [0] * n 
        dp[0] = nums[0]
        dp[1] = max(nums[0], nums[1])
        for i in range(2, n):
            dp[i] = max(dp[i - 1], dp[i - 2] + nums[i])
            ans = max(ans, dp[i])
        print(dp)
        return ans

思考:如果是环形,则直接讨论第一个位置有没有被打,如果被打了就对后面 [2 , n - 2] 做打家劫舍,如果没有,就对后面 [2, n - 1] 做打家劫舍。

279. 完全平方数

题干:给你一个整数 n ,返回 和为 n 的完全平方数的最少数量。

思路:明显的完全背包,其中 1 - int(sqrt(n)) 是可以选的物品,n 是背包容量。因此用 DP 表示背包容量为 i 时,正好填满的物品最小数量。

状态转移方程 dp[i + j * j] = min(dp[i + j * j], dp[i] + 1)

此外,此题也可以数学 O1 做出来。

class Solution:
    def numSquares(self, n: int) -> int:
        high = int(sqrt(n))
        dp = [inf] * (n + 1)
        dp[0] = 0
        for i in range(n + 1):
            for j in range(1, high + 1):
                if i + j * j <= n:
                    dp[i + j * j] = min(dp[i + j * j], dp[i] + 1)
        return dp[n]

322. 零钱兑换

题干:给你一个整数数组 coins ,表示不同面额的硬币;以及一个整数 amount ,表示总金额,计算凑到总金额的最小硬币个数。

思路:还是完全背包问题,多了一个可能背包凑不满的情况

状态转移方程:dp[i + j] = min(dp[i + j], dp[i] + 1)

class Solution:
    def coinChange(self, coins: List[int], amount: int) -> int:
        n = amount
        dp = [inf] * (n + 1)
        dp[0] = 0
        for i in range(n + 1):
            for j in coins:
                if i + j <= n:
                    dp[i + j] = min(dp[i + j], dp[i] + 1)
        return dp[n] if dp[n] != inf else -1

思考:零钱兑换Ⅱ是返回凑到amount的硬币种类的数量,那么此题就要先遍历coins 再遍历 i,然后用状态转移方程 dp[i + j] += dp[i]

class Solution:
    def change(self, amount: int, coins: List[int]) -> int:
        n = amount
        dp = [0] * (n + 1)
        dp[0] = 1
        for j in coins:
            for i in range(n + 1):
                if i + j <= n:
                    dp[i + j] += dp[i]
            # print(dp)
        print(dp)
        return dp[n]

139. 单词拆分

题干:给你一个字符串 s 和一个字符串列表 wordDict 。如果可以利用wordDict中出现的一个或多个单词拼接出 s 则返回 true。可以重复使用。

思路:同样还是类似完全背包的动态规划,其中 dp 表示以 i 为截至的字符串能不能被凑出来。如果想节省复杂度,可以用 wordDict 记录每个的长度,然后切片得到子字符串,这样就和完全背包几乎一样。

状态转移方程 :dp[j] = dp[i] or dp[j] if s[i:j] in m

class Solution:
    def wordBreak(self, s: str, wordDict: List[str]) -> bool:
        m = set(wordDict)
        n = len(s)
        dp = [False] * (n + 1)
        dp[0] = True
        for i in range(n + 1):
            for j in range(i + 1, n + 1):
                if s[i:j] in m:
                    dp[j] = dp[i] or dp[j]
        print(dp)
        return dp[n]

300. 最长递增子序列

题干:找到最长递增子序列的长度(子序列是指对每个元素选或不选而不改变顺序的集合)。

思路:

DP + 二分查找,遍历并用 tails 记录当前 i 下所有可能的子序列长度对应的最小值。

比如 [1, 3, 4, 2, 5] 遍历完后,tails 应该是 [1, 2, 4, 5],所以 tails 一定是单调递增的。

遍历过程中可以对其二分查找,找 nums[i] 在 tails 对应的位置,如果最后 l 与 tails 长度相同,则说明 tails 可以扩充,否则修改 tails[l] 的最小值

class Solution:
    def lengthOfLIS(self, nums: List[int]) -> int:
        n = len(nums)
        tails = []
        tails.append(nums[0])
        for i in range(1, n):
            l = 0
            r = len(tails)
            while l < r:
                mid = l + r >> 1
                if tails[mid] < nums[i]:
                    l = mid + 1
                else:
                    r = mid
            # print(tails, l, nums[i])
            if l == len(tails):
                tails.append(nums[i])
            else:
                tails[l] = min(nums[i], tails[l])
        
        return len(tails)

152. 乘积最大子数组

题干:找出数组中乘积最大的非空连续 子数组(该子数组中至少包含一个数字),并返回该子数组所对应的乘积。

思路:用二维 DP 记录包含 i 位置的最大和最小元素,如果是 num 就向前对应,如果是负数就 相互取反。

class Solution:
    def maxProduct(self, nums: List[int]) -> int:
        # dp 表示包含 i 位置的最大和最小元素
        n = len(nums)
        dp = [[0, 0] for _ in range(n + 1)]
        dp[0] = [1, 1]
        ans = nums[0]  # 初始化为第一个元素
        
        for i in range(1, n + 1):
            cur = nums[i - 1]
            if cur < 0:
                dp[i][0] = max(cur, dp[i - 1][1] * cur)
                dp[i][1] = min(cur, dp[i - 1][0] * cur)
            elif cur > 0:
                dp[i][0] = max(cur, dp[i - 1][0] * cur)
                dp[i][1] = min(cur, dp[i - 1][1] * cur)
            else:
                dp[i][0] = 0
                dp[i][1] = 0
            ans = max(ans, dp[i][0])
        print(dp)
        return ans

416. 分割等和子集

题干:给你一个 只包含正整数 的 数组 nums 。判断是否可以将这个数组分割成两个子集使其 sum 相等。

思路:目标是在数组中选到和为 sum(nums) // 2 的数,本质上还是一个物品只能选一次的背包问题,如果一开始 sum 是偶数直接返回。

dp[i][j] 表示为,前 j 个数能否表达出 sum 为 i 。

因此状态转移方程为:dp[i][j] = dp[i][j - 1] or dp[i - x][j - 1] or dp[i][j] 即不选 j 和选 j 的情况。

# 目标是在数组中选到和为 sum(nums) // 2 的数
# 本质上还是一个物品只能选一次的背包问题
class Solution:
    def canPartition(self, nums: List[int]) -> bool:
        all = sum(nums) 
        n = len(nums)
        if all % 2 == 1: # all 必须为偶数
            return False
        target = all // 2
        # 前 j 个数能否表达出 sum 为 i
        dp = [[False] * n for _ in range(target + 1)]
        for i in range(n):
            if nums[i] > target:
                return False
            dp[0][i] = True
              
        for i in range(target + 1):
            for j in range(1, n):
                x = nums[j]
                dp[i][j] = dp[i][j - 1]
                if i - x >= 0:
                    dp[i][j] = dp[i - x][j - 1] or dp[i][j]

        return dp[-1][-1]

32. 最长有效括号

题干:找括号串中,有效部分的最长大小

输入:s = ")()())"
输出:4
解释:最长有效括号子串是 "()()"

思路:用 stack 模拟找到出错的位置,之后就变成找未错位置中最长的串就可以。这题和动态规划关系不大。

class Solution:
    def longestValidParentheses(self, s: str) -> int:
        n = len(s)
        unmatch = [0] * n
        stack = []
        for i in range(n):
            c = s[i]
            if not stack and c == ')':
                unmatch[i] = 1 # 出错
            elif c == ')':
                stack.pop()
            if c == '(':
                stack.append(i)
        for i in stack: # 剩下的栈也是错的
            unmatch[i] = 1
        # 找最长的连续零
        l = -1
        r = -1
        ans = 0
        for i in range(n):
            if unmatch[i]:
                r = i
                ans = max(ans, r - l - 1)
                l = i  
        return max(ans, n - r - 1)

多维动态规划
#

62. 不同路径

题干:一个机器人位于一个 m x n 网格的左上角,可以向左或向右走一步,问一共有多少条不同的路径。

思路:组合数即可,机器人共走 m + n - 2 步,总步数中选 m - 1 步就是答案(理解为找出机器人向右走的占总步数中的哪些部分,就是一共多少路径)

class Solution:
    def uniquePaths(self, m: int, n: int) -> int:
        return comb(m + n - 2, m - 1)

64. 最小路径和

题干:给定一个包含非负整数的 m x n 网格 grid ,请找出一条从左t上角到右下角的路径,使得路径上的数字总和为最小。

输入:grid = [[1,3,1],[1,5,1],[4,2,1]]
输出:7
解释:因为路径 1→3→1→1→1 的总和最小。

思路:记忆化搜索走迷宫,相当于每次只有向右和向下两种选择。

class Solution:
    def minPathSum(self, grid: List[List[int]]) -> int:
        m = len(grid)
        n = len(grid[0])
        @cache
        def dfs(i: int, j: int) -> int:
            if i == 0 and j == 0:
                return grid[i][j]
            elif i == 0:
                return dfs(i, j - 1) + grid[i][j]
            elif j == 0:
                return dfs(i - 1, j) + grid[i][j]
            return min(dfs(i - 1, j), dfs(i, j - 1)) + grid[i][j] 
        return dfs(m - 1, n - 1)

5. 最长回文子串

题干:找数组中的最长回文子串(子串一定连续)

思路:

  1. 中心扩散动态规划,即区间 DP 的一种,dp[i][j] 表示从 i - j 的回文字串长度,先遍历 j ,再从 j 向前遍历 i,从而实现从中间遍历到两边。时间复杂度 ON2
  2. 马拉车算法实现 ON
class Solution:
    def longestPalindrome(self, s: str) -> str:
        n = len(s)
        dp = [[-inf] * n for _ in range(n)]
        mx = 0
        index = 0 # 用 index 维护最大值的位置
        for j, y in enumerate(s):
            for i in range(j, -1, -1):
                x = s[i]
                if j < i:
                    continue
                if i == j:
                    dp[i][j] = 1
                    if mx < 1:
                        index = i
                        mx = 1
                if i == j - 1 and x == y:
                    dp[i][j] = 2
                    if mx < 2:
                        index = i
                        mx = 2
                    
                if x == y and j - i >= 2:
                    if mx < dp[i + 1][j - 1] + 2:
                        index = i
                        mx = dp[i + 1][j - 1] + 2
                    dp[i][j] = max(dp[i + 1][j - 1] + 2, dp[i][j])
        # print(dp)
        return s[index:index + mx]

1143. 最长公共子序列

题干:给定两个字符串 text1text2,返回这两个字符串的最长 公共子序列 的长度

输入:text1 = "abcde", text2 = "ace" 
输出:3  
解释:最长公共子序列是 "ace" ,它的长度为 3 。

思路:用 dp[i][j] 表示遍历到 i j 时最长公共序列长度,当 t1[i] == t2[j] 的时候 +1, 然后分别跳过 i 和 j。

下面用记忆化搜索,写起来简单点

class Solution:
    def longestCommonSubsequence(self, text1: str, text2: str) -> int:
        @cache
        def dfs(i, j):
            if i < 0 or j < 0:
                return 0
            if text1[i] == text2[j]:
                return dfs(i - 1, j - 1) + 1
            return max(dfs(i, j - 1), dfs(i - 1, j))
        return dfs(len(text1) - 1, len(text2) - 1)

72. 编辑距离

给你两个单词 word1word2, 请返回将 word1 转换成 word2 所使用的最少操作数 。

你可以对一个单词进行如下三种操作:

  • 插入一个字符
  • 删除一个字符
  • 替换一个字符
输入:word1 = "horse", word2 = "ros"
输出:3
解释:
horse -> rorse (将 'h' 替换为 'r')
rorse -> rose (删除 'r')
rose -> ros (删除 'e')

思路:

状态机 DP,dp[i][j]设置为前 i 个 word1 转变为 前 j 个 word2 的编辑距离。

有初始化:当 i == 0 时 dp[i][j] = j 同样,j == 0 时 dp[i][j] = i,这可以理解为全部删除 word1 串,和将空串添加为 word2 串。

如果 word1[i ] == word2[j] 则不需要增加编辑距离,其他情况下,有状态转移方程 min(dp[i - 1][j], dp[i][j - 1], dp[i - 1][j - 1]) + 1 可以视作分别做 ”删、增、改“

# 记忆化搜索
class Solution:
    def minDistance(self, word1: str, word2: str) -> int:
        n = len(word1)
        m = len(word2)
        @cache
        def dfs(i, j):
            if i < 0:
                return j + 1
            if j < 0:
                return i + 1
            
            if word1[i] == word2[j]:
                return dfs(i - 1, j - 1)
            return min(dfs(i - 1, j), dfs(i, j - 1), dfs(i - 1, j - 1)) + 1
        return dfs(n - 1, m - 1)
# 递推
class Solution:
    def minDistance(self, word1: str, word2: str) -> int:
        n = len(word1)
        m = len(word2)
        dp = [[0] * (m + 1) for _ in range(n + 1)]
        for i in range(n + 1):
            dp[i][0] = i  # 将 word1 的前 i 个字符变为空字符串,需要 i 次删除
        for j in range(m + 1):
            dp[0][j] = j  # 将空字符串变为 word2 的前 j 个字符,需要 j 次插入

        for i in range(1, n + 1):
            for j in range(1, m + 1):
                if word1[i - 1] == word2[j - 1]:
                    dp[i][j] = dp[i - 1][j - 1]
                else:
                    dp[i][j] = min(dp[i - 1][j] + 1, dp[i][j - 1] + 1, dp[i - 1][j - 1] + 1)
        return dp[n][m]

技巧
#

136. 只出现一次的数字

题干:数组除了某个元素只出现一次以外,其余每个元素均出现两次。找出那个只出现了一次的元素。

思路:异或和即可

class Solution:
    def singleNumber(self, nums: List[int]) -> int:
        ans = 0
        for num in nums:
            ans ^= num
        return ans

169. 多数元素

题干:找数组中大于等于一半数量的元素

思路:摩尔计数法,记录当前最多元素 X。当出现 X,则 cnt + 1, 出现其他元素则 cnt - 1。当 cnt <= 0 时候,改 X 为当前遍历到的元素。

class Solution:
    def majorityElement(self, nums: List[int]) -> int:
        count = 0
        candidate = nums[0]
        for num in nums:
            if num == candidate:
                count += 1
            else:
                if count > 0:
                    count -= 1
                else:
                    count = 1
                    candidate = num
        return candidate

75. 颜色分类

题干:荷兰国旗问题,数组中只有 0, 1, 2 如何快速排序

思路:用 p1,p2 记录当前遍历到的 0 和 2 的数量,分别指向前 p1 位置和后 p2 位置,swap 直到满足 nums[i] == 1。

# 荷兰国旗问题
class Solution:
    def sortColors(self, nums: List[int]) -> None:
        n = len(nums)
        p0 = 0
        p2 = n - 1
        for i in range(n):
            while  i < p2 and nums[i] == 2:
                nums[i], nums[p2] = nums[p2], nums[i]
                p2 -= 1
            while nums[i] == 0 and i > p0:
                nums[i], nums[p0] = nums[p0], nums[i]
                p0 += 1
        return nums

31. 下一个排列

题干:找数组中下一个排序,希望原地修改。

输入:nums = [1,2,3]
输出:[1,3,2]
找规律:
[1, 2, 3, 4]
[1, 2, 4, 3]
[1, 3, 2, 4]
[1, 3, 4, 2]
[1, 4, 2, 3]
[1, 4, 3, 2]
[2, 1, 3, 4]
[2, 1, 4, 3]

思路:

由上面的规律,可以发现找下一个排序的规律:

  1. 如果当前数组为全部降序,则说明此时已经最大,下一个是全部取反。
  2. 从后往前找第一个升序(arr[i] > arr[i + 1])的位置 i,x。
  3. 从后往前找第一个比 x 大的位置:j,y。
  4. 交换 i, j 的位置,此时 i 之后的数一定是降序,将之后的的位置反转为升序。

思考:如果改成 “上一个排序”,则同样是找规律,把降序升序换一下就可以。

class Solution:
    def nextPermutation(self, nums: List[int]) -> None:
        """
        Do not return anything, modify nums in-place instead.
        """
        n = len(nums)
        # 从后向前找最后一个非升序的下标
        idx1 = -1
        for i in range(n - 2, -1, -1):
            if nums[i + 1] > nums[i]:
                idx1 = i 
                break
        if idx1 == -1: # 如果全部降序,相当于是最大序列。下一个是最小序列,需要特判。
            return nums.reverse() 
        # 从后往前找最后一个大于 nums[idx1] 的
        idx2 = -1
        for i in range(n - 1, -1, -1):
            if nums[i] > nums[idx1]:
                idx2 = i
                break
        # 交互下标
        nums[idx1], nums[idx2] = nums[idx2], nums[idx1]
        # 将 idx1 后面的降序子数组改成升序
        nums[idx1+1:] = reversed(nums[idx1+1:]) # 实际上会产生临时内存,如果想空间 O1 可以双指针反转

287. 寻找重复数

题干:数组中只有一个重复的数,想不修改数组的情况下 O1 空间找到该数。数组中全部数字都在 [1, n] 中。

思路:将数字映射视为链表,然后和双指针找环入口一样。

# 将数组映射视作链表,之后就与快慢指针找环入口一致
class Solution:
    def findDuplicate(self, nums: List[int]) -> int:
        n = len(nums)
        slow = fast = 0
        
        while True:
            fast = nums[nums[fast]]
            slow = nums[slow]    
            if fast == slow:
                break
        fast = 0
        while True:
            fast = nums[fast]
            slow = nums[slow]
            if fast == slow:
                return fast
            
        return 0

备忘录
#

完整输入输出
#

尤其涉及到 class 的代码,类和完整输入输出可能会忘了怎么写。

下面以反转链表的题目为例,完整版本如下。

import os
class node():
    def __init__(self, val = -1, next = None):
        self.val = val
        self.next = next

def reversed_list(head):
    pre = None
    cur = head
    while cur:
        tmp = cur.next
        cur.next = pre
        pre = cur
        cur = tmp
    return pre

def build(arr):
    dummy = node()
    cur = dummy
    for i in arr:
        tmp = node(i)
        cur.next = tmp
        cur = cur.next
    return dummy

def print_node(head):
    cur = head
    while cur:
        print(cur.val, end=" ")
        cur = cur.next

if __name__ == '__main__':
    arr = list(map(int, input().split()))
    head = build(arr).next
    head_re = reversed_list(head)
    print_node(head_re)

import 包
#

import sys
import math
from collections import *
from heapq import *
from bisect import *
from functools import *

# 递归防止爆栈,cache 必备
sys.setrecursionlimit(10**6)

相关文章