Mercor + SpaceXSI 首轮准备2026-10-29 · D10 · 离 11/16 还有 18 天

这两天卡住的地方,
一页一张图

先看两场面试和两张地图,再一处一处讲:哪里卡住了、正确的是什么、面试里要说哪几句。讲解用中文,代码用英文,代码的输出都是真跑出来的。

← → 或空格翻页 · G 打开目录 · 页码在右下角
全貌

11/16 下午两场:先 30 分钟只说,再 60 分钟边写边讲

Mercor

2:00–2:30 PM PSTSoftware Engineer, Agents · 30 分钟
形式
只说不写。快问快答 3–5 道:排序、数排列的概率、读代码挑毛病、幂等和重试、轻设计。
看什么
先 frame 再解;往下讲一层机制和代价;不等问就说复杂度;不会就照实说怎么推;被质疑马上跟着调。
不能用
任何 AI、第二块屏幕、搜答案。

SpaceXSI

3:00–4:00 PM PSTSoftware Engineer, Agent Harness · 60 分钟
形式
共享屏幕,本地 IDE 写 Python,和一位工程师做一道带真实工程取舍的题。
看什么
边做边讲、问澄清问题;先解出来,不求代码漂亮;从头跑到尾没有大 bug。
邮件点名
要提前准备的只有一样:所用语言的 filesystem interface。
不能用
agent、Cursor tab 这类能看到代码的工具。AI chat 只能问语法。
10/20D1 开始 10/25门槛 1 过了 10/29今天 D10 11/1门槛 2:第一次打分的 mock 11/8门槛 3 11/14按真实时间彩排 · 11/15 休息 11/16面试
全貌 · 地图

SpaceXSI 地图:13 块积木 8 块 ✓,题型 E(P03)正在做

流程

  • 问清楚:输入、输出、出错、规模
  • 计划写成 4 行注释
  • 第一版边写边讲(每个决定说一句)
  • 自己造几个文件测边界
  • 35 分钟接住改需求:先说改哪里
  • 讲取舍:内存、大文件、很多文件19–21

积木

  • 1 遍历所有文件
  • 2 安全地读一个文件
  • 3 相对路径
  • 4 数数、分组
  • 5 排序取前 k 个
  • 6 写文件
  • 7 解析一行
  • 8 JSON 和 JSONL
  • 9 os.walk 剪枝
  • 10 文件哈希
  • 11 大文件:seek、从末尾读7–11
  • 12 安全写入22
  • 13 跑命令

题型

  • A 遍历,每个算一个值,排序5
  • B 解析日志做统计
  • C 搜索:agent 用的 grep
  • D 找重复文件21
  • E 大文件:P03 tail -f 正在做12–18
  • F agent 工具:edit_file19–20
不看提示能做出来 学过,还要提示 还没开始 7–11这份 PPT 的页码,点了跳过去
全貌 · 地图

Mercor 地图:面经里的 12 类题,这两天在排序和计数上

题型(面经)

  • 排序 M1、M224–29
  • 数排列的概率 M330
  • 读代码:缓存 M4
  • 读代码:扣钱 M6、幂等 M8
  • 读代码:拼 CSV M5
  • 读代码:带重试的退款 M7
  • 轻设计 M9、M10
  • 数据结构取舍 M11
  • 贝叶斯 M12
  • LLM 应用快问 M14
  • 简历 M15
  • 概率难题 M13

流程

  • 先 frame:一句话说这题本质是什么
  • 说方法,再往下讲一层
  • 不等问,说时间和空间复杂度
  • 答短:一题 3–5 分钟
  • 被质疑马上调整,不争
  • 不会就直说:我会这样推31
  • 结尾问一个真问题

积木

  • 复杂度五档
  • 排序的机制24–28
  • 排列和计数30
  • 条件概率、贝叶斯
  • 线程和锁
  • 缓存
  • 部分失败
  • 幂等和重试
  • 数据库
  • 拼字符串
  • set 和 Bloom filter
  • LLM 应用
全貌 · SpaceXSI 主线

文件题都是四步:准备 → 找文件 → 每个算一个值 → 排序返回

先把这四步练到不用想,再往上叠大文件、出错、软链接、改需求。门槛 1 的三道新题就是这么过的(10/25,28/28,约 48 分钟)。

from pathlib import Path

def largest_files(root, k):
    root = Path(root)
    pairs = []
    for p in root.rglob("*"):
        if p.is_symlink() or not p.is_file():
            continue
        try:
            size = p.stat().st_size
        except OSError:
            continue
        pairs.append((p.relative_to(root).as_posix(), size))
    return sorted(pairs, key=lambda t: (-t[1], t[0]))[:k]
①

准备

root = Path(root),装结果的 list 或 dict。

积木 3
②

找文件

rglob("*") 或 os.walk;跳过软链接、不是文件的、.git。

积木 1、9
③

每个算一个值

大小、行数、哈希……读不了就 except OSError 跳过这个文件。

积木 2、4、10
④

排序返回

大的在前、同样大按名字:(-t[1], t[0]);路径用相对 root 的。

积木 5、3
第一部分

SpaceXSI这两天卡住的地方

  1. 7seek 的第二个数:从哪儿数
  2. 8为什么用 "rb"
  3. 9tail:从末尾一块一块往回读
  4. 10tail 的代码:三段、五个问题
  5. 11splitlines()[-n:]
  6. 12–18P03 tail -f:题目、字节、完整代码、逐行讲
  7. 19edit_file:两个 agent 同时改
  8. 20edit_file:文件有 2 GB
  9. 21找重复:内存花在哪
  10. 22安全写入:临时文件 + 一步换
SpaceXSI · 积木 11 大文件10/28 14:00 你问:seek(0, 2) 为什么是 2

seek 的第二个数是“从哪儿开始数”:0 开头,1 现在的位置,2 末尾

f = open("practice/drill/files/poem.txt", "rb")
f.seek(0, 2)    # 46: the end. seek returns where it landed
f.seek(-6, 2)   # 40: 6 bytes before the end
f.read()        # b'sweet\n'
f.seek(0)       # 0: same as f.seek(0, 0)
f.read(5)       # b'roses', now at 5
f.seek(3, 1)    # 8: 3 more from where we are
第二个数从哪儿数例子(poem.txt,46 个字节)
0(不写就是 0)开头seek(40) → 40
1现在的位置在 5:seek(3, 1) → 8
2末尾seek(0, 2) → 46 · seek(-6, 2) → 40
  • seek 返回跳到的位置,所以 pos = f.seek(0, 2) 顺手拿到了文件大小。
  • 0、1、2 是 Unix / C 早年定的编号,os.SEEK_END 就是 2。记不住,面试里可以问语法。
SpaceXSI · 积木 11 大文件10/28 14:48 你问:这题为啥要 binary

为什么用 "rb":位置按字节数,文本模式按字读,一个中文字是 3 个字节

单位对不上

practice/drill/out/zh.txt 里是“第一行\n第二行\n”:6 个字 × 3 个字节 + 2 个 \n = 20 个字节。seek、tell、pos 数的是字节,文本模式的 read(n) 数的是字。像物理里米和秒混着用。

放到 tail 里

块的边界是算出来的(每块 4096 个字节),可能正好切开一个字。所以用 "rb" 读字节,拿到整行以后再 decode。

10/28 实测:把 tail 改成文本模式,三行中文,块大小 3、4、6 都报错,4096 碰巧没事;纯英文永远测不出来。另外文本模式不让 seek(-6, 2)(UnsupportedOperation)。

SpaceXSI · 积木 11 大文件10/28 讲课的那张表

tail:从末尾一块一块往回读,\n 比 n 多一个就停

notes.txt 是 28 个字节(三行:hello world、bye、hello again),每块 8 个字节,要最后 2 行。

轮pos这一块读到data(新读的拼在前面)\n 个数停吗
128 → 20b"o again\n"b"o again\n"11 ≤ 2,接着读
220 → 12b"bye\nhell"b"bye\nhello again\n"22 ≤ 2,接着读
312 → 4b"o world\n"b"o world\nbye\nhello again\n"33 > 2,停

最后 splitlines()[-2:] → ['bye', 'hello again'](第 11 页)。开头 4 个字节 hell 一直没读。10 GB 的日志要最后 20 行也一样只读最后几块:try_tail 最后一行 read 4096 of 1088895 bytes。

SpaceXSI · 积木 11 大文件10/28 14:24 看着抄了三遍:“进不了脑子”

tail 的代码分三段,中间那段一行回答一个问题

def tail(path, n=10, block_size=4096):
    if n <= 0:
        return []
    with open(path, "rb") as f:
        pos = f.seek(0, 2)
        data = b""
        while pos > 0 and data.count(b"\n") <= n:
            step = min(block_size, pos)
            pos = pos - step
            f.seek(pos)
            data = f.read(step) + data
    lines = data.splitlines()[-n:]
    out = []
    for line in lines:
        out.append(line.decode("utf-8", errors="replace"))
    return out
10/28 19:52 空文件凭记忆写,5 分钟,try_tail 七行全对,隐藏测试 15/15

准备:n ≤ 0 先返回([-0:] 是整个 list);"rb" 打开;跳到末尾,pos 就是文件大小;data 从空的 bytes 开始。

  1. ①还要不要读?两个条件:没到开头(pos > 0),而且 \n 还不够多(<= n)
  2. ②这一块几个字节?一块,但别越过第 0 个字节:min(block_size, pos)
  3. ③新位置在哪?往前挪一块:pos − step
  4. ④跳过去f.seek(pos)
  5. ⑤读到的放前面还是后面?前面:它在文件里更靠前

收尾:切成行、留最后 n 行(下一页);只对留下的行 decode。

怎么记住的:别抄,盖住了自己想每一行在回答哪个问题。隔了 4 小时再写,反而一次就对。

SpaceXSI · 积木 11 大文件10/28 14:50 你说:这一句我也有点不懂

splitlines()[-n:] 是两件事:先切成行,再只留最后 n 行

为什么要等 \n 比 n 多一个才停

要最后 n 行,就得看到倒数第 n 行前面那个 \n:它标出这一行从哪开始。只有 n 个 \n 时,最前面那段可能只是半行。要 2 行,看到 3 个 \n 才停:bye 前面那个 \n 保证 bye 是完整的。

  • 行数比 n 少:一直读到开头(pos = 0),最前面那段就是文件的第一行,完整的,全给。
  • n = 0:[-0:] 就是 [0:],整个 list。所以一开头先 return []。
data = b"o world\nbye\nhello again\n"
data.splitlines()       # [b'o world', b'bye', b'hello again']
data.splitlines()[-2:]  # [b'bye', b'hello again']
[10, 20, 30][-0:]       # [10, 20, 30]: -0 is 0
SpaceXSI · P03 tail -f · 1 / 710/28 20:24 写到 seek(offset) 停住:“写不出来”

P03:日志一直在变长,agent 隔一会儿来要“上次以后新写完的整行”

  • offset:文件里的位置,按字节数。它前面的 agent 都看过了。第一次传 0。
  • 返回两样:新的整行(list,去掉 \n,变成字),和下次要传的 offset。
  • 半行不给:最后一段没有 \n,说明服务器还在写;下次它会完整地回来。
  • 文件变小了:比 offset 还短,是被清空了或换成了新文件(日志轮转),当 offset 是 0。
  • 文件不存在:让 FileNotFoundError 抛出来。
你 10/28 问的三个

是不是二进制读:是,"rb"。offset 是干什么的:记上次看到第几个字节。服务器又改了文件怎么办:这就是题目本身,每次只读新加的那段。

SpaceXSI · P03 tail -f · 2 / 7try_follow.py 前五行

try_follow 的五次调用:offset 0 → 24 → 30 → 41 → 41 → 10

上次看过了 这次给出去的整行 半行,先不给 传进来的 offset 返回的新 offset ↵ = \n

关键是第 ② 次:半行 GET /ab 不给,新 offset 停在它前面(30);第 ③ 次它写完了,整行 GET /about 才给出来。第 ⑤ 次文件只有 10 个字节,比 41 小,从 0 开始。

SpaceXSI · P03 tail -f · 3 / 7放大第 ② 次调用

一次调用:从 offset 读到末尾,切在最后一个 \n 后面

  1. ①f.seek(24):跳过看过的 24 个字节。
  2. ②data = f.read():读到末尾,13 个字节 b"GET /\nGET /ab"。
  3. ③end = data.rfind(b"\n") = 5:最后一个 \n 在 data 里的位置。
  4. ④data[:end + 1] = 前 6 个字节,全是整行 → ['GET /']。
⑤ 新 offset:两种位置别混

end = 5 是 data 里的位置(从 0 数)。data 是从文件第 24 个字节开始的,所以那个 \n 在文件里是 24 + 5 = 29。新 offset 要在它后面:24 + 5 + 1 = 30。

SpaceXSI · P03 tail -f · 4 / 7参考代码(L4,我写的,不算你写出来的)

完整代码:15 行,五个问题

def follow(path, offset):
    with open(path, "rb") as f:
        size = f.seek(0, 2)              # the file size
        if size < offset:                # smaller: emptied or replaced
            offset = 0                   # so start over
        f.seek(offset)                   # skip what was seen
        data = f.read()                  # all new bytes, to the end
    end = data.rfind(b"\n")              # the last \n; -1 if none
    if end == -1:
        return [], offset                # no complete line yet
    lines = data[:end + 1].splitlines()  # complete lines only
    out = []
    for line in lines:
        out.append(line.decode("utf-8", errors="replace"))
    return out, offset + end + 1         # right after that \n
这一份跑过:try_follow 七行全对,隐藏测试 13/13
  1. ①文件现在多大?比 offset 小怎么办?第 3–5 行
  2. ②跳到 offset,读到末尾第 6–7 行
  3. ③最后一个 \n 在哪?没有怎么办?第 8–10 行
  4. ④整行切出来,变成字第 11–14 行
  5. ⑤新 offset 是多少?第 15 行
  6. 跟 tail 一样,一行回答一个问题。写的时候卡住了,先说卡在哪一个。
SpaceXSI · P03 tail -f · 5 / 7

逐行讲(上):打开、量大小、跳过去、读到末尾

def follow(path, offset):
    with open(path, "rb") as f:
        size = f.seek(0, 2)
        if size < offset:
            offset = 0
        f.seek(offset)
        data = f.read()
    end = data.rfind(b"\n")
    if end == -1:
        return [], offset
    lines = data[:end + 1].splitlines()
    out = []
    for line in lines:
        out.append(line.decode("utf-8", errors="replace"))
    return out, offset + end + 1
1

两个参数:文件路径,上次停在第几个字节。第一次传 0。你问过要不要默认值:不用,忘了传会直接报错,比悄悄从头再给一遍好。

2

"rb":offset 按字节数(第 8 页)。文件不存在时 open 自己抛 FileNotFoundError,题目就要它抛,不写 try。

3

跳到末尾,seek 返回跳到的位置 = 文件大小(第 7 页)。第 ② 次调用时 size 是 37。

4–5

文件比上次看到的位置还短:被清空了,或换成了新文件。旧位置没意义了,从 0 读。用 < 不用 <=:相等只是“没有新内容”。

6

跳到上次停下的地方,看过的不读。等于 f.seek(offset, 0);你写的 pos = f.seek(offset, 0) 也对,只是 pos 用不上。

7

不带数字的 read():从现在的位置读到末尾,拿到 bytes。第 ② 次是 13 个字节;没有新内容时是 b""。出了 with,文件自动关上。

SpaceXSI · P03 tail -f · 6 / 7

逐行讲(下):找最后一个 \n、切整行、变成字、算新 offset

def follow(path, offset):
    with open(path, "rb") as f:
        size = f.seek(0, 2)
        if size < offset:
            offset = 0
        f.seek(offset)
        data = f.read()
    end = data.rfind(b"\n")
    if end == -1:
        return [], offset
    lines = data[:end + 1].splitlines()
    out = []
    for line in lines:
        out.append(line.decode("utf-8", errors="replace"))
    return out, offset + end + 1
8

rfind 从右往左找最后一个 \n,返回它在 data 里的位置(从 0 数);一个都没有返回 -1。第 ② 次是 5。

9–10

没有一整行:最后那段还在写,先不给,offset 不动。刚被清空过的话,这时 offset 已经是 0,正好是题目要的 [] 和 0。

11

data[:end + 1] 是从头到最后那个 \n(包括它),全是整行,后面的半行不要。splitlines 在每个 \n 切开、去掉 \n;空行留着:b"a\n\nb\n" → [b'a', b'', b'b']。

12–14

字节变成字,只对整行做:整行不会把一个中文字切开。坏字节变成“�”,不报错:只是给 agent 看,不写回文件。

15

返回两样。data 从文件第 offset 个字节开始,那个 \n 在文件里是 offset + end,新 offset 在它后面再 +1。第 ② 次:24 + 5 + 1 = 30。

SpaceXSI · P03 tail -f · 7 / 7每一种都在 try_follow 上跑过

容易写错的六处,和面试里要说的四句

写成这样try_follow 里会看到
半行也给,新 offset 直接用文件大小第 ② 次给出 'GET /ab',第 ③ 次给出 'out':一行被切成两截
最后忘了 +1offset 停在 \n 上,以后每次都多出一个空行 ''
变小的判断写成 size <= offset第 ④ 次没有新内容(41 = 41),也当成变小,四行全部重给一遍
文件变小了不管第 ⑤ 次从 41 读,读到 b"",'restarted' 一直拿不到
用 split(b"\n") 代替 splitlines()末尾多一个 b'',每次多给一个空行
每行 strip(),或用文本模式 "r"行首行尾的空格被吃掉;位置按字节、read 按字,中文会错位
面试里要说的
  • 为什么 "rb":offset 是字节位置。
  • 为什么只给整行:最后一段可能还在写,下次它会完整地回来。
  • 为什么变小就从 0:被清空或轮转了,旧位置没意义。
  • 为什么快:只读上次以后新加的。try_follow 最后一行:一百多万字节的文件只读了 9 个字节。
SpaceXSI · edit_file 取舍 110/27 19:17 你说:写之前再查一次哈希

两个 agent 同时改:查哈希和写回之间有空当,再查一次只把空当变小

你答对的和差的

答对了:一个人的改动会被另一个盖掉(图里 B 的改动被 A 盖掉)。办法说的是“写之前再查一次哈希”:查完到写回之间还是能插进来,只是空当小了。“查了再做”不是一步,这叫竞态(race condition)。

要说的:用一把锁把“读、查、写”包起来

同一时间只让一个 edit 进去,后来的等着;轮到它时再算哈希,就会正确地报“文件改过了”。

agent A锁读 · 查 ✓ · 写回放锁
agent B等着……锁读 · 查 ✗ → 报错:文件改过了
SpaceXSI · edit_file 取舍 210/27 19:17 你答:不知道

2 GB 的文件:整个读进来改,内存里至少三份,约 6 GB

先说

给个大小上限,超了报一个说清楚的错。agent 改的是代码文件;几个 GB 的日志不该用查找替换来改。

被追问再说:一块一块来

一块一块读,一块一块写进临时文件;每块末尾留 len(old) − 1 个字符,接到下一块前面一起找,防止 old 跨在两块中间;出现第二次就删掉临时文件、报错;最后 os.replace 换过去。内存只要一块。

SpaceXSI · 题型 D 找重复文件10/27 01:18 你说:这一块内存我是真不懂

找重复:一千万个文件,内存大头是按大小分组的那个 dict

取舍 1:所有文件大小都一样(你答对了)

大小那层一个也筛不掉,每个文件都要算快速哈希;开头 4 KB 也都一样的话,最慢的就变成全量哈希。

取舍 2:一千万个文件,内存在哪(你猜对了是 d)

数格子:d 里每个文件一个 Path,n 个,最大;quick、hashed 只装重复的;chunk 一块一块换,不随 n 变。实测一个 Path 放进 dict 约 290 字节:一千万个约 3 GB,存字符串约 1 GB。往下一层:扫两遍,第一遍只数每个大小有几个,第二遍只存大小重复的。

SpaceXSI · 积木 12 安全写入10/27 01:13 凭记忆写:漏了删临时文件

安全写入:先写同一个文件夹里的临时文件,再一步换过去

import os
from pathlib import Path

def atomic_write(path, text):
    path = Path(path)
    tmp = path.parent / (path.name + ".tmp")
    try:
        with open(tmp, "w", encoding="utf-8") as f:
            f.write(text)
        os.replace(tmp, path)
    except Exception:
        tmp.unlink(missing_ok=True)
        raise
except 那三行(10/26 你说没看明白,10/27 凭记忆漏了中间一行)

except Exception 接住任何常见的错;tmp.unlink(missing_ok=True) 删掉写了一半的临时文件,还没建出来也不报错;raise 把错原样扔出去,让调用的人知道没写成。那次还把 os.replace(tmp, path) 的逗号写成了句点。

第二部分

Mercor这两天卡住的地方

  1. 24合并两堆:只比最前面的
  2. 25n 和 log n 从哪来
  3. 26空间 O(n) 和 Timsort
  4. 27M1 五步讲法
  5. 28最慢的排序:n · n!
  6. 29M2 五步讲法
  7. 30咖啡店:分母是 24
  8. 31被问住了先推,不说“不知道”
Mercor · M1 排序10/27 15:13 第一遍:没说合并具体怎么做

合并两堆排好的:只比两堆最前面那个,小的拿走

  • 为什么只看两个就够:每堆是排好的,最前面那个就是这堆最小的。
  • 多少步:每比一次放下一个,n 个数最多 n 步 → O(n)。
  • 一堆空了:另一堆剩下的直接接上,不用再比。
  • 两堆不用一样大:[1, 2, 3, 5] 和 [4] → 拿 1、2、3、4,右堆空了,接上 5。

示范在 practice/d08/merge_demo.py,会打印每一步。

Mercor · M1 排序10/28 15:35 被追问 log n 从哪来,你答:不知道

n 和 log n 从哪来:每轮堆的大小翻一倍,每轮每个数放下一次

  • 几轮:1 → 2 → 4 → 8,翻倍几次到 n,就是 log₂ n 轮。8 个 3 轮,16 个 4 轮,7 个也是 3 轮(往上取整:落单的那堆原样留到下一轮)。
  • 每轮几步:每个数在某一次合并里放下一次 → n 步。
  • 一共:n × log n。16 个:4 轮 × 16 = 64 步。
  • 数字:100 万个 20 轮,约 2,000 万步;理牌那种 O(n²) 约 5,000 亿步。

最下面一层只是拆开摆好,不算一轮。

Mercor · M1 排序10/28 两次:Timsort 漏了 / 说成接近 O(n)

空间 O(n) 从新 list 来;Timsort 只在本来快排好时接近 O(n)

归并用空间换时间:多开 n 格,换来 n log n。

Timsort 是归并的改良版:先找出本来就排好的段(run),再合并。最坏还是 O(n log n);本来快排好的,一整段走一遍就认出来,接近 O(n)。

10/28 早上漏了 Timsort;晚上说成“接近 O(n)”,少了条件:只在本来快排好的时候。

Mercor · M1 排序 · 范例看完关掉,用英文讲一遍

M1 五步讲法:详细讲一个比 O(n²) 快的排序

1归结成什么

我讲归并排序。两堆各自排好的数合成一堆很便宜,走一遍就行;所以先拆到每堆一个,再一层层两两合并。O(n log n),代价是多用 O(n) 内存。

2做法

一直对半分到每堆一个;合并两堆时比两堆最前面的数,小的放进新 list,那堆往后挪一个;一堆空了,另一堆剩下的直接接上;一层层合并到只剩一堆。

3例子

[5, 2, 7, 1] → [5] [2] [7] [1] → [2, 5] [1, 7] → 2 比 1 拿 1,2 比 7 拿 2,5 比 7 拿 5,接上 7 → [1, 2, 5, 7]。

4复杂度

每层每个数放下一次 = n 步;每轮堆大小翻倍,log n 轮 → O(n log n)。最直接的排法是 O(n²),100 万时 2,000 万步对 5,000 亿步。合并要新 list,额外 O(n)。Python 的 sorted 是 Timsort,归并的改良版,本来快排好的数据接近 O(n)。

5边界

空 list、只有一个数,直接返回;不管输入是乱的、排好的、倒着的,都是 n log n,不会变慢。

10/28 晚漏过的:例子;为什么是 log n 层(每轮翻倍);Timsort 只在本来快排好时才接近 O(n)。

Mercor · M2 排序10/27 19:27 “讲不出来了” · 10/28 看完范例说进不去

最慢的排序:试遍所有顺序,n! 种 × 每种看 n 步 = O(n · n!)

nn²(理牌那种)n · n!(试遍所有顺序)
525600
10100约 3,600 万
20400约 4.9 × 10¹⁹(两万多年)
  • 列出所有顺序:每个数轮流站第一个,剩下的用同样的办法列(函数调用自己,list 短一个);只剩一个或零个时只有一种顺序,直接返回。
  • 还有更差的:随机洗牌,洗到排好为止。平均约 n! 次,没有上限,可能永远排不完。
  • 真要列排列:Python 自带 itertools.permutations。
Mercor · M2 排序 · 范例看完关掉,用英文讲一遍

M2 五步讲法:说一个比 O(n²) 慢的排序,怎么列出每个排列

1归结成什么

最慢、但还是对的排法,就是把所有顺序都试一遍,每试一种看一眼是不是从小到大。n 个数有 n! 种顺序,每种看一眼要 n 步 → O(n · n!),比 O(n²) 慢得多。

2做法

一种一种列出所有顺序;每种从头走到尾,相邻两个比一下,都是前面的小,就找到了,返回。

3怎么列

每个数轮流站第一个,剩下的用同样的办法列(函数调用自己,list 短一个);只剩一个或零个时直接返回,递归在这里停。[A, B, C]:A 站第一 → ABC、ACB;B 站第一 → BAC、BCA;C 站第一 → CAB、CBA。3 × 2 × 1 = 6 种。

4复杂度和数字

n! 种 × n 步。n = 10 约 3,600 万步;n = 20 约 4.9 × 10¹⁹ 步,按 10/25 测的速度两万多年。n! 比 n 的任何次方都长得快。

5被追问更差的

随机洗牌,洗到排好为止。平均要洗约 n! 次,而且没有上限,运气差可能永远排不完。真要列排列,Python 自带 itertools.permutations。

Mercor · M3 数排列的概率10/27 20:02 分子数对了,分母写成 16

咖啡店:四杯排名次,24 种顺序一样可能,A 店占前两名的 4 种 → 1/6

每组从左到右是第 1、2、3、4 名。橙色是 A 店的两杯,蓝色是 B 店的两杯;绿底是 A 店两杯正好占了前两名。

  1. ①数顺序:一共 4! = 24 种;A 的两杯在前两名、B 的在后两名:2! × 2! = 4 种 → 4/24 = 1/6。
  2. ②只看哪两杯在前:4 杯里挑 2 杯,C(4, 2) = 6 种,A 店那一对占 1 种 → 1/6。
  3. ③一个名次一个名次:第一名是 A 店的 2/4;第二名是另一杯 A 店的 1/3(不放回)→ 1/6。
分母为什么不是 16

16 是按“可以重复”数的。排名次每杯只占一个名次,不能重复 → 4! = 24。先问自己:同一个东西能用几次?

Mercor · 流程:不会就直说怎么推10/27、10/28 两次:“不知道”

被问住了,先说“我推一下”,拿一个小例子出声走一遍

1

“log n 从哪来?”

10/28 Mercor M1,你答:不知道

推:拿 16 个数。一个一堆 → 两个 → 四个 → 八个 → 十六个,翻了 4 次;2⁴ = 16,所以 4 = log₂ 16。每一轮每个数放下一次,16 步。一共 16 × 4。

2

“文件有 2 GB 怎么办?”

10/27 edit_file 取舍,你答:不知道

推:整个读进来一份 2 GB;partition 切开又一份;拼起来再一份,约 6 GB。太大了 → 先给个上限;真要支持,就一块一块读写。

三步

① 我没直接做过,我推一下 → ② 拿一个小例子出声走 → ③ 推不动了,就说卡在哪一步。面试官要看的是你怎么想;一句“不知道”,这一问就结束了。

收尾10/28 定的,写进了 CLAUDE.md

怎么练:10/28 定的三条

1

口述题也先给正确答案

你告诉我答案 我加深正确的记忆 不想加深错误的记忆

练习时:看五步范例 → 关掉 → 用英文讲 → 对着范例说漏了哪几点。mock 和门槛里不给。

2

凭记忆写,语法卡住了可以查

spacex 会考的这么细没

跟面试一样:help(),或者问 Claude,只答语法,不给思路和代码结构。要记的是思路:每一行在回答哪个问题。

3

一次最多开两条线

我感觉我们开的threads太多了

一条 SpaceXSI、一条 Mercor。一条收尾了(不看提示写出来或讲出来)才开下一条。

tail 就是这么过的:看着抄了三遍,“进不了脑子” → 停了 4 小时 → 空文件凭记忆写,5 分钟,15/15。

收尾D10 · 10/29 周四

明天两件事,一条 SpaceXSI,一条 Mercor

①

SpaceXSI · P03 凭记忆写

这份 PPT 里的代码是我给的,不算你写出来的。照 tail 的办法:关掉 PPT,在空文件里凭记忆写 follow,跑 try_follow,七行都对上再说“写完”。

过了再改一次需求、问一个取舍。卡住了,先说卡在五个问题的哪一个。

②

Mercor · M1 不看范例讲

用英文讲一遍归并:合并怎么做、一个例子、n 和 log n 从哪来(每轮翻倍)、空间从哪来(新 list)、Timsort(本来快排好时才接近 O(n),最坏还是 n log n)。

先睡够。

卡点图解
01 / 33
横过来看更清楚