← 2026-07-19 📂 All Days 2026-07-21 →

🔄 复习日 / Review Day 95

**Date:** 2026-07-20 | **Day:** 95 | **Phase:** Expert

📊 进度 / Progress

NeetCode: 75/150 · SysDesign: 60/40 · Behavioral: 60/40 · Frontend: 37/50 · AI: 30/30

🔥 Reviewing Days 91–94


今天是复习日!回顾过去4天的内容。

It's review day! Let's consolidate the last 4 days.

回顾主题 / Topics Covered:

  • Day 91: Permutations (回溯)
  • Day 92: Subsets II (去重回溯)
  • Day 93: Combination Sum II (限量回溯)
  • Day 94: 深度复习 — Word Search & 2D Grid Backtracking
  • Day 91: pytest Fixtures & factory_boy
  • Day 92: Property-Based Testing (hypothesis)
  • Day 93: Testing Async Code (pytest-asyncio)

📝 Quick Quiz — 3 Mini-Reviews


Q1: [💻 Algorithms — Backtracking]

Permutations vs Subsets II vs Combination Sum II — 这三道题都用回溯,但各有不同。说出它们最核心的区别是什么?

All three use backtracking, but each has a key structural difference. What distinguishes them?

<details><summary>显示答案 / Show Answer</summary>

Permutations(全排列):每个数都要用,顺序重要,用 used[] 数组标记已选元素,不能重复用同一个下标。模板特征:if len(path) == len(nums): result.append(path[:])

Subsets II(含重复的子集):数组有重复元素,需要先排序,然后在同一层跳过重复值:if i > start and nums[i] == nums[i-1]: continue。长度不限,每次递归都收集结果。

Combination Sum II(每个元素只用一次):同样先排序+去重,区别是 start = i + 1(每元素只能用一次),且需要剪枝 if running_sum > target: break。

核心口诀 / Cheat Sheet:

Permutations → used[],顺序重要 Subsets II → 排序+跳重,start传i Combo Sum II → 排序+跳重,start传i+1,剪枝

</details>


Q2: [🐍 Python Craft — Testing]

你在用 hypothesis 做属性测试,写了 @given(st.lists(st.integers())) 来测试你的 merge_sort 函数。测试跑了1000次但一次都没找到 bug。这能说明你的代码正确吗?什么情况下 property-based testing 比 example-based testing 更有价值?

You run hypothesis 1000 times with no failures. Does this prove your merge_sort is correct? When does property-based testing shine vs example-based?

<details><summary>显示答案 / Show Answer</summary>

不能证明正确,但提高了信心。 Hypothesis 会收缩(shrink)失败案例找到最小反例,但它不做穷举验证。

Property-based testing 的价值在于:

  1. 边界盲点:你想不到的输入,hypothesis 能自动生成(空列表、MAX_INT、重复元素、负数)
  2. 不变量验证:测试"输出长度等于输入长度"、"输出已排序"这类性质,而不是具体值
  3. 回归覆盖:一旦找到失败案例,hypothesis 会记录在 .hypothesis/ 文件中,下次永远会重跑

Example-based 更适合:

  • 明确业务逻辑("输入A应该返回B")
  • 文档式测试,让人一眼看懂期望行为

最佳实践: 两者结合 — example tests 作为规格文档,property tests 作为健壮性保险。

from hypothesis import given, settings
import hypothesis.strategies as st

@given(st.lists(st.integers()))
@settings(max_examples=500)
def test_sort_properties(lst):
    result = my_sort(lst)
    assert len(result) == len(lst)          # 长度不变
    assert all(result[i] <= result[i+1]     # 已排序
               for i in range(len(result)-1))
    assert sorted(result) == sorted(lst)    # 元素相同

</details>


Q3: [💻 Algorithms — 2D Grid Backtracking]

Word Search(单词搜索)在深度复习中讲到了一个关键优化:原地标记已访问 vs 用 visited 集合。解释为什么原地修改 board[r][c] = '#' 在这里是正确且更优的,以及在什么情况下你 不应该 这样做?

Word Search uses in-place marking (board[r][c] = '#') instead of a visited set. Why is this correct and faster here? When should you NOT do this?

<details><summary>显示答案 / Show Answer</summary>

为什么原地标记有效:

  • 回溯时我们会恢复原始值:board[r][c] = temp,所以不会破坏输入(对当前路径而言)
  • 避免了每次递归调用的 O(1) set 查找开销(实际上 set 查找也是 O(1),但减少了对象创建/GC 压力)
  • 更重要的是:空间从 O(m×n) 降到 O(1)(不计递归栈),在内存敏感场景有意义
def dfs(r, c, k):
    if k == len(word): return True
    if not (0 <= r < rows and 0 <= c < cols): return False
    if board[r][c] != word[k]: return False

    temp = board[r][c]
    board[r][c] = '#'          # 标记访问
    found = any(dfs(r+dr, c+dc, k+1) for dr,dc in dirs)
    board[r][c] = temp          # 回溯恢复
    return found

什么时候 不应该 原地修改:

  1. 函数要保证输入不变:调用方可能依赖原始 board 进行多次独立查询
  2. 并发场景:多线程同时搜索同一个 board 会数据竞争
  3. 需要记录完整路径:原地标记只知道"访问过",不知道"第几步到达"
  4. board 值有业务含义:修改会产生副作用(比如 board 是实时状态)
**口诀:** 回溯 + 单线程 + 只关心可达性 → 原地标记更优雅。需要不可变输入或路径信息 → 用 visited 集合。

</details>


💡 复习巩固记忆,螺旋式上升。回溯三兄弟 + 测试三剑客,相互印证才是真正理解。

📅 明天继续新内容!Day 96 coming up 🚀