Appendix A · Python Rehab Kit

附录 A Python 刷题急救包 复健,不是学习——主体动作是打字

惯用法 24 条翻车点 5 个复健题 10 道改错自测 5 题
§1 Scope 划定范围

复健,不是学习

这一页做的是复健,不是学习:你会 Python,只是长期把写代码这件事委托给 AI 之后,手感萎缩了。所以本页的主体动作是打字,不是阅读——§2 的速查表是拿来查的,不是拿来读的;§3、§4 的练习篇幅是它的两倍,那才是重点。

先把范围划死,降低心理负担:LeetCode Python 是整个语言的一个极小子集。下面 24 条惯用法加 5 个专属翻车点,覆盖刷题日常会碰到的绝大部分写法——每一条都在构建时实际执行验证过,包括注释里的反例。碰到没收录的写法很正常,查一下就行,不必回头补课。

以下内容刷题用不到,一个都不用碰:

  • 装饰器
  • 生成器进阶(yield from、协程式用法)
  • async / await
  • 类型系统进阶(泛型、Protocol)
  • 包管理(venv / pip / poetry)
  • 面向对象设计(继承体系、魔术方法)
§2 Cheat Sheet 惯用法速查

拿来查的,不是拿来读的

按使用频率排序。编号(C1、W1、Q5…)供 §3 的「复健自查」引用,点编号可跳回。

容器基本功
C1  a = [0] * n                          # 一维初始化:n 个 0
C2  g = [[0] * n for _ in range(m)]      # 二维初始化;[[0]*n]*m 是 m 份同一引用(翻车点①)
C3  a[i:j], a[::-1]                      # 切片左闭右开;[::-1] 反转
C4  d[k] = d.get(k, 0) + 1               # dict 计数三连:键不存在按 0 算
C5  s.add(x); x in s                     # set:O(1) 判断存在性
C6  st.append(x); st.pop()               # list 当栈:同侧进出,栈顶是 st[-1]
C7  max(best, x), min(xs), sum(xs)       # 打擂台与聚合;max 还能带 key=
遍历三件套
T1  for i, x in enumerate(nums): ...     # 要下标也要值
T2  for a, b in zip(xs, ys): ...         # 并排走两个序列,短的一停全停
T3  out = [f(x) for x in nums if ok(x)]  # 推导式:过滤 + 变换一行完成
字符串
W1  s.lower(), c.isalnum()               # 归一化 / 判断字母数字:125 靠这两个过滤
W2  ''.join(chs), s.split()              # 拼接 / 按空白切分;字符串不可变,不能改单个字符
排序
S1  nums.sort()                          # 原地升序;返回 None,别写 nums = nums.sort()
S2  pairs.sort(key=lambda p: (p[0], -p[1]))  # 多关键字;数值键取负 = 这一键降序
collections + heapq 四件套
Q1  from collections import deque, Counter, defaultdict
Q2  q = deque(); q.append(x); q.popleft()  # BFS 队列:右进左出,两头都是 O(1)
Q3  Counter(s)                           # 一行频次统计,可当 dict 用
Q4  g = defaultdict(list); g[u].append(v)  # 建图:首次访问自动变 []
Q5  heapq.heappush(h, x); heapq.heappop(h)  # 最小堆;要最大堆就 push -x,3.14 起另有 heappush_max(翻车点④)
LeetCode 特有语法
L1  best = float('inf')                  # 打擂台初始值;负无穷 float('-inf')
L2  7 // 2, -7 // 2                      # 整除向下取整:结果是 3 和 -4(不是 -3),DP 边界常见坑
L3  a, b = b, a + b                      # 平行赋值:右边整体先求值;链表反转 / 滚动 DP 靠它
L4  class ListNode: ...                  # 题目自带的定义,会读会用即可,不用自己写
L5  import sys; sys.setrecursionlimit(10 ** 6)   # 深递归保险(树 / 图 DFS 防爆栈)

五个 Python 专属翻车点

  1. 二维初始化的引用陷阱——改 g[0][0] 等于改每一行——正确写法见 C2 的列表推导。
    P1  g = [[0] * n] * m        # m 行是同一个 list 的 m 个引用
  2. 可变默认参数跨调用共享——默认值只在函数定义时创建一次,第二次调用还是同一个 dict。要缓存就写 memo=None,函数体里再建。
    P2  def f(x, memo={}): memo[x] = 1; return len(memo)
  3. 闭包里改外层变量要 nonlocal——函数内一旦对某个名字赋值,Python 就把它当本地变量,读它就 UnboundLocalError。在 inner 开头加 nonlocal cnt。
    P3  def outer():
        cnt = 0
        def inner():
            cnt += 1          # 少了 nonlocal cnt
        inner()
  4. heapq 默认是最小堆——heappop 永远弹最小。要最大堆:push 时存 -x,pop 后取负还原(见 Q5);Python 3.14 起也可用 heappush_max / heappop_max,先确认面试环境的版本。
    P4  largest = heapq.heappop(h)   # 想要最大值?弹出来的偏偏是最小值
  5. 没有 ++ / --——自增只有 i += 1。++i 是两个一元正号,静默等于 i,连报错都不给。
    P5  i++                       # SyntaxError;更阴险的是 ++i 合法但什么都不做
§3 Typing Rehab 打字复健计划

本页的主体:手打这 10 道

规则,一条都别放水:

  • 手打每一行,禁止复制粘贴;
  • 编辑器关掉 AI 补全(Copilot / Cursor Tab / 各种 …);
  • 每道 ≤ 15 分钟;
  • 卡住看速查表,不看题解——每题下方的「复健自查」告诉你看哪几条;
  • 打完在 LeetCode 提交验证,不是在脑子里跑一遍。

10 道复健题,每道标注它复健的惯用法。它们同时全部收录在正文各章(其中 9 道是热身层)——双重用途:打完这一页,全书 17 道热身题你已经完成了 53%。

复健题单按数据结构由简到繁,全部手打10 道
  • 1 Two SumEasy正文 01 · 热身
    复健目标:dict 计数 / 查找
    复健自查(卡住再点开,不是题解)

    本题用到:C4、T1——逐条对着敲;还是卡,回 §2 看整组,仍然不看题解。

  • 217 Contains DuplicateEasy正文 01 · 热身
    复健目标:set 判存在
    复健自查(卡住再点开,不是题解)

    本题用到:C5——逐条对着敲;还是卡,回 §2 看整组,仍然不看题解。

  • 242 Valid AnagramEasy正文 01 · 标准
    复健目标:Counter / 手写计数
    复健自查(卡住再点开,不是题解)

    本题用到:Q3、C4——逐条对着敲;还是卡,回 §2 看整组,仍然不看题解。

  • 125 Valid PalindromeEasy正文 02 · 热身
    复健目标:字符串方法、双指针
    复健自查(卡住再点开,不是题解)

    本题用到:W1、C3——逐条对着敲;还是卡,回 §2 看整组,仍然不看题解。

  • 20 Valid ParenthesesEasy正文 04 · 热身
    复健目标:list 当栈
    复健自查(卡住再点开,不是题解)

    本题用到:C6、C4——逐条对着敲;还是卡,回 §2 看整组,仍然不看题解。

  • 704 Binary SearchEasy正文 05 · 热身
    复健目标:下标边界、整除
    复健自查(卡住再点开,不是题解)

    本题用到:L2——逐条对着敲;还是卡,回 §2 看整组,仍然不看题解。

  • 复健目标:单次遍历 + 打擂台
    复健自查(卡住再点开,不是题解)

    本题用到:L1、C7、T1——逐条对着敲;还是卡,回 §2 看整组,仍然不看题解。

  • 70 Climbing StairsEasy正文 16 · 热身
    复健目标:dp 数组初始化 / 滚动变量
    复健自查(卡住再点开,不是题解)

    本题用到:C1、L3——逐条对着敲;还是卡,回 §2 看整组,仍然不看题解。

  • 206 Reverse Linked ListEasy正文 06 · 热身
    复健目标:ListNode、平行赋值
    复健自查(卡住再点开,不是题解)

    本题用到:L4、L3——逐条对着敲;还是卡,回 §2 看整组,仍然不看题解。

  • 104 Maximum Depth of Binary TreeEasy正文 07 · 热身
    复健目标:递归 + 取较大者
    复健自查(卡住再点开,不是题解)

    本题用到:C7、L5——逐条对着敲;还是卡,回 §2 看整组,仍然不看题解。

出口标准 · Exit Criteria

10 道全部一次提交通过(或第二次),且没有一道超过 15 分钟 → 关闭本页,进入正文。达不到就重打不达标的那几道——不加新题。

§4 Self-check 自测 · 30 秒改错

看得出坑,才算复健完成

给你一段带 bug 的代码,30 秒内指出错在哪。五段代码分别踩了 §2 的五个翻车点。

30 秒改错Q1 · 这段代码里埋了一个 Python 专属的坑——错在哪?

rows, cols = 3, 4
visited = [[False] * cols] * rows
visited[0][0] = True
看答案

翻车点①:[[False]*cols]*rows 复制的是引用。要 rows 份独立的行,只能用列表推导(见 C2)。

30 秒改错Q2 · 这段代码里埋了一个 Python 专属的坑——错在哪?

def collect(node, acc=[]):
    acc.append(node)
    return acc
看答案

翻车点②:可变默认参数跨调用共享。习惯写法 acc=None,函数体里 if acc is None: acc = []。

30 秒改错Q3 · 这段代码里埋了一个 Python 专属的坑——错在哪?

def count_leaves(root):
    total = 0
    def dfs(node):
        if node is None:
            return
        if not node.left and not node.right:
            total += 1
        dfs(node.left)
        dfs(node.right)
    dfs(root)
    return total
看答案

翻车点③:total += 1 让 Python 把 total 当 dfs 的本地变量。在 dfs 开头加 nonlocal total,或改成 dfs 返回计数再相加。

30 秒改错Q4 · 这段代码里埋了一个 Python 专属的坑——错在哪?

import heapq

def top3(nums):
    h = list(nums)
    heapq.heapify(h)
    return [heapq.heappop(h) for _ in range(3)]   # 想拿最大的 3 个
看答案

翻车点④:heappop 弹的是最小值。push 时取负、pop 后还原,或者直接 heapq.nlargest(3, nums)(3.14 起也可用 heappush_max 等,先确认环境版本)。

30 秒改错Q5 · 这段代码里埋了一个 Python 专属的坑——错在哪?

def total(nums):
    s, i = 0, 0
    while i < len(nums):
        s += nums[i]
        i++
    return s
看答案

翻车点⑤:i++ 连运行的机会都没有——编译期就 SyntaxError。更阴险的是前缀 ++i 合法但什么都不做。

5 道改错题与 §2 的 5 个翻车点一一对应;每段代码的 bug 都在构建时实际执行证明过是真的。选项顺序构建时打乱,别背位置。