Column 1 · Cracking the Oyster:先问对问题,再选对表示
按第二版Column 1重建friendly conversation、precise problem statement、external/multi-pass/bitmap design、bit-vector implementation、preconditions与time-space-I/O证书。
从“怎样排序磁盘文件”这个错误问题开始
Column 1以一通求助电话展开。程序员问“怎样排序一个disk file”,直觉答案是external merge sort:生成sorted runs,写work files,再多路merge。算法当然正确,却可能解决了一个比真实需求更一般、更昂贵的问题。
先预测:若records其实只是distinct integers,所有values都来自一个不大的known range,不携带payload,而且只需ascending output,还需要comparison sort和temporary files吗?
破解珍珠问题()的关键不是先写算法,而是先把外壳撬开,找到隐藏的structure。
1.1 A Friendly Conversation:约束从追问中出现
A friendly conversation不是闲聊,而是需求发现过程。应依次问:
- Records是什么? 仅是integers,还是key加payload?
- 数量与range? 最多m个records,key落在0到n−1。
- Duplicates? 若无duplicate,输出是set的ordered listing。
- Memory和time? 约一兆字节级memory,顺序I/O可接受。
- 运行频率? One-off script和daily production值得投入的设计不同。
- Failure contract? Out-of-range、duplicate、malformed input如何处理?
原问题一旦精确,sorting不再是“重排m个records”,而是“表示finite universe中的一个subset,再按universe order枚举”。
1.2 Precise Problem Statement:把每个形容词换成数值
精确问题陈述()可写成:
输入至多m个distinct nonnegative integers,每个value严格小于n;在bounded memory中输出这些integers的ascending sequence。
核心preconditions:
输出y必须满足:
且:
这四条分别约束range、uniqueness、membership preservation和order。若contract允许duplicates,set equality会丢multiplicity;若record带payload,integer membership不足以恢复原record。
1.3 Program Design:先列方案,再用contract筛选
程序设计()至少考虑三条路线。
External merge sort
把memory能容纳的chunks分别排序并spill,最后merge。它处理general records和duplicates,I/O model可预测,但需要temporary storage、run metadata和merge code。
Multiple-pass algorithm
若memory只能表示range的一小段,就把domain切成intervals。每遍scan完整input,只处理当前interval;每遍结束按位扫描并输出。
设每遍bitmap能表示B个keys,则passes:
总输入读取约p·m records,无work files。它以sequential I/O换memory,不是“更慢一定更省空间”的抽象口号,而是明确可计算的tradeoff。
Bitmap or bit vector
若完整domain bitmap能装入memory,读取input once,令bit[x]=1,再从0到n−1扫描所有bits并输出set positions。No comparisons、no temporary files。
Bitmap()利用了本题三项罕见组合:finite dense domain、no duplicates、no payload。
1.4 Representation:one bit per possible integer
若domain size为n,理论space正好n bits:
Ten million possible values需要10,000,000 bits,也就是1,250,000 decimal bytes,约1.19 MiB。称作“约一兆字节”时必须说明MB/MiB和array overhead,不能让口头预算代替byte calculation。
用32-bit unsigned words实现时:
Array word count:
Power-of-two word size允许用shift与mask:
enum { BITS_PER_WORD = 32, SHIFT = 5, MASK = 31 };
size_t word_index(uint32_t i) { return i >> SHIFT; }
uint32_t bit_mask(uint32_t i) { return UINT32_C(1) << (i & MASK); }用UINT32_C(1)而不是signed 1,避免把signed 1左移到sign bit带来的C undefined behavior。uint32_t也让word width进入type contract。
1.5 Implementation Sketch:set、clear、test
实现草图()只需三种bit operations:
void set_bit(uint32_t *bits, uint32_t i) {
bits[word_index(i)] |= bit_mask(i);
}
void clear_bit(uint32_t *bits, uint32_t i) {
bits[word_index(i)] &= ~bit_mask(i);
}
bool test_bit(const uint32_t *bits, uint32_t i) {
return (bits[word_index(i)] & bit_mask(i)) != 0;
}Initialization不必逐bit clear;word-parallel zero fill更直接:
const size_t words = (n + BITS_PER_WORD - 1) / BITS_PER_WORD;
uint32_t *bits = calloc(words, sizeof(*bits));
if (bits == NULL) fail("allocation");calloc返回all-bits-zero storage,恰好表示empty set。若buffer复用,必须显式memset; stale bits会产生从未输入的phantom values。
1.6 Two-pass control flow
Bitmap sort有两个logical passes,但只读input一次:
- Load pass:read each integer x,validate,再set bit x。
- Domain pass:for i from 0 through n−1,test bit i;若set则emit i。
uint32_t x;
while (read_uint32(input, &x)) {
if (x >= n) fail("out-of-range key");
if (test_bit(bits, x)) fail("duplicate key");
set_bit(bits, x);
}
for (uint32_t i = 0; i < n; ++i) {
if (test_bit(bits, i)) write_uint32(output, i);
}Monotone scan自动产生sorted order;set bit保证每个valid input key至少输出一次;duplicate rejection与one-bit state保证exactly once。
1.7 Correctness invariant
Load loop处理前k个records后,保持:
Initialization时k=0,all bits zero。处理valid new x只改变x对应bit,且duplicate check保证它此前为zero,因此invariant保持。
Output loop扫描到i之前,保持:
- all input keys smaller than i已按ascending order输出;
- no key at least i已输出;
- output不含non-input value。
若test(i)为true就输出i,否则跳过。Loop结束于n时,membership与strict order同时满足。
1.8 Complexity不是只看CPU
设m为input record count、n为key universe size:
它不是comparison sorting的O(m log m),因为遍历了整个domain。若n与m同阶且domain dense,bitmap极佳;若n远大于m,domain scan和space可能比hash set加sort更差。
实际time包含:
- Parsing m integers。
- Sequential input read。
- n bit tests。
- Formatting/writing m outputs。
- Cache and memory bandwidth。
若text output占主导,bit tricks的nanosecond improvement没有意义。Column的原则是先改变representation和algorithm,再profile局部code。
1.9 Preconditions改变时,方案怎样变化
Duplicates matter
One bit只能表达zero-or-one。若需要输出multiplicity,可改为small counters:
若counts无known bound,使用hash map或external sorting。
Records carry payload
Direct-address table可存pointer/list而非bit,但space变为n machine words甚至更多。若domain sparse,balanced tree、hash table或comparison sort更合理。
Range unknown or huge
不能按max key直接allocate。可先做range discovery pass、coordinate compression、hash partition,或使用external sort。任何方案都需重新计算passes和temporary space。
Input may be malformed
Parsing failure、negative value、overflow、out-of-range和duplicate必须有明确behavior。静默截断或用modulo映射会把invalid input变成错误的valid output。
1.10 Time-space tradeoff与“不是tradeoff”的地方
Full bitmap fit时,它常常同时减少space、code complexity和I/O:不需要temporary files,也不需要general record structures。这不是牺牲time换space,而是使用更强problem information消除不必要工作。
Memory不足时才出现真正tradeoff:domain partition让每遍bitmap变小,但input scans增加。应以storage bandwidth和latency估算,不凭“sequential scan很快”猜测。
例如n=10,000,000,available bitmap memory为128 KiB,可表示1,048,576 keys,每次需约10 passes。若input为40 MB,至少读取约400 MB;加大到256 KiB可降到5 passes。这个结果能直接进入capacity review。
1.11 Principles:从这一颗珍珠带走什么
Column 1的principles不只是“位图很省空间”:
- Understand the problem:重复追问直到每个constraint可度量。
- Exploit structure:finite range、uniqueness和no payload决定representation。
- Compare designs:general external sort、multi-pass、bitmap各有valid domain。
- Make cost explicit:CPU、memory、input passes、work files和output都计费。
- Write invariants before code:membership和monotone emission证明correctness。
- Validate assumptions at boundaries:range、duplicates、allocation与parse errors。
- Prefer simple programs produced by precise thinking:simple code是analysis结果,不是省略需求。
1.12 独立验收证书
Production验收可记录:
- Contract:m、n、uniqueness、payload、memory、output format。
- Allocation:ceil(n/32) words,无overflow。
- Load invariant:processed keys与set bits等价。
- Output invariant:prefix complete、sorted、no extras。
- Complexity:one input scan、one domain scan、one output stream。
- Mutation tests:duplicate、n、n+1、negative、empty input、last valid key、stale buffer。
只有当所有checks通过,才能说bitmap implementation解决了精确问题。若业务contract变化,应重新回到friendly conversation,而不是继续给bitmap打补丁。
1.13 逐步运行路线
小结
- Cracking the Oyster从vague request中寻找决定性constraints。
- A friendly conversation把“sort a disk file”改写成finite-domain set enumeration。
- Precise Problem Statement必须量化range、count、memory、uniqueness、payload和failure behavior。
- Program Design先比较external merge、multi-pass与bitmap,再选择最符合contract者。
- Bitmap以one bit per possible key表示set,适合bounded、dense、distinct、payload-free keys。
- Implementation Sketch由word index、bit mask、set/clear/test、load pass和domain pass组成。
- Load invariant证明membership;monotone scan invariant证明ascending exact output。
- Bitmap sort用O(n) bits、O(m+n) time,性能取决于domain density与I/O。
- Multi-pass把smaller bitmap换成more input scans,是可量化的time-space-I/O tradeoff。
- Duplicates、payload、unknown range或malformed input都会改变representation和correctness contract。