形式陈述
输入、模型与保证
Funnel Sort 在缓存无关模型 理路 缓存无关模型 Cache-oblivious model 算法代码不使用缓存参数,但在理想缓存中对所有块尺度分析 miss。 中完成比较排序 理路 比较排序 Comparison sorting 仅通过元素两两比较确定排列次序的排序模型。 :输入为连续存放的 N 个定长记录,输出保持全部记录并按键非降序排列。空输入和单项直接返回;程序的基例阈值是固定常数,不读取块大小 B 或缓存容量 M 。
在全相联、最优替换的理想缓存中,若 M = Ω ( B 2 ) 且内存至少容纳固定数量的块,对 N ≥ 1 有
Q ( N ) = O ( 1 + N B ( 1 + log M N ) ) 次缓存缺失,比较工作为 O ( N log ( N + 1 ) ) 。在这个 tall-cache 范围内,它与比较排序 I/O 下界 理路 外存排序 I/O 下界 external-memory sorting lower bound · I/O sorting lower bound 在原子记录的两层 I/O 模型中,排序需要与多路归并上界匹配的块传输次数。 同阶,也等价于截断小输入对数后的外排表达式。开头的常数覆盖不足一块的非空输入。
带批量接口的 k-merger
一个 k -merger 保存合并状态;一次调用输出接下来的 k 3 项,输入提前耗尽时只输出剩余有效项并报告结束。已排序输入通过 FIFO 缓冲提供。常数 k ≤ 3 直接比较各个流头;一般 k 则递归构造较小 merger。
先看每层平方根都与递归批量兼容的标准写法,例如 k = 2 2 j (j ≥ 0 ,k = 2 为常数基例),而不只要求最外层 k 是平方数:k 个左 merger 各合并 k 条输入,每次产生 k 3 / 2 项,写入容量为 2 k 3 / 2 的中间循环队列;一个右 k -merger 再合并这些队列。右 merger 每次执行前,先给少于 k 3 / 2 项且尚未耗尽的中间队列调用一次对应左 merger。填充后队列至少有一个右调用所需的最大消耗量,又不会超过容量;随后调用右 merger 输出 k 3 / 2 项。重复 k 3 / 2 次便输出 k 3 项。
一般整数扇入,包括 25 这类仅最外层为平方数的情形,采用下面统一的带保护版本:它为递归预取留下空间,不能只把所有平方根独立取整。一个保守实现取 q = ⌊ k ⌋ 个组,每组至多 s = ⌈ k / q ⌉ 条流,左侧用 s -merger,右侧用 q -merger。令 C q 是右 merger 全部内部缓冲及尾批次最多能保存的记录数,在构造时即可计算。一次右调用输出至多 q 3 项,按记录守恒,从任一输入读取的项数至多为 T = q 3 + C q 。
因此每条中间队列预留 T + s 3 容量;调用右 merger 前,反复调用对应左 merger,直到该队列至少有 T 项或左输入耗尽。一次补充至多增加 s 3 项,所以不会溢出。最后一个右批次若超出本次 k 3 输出配额,多出的有效项保留供下次调用,至多 q 3 − 1 项;空输入使用耗尽标志处理。因为 C q = O ( q 2 ) ,这些前瞻空间仍只给每条队列增加 O ( k ) 项,整体空间保持 O ( k 2 ) 。所有容量都只依赖扇入,不读取 M , B 。
正确性按网络递归:左 merger 产生各自输入并集的有序前缀,中间 FIFO 保持这些前缀的次序,右 merger 再取各个前沿中的最小项。补充规则保证一个尚有更小项的分支不会因缓冲暂空而被误当成耗尽。因此每次输出恰为全体剩余输入的最小项,且每条记录恰输出一次。
排序递归
对超出固定基例的 N ,取 k = ⌈ N 1 / 3 ⌉ ,把输入分成 k 段,各段长度至多 ⌈ N / k ⌉ ,先递归排序,再用一个 k -merger 合并。因 k 3 ≥ N ,一次批量调用足以输出全部记录;最后一段较短及空段按同一耗尽规则处理。
直觉
Funnel Sort 用递归网络在许多未知缓存尺度上同时形成多路归并。靠近叶子的缓冲吸收细粒度输入,靠近根的较大缓冲成批供应全局最小前沿;当某个子漏斗恰能驻留缓存时,它可以连续生产大量输出,成本便按整块摊到记录上。
图片加载失败 递归二路漏斗与分级缓冲
例子与边界
八条短流如何穿过漏斗
设有八条排序流,首项依次为
1 , 4 , 6 , 9 , 12 , 15 , 18 , 22. 底层四个二路 merger 先把相邻流的小批量前缀写入四个缓冲;中层两个 merger 再各自按需填充更大的输出缓冲;根只比较这两个中层缓冲的首项。根先输出 1,随后补充该路径上的空槽,而不会扫描其余七条流。
若含 1 的输入下一项为 20,根接下来看到的全局最小值是 4。此时其他支路已经把 4、6 等候选顺序放在靠近根的缓冲里;磁盘上的长尾并未因每个输出而随机访问。一个缓冲耗尽时才触发 refill,refill 产生一段而非一个记录。
这个例子说明批量填充的方向,却不以八条流证明渐近界。真正分析在某个递归子漏斗首次能装入 M 时,把连续批量输出与边界队列的传输共同摊到每条记录。
缓冲容量怎样进入 I/O 证明
在上述递归兼容的标准扇入下,merger 的存储满足
S ( k ) ≤ ( k + 1 ) S ( k ) + O ( k 2 ) = O ( k 2 ) . 把递归对象连续存放,循环队列的头尾各保留一个块,则 r 次顺序入队、出队只需 O ( 1 + r / B ) 次缓存缺失。取足够小常数 a :当 k ≤ a M 时,O ( k 2 ) 个内部单元与 k 条输入队列的边界块可同时驻留,因为 tall-cache 保证 k B = O ( M ) 。这次合并产生 k 3 项,成本为 O ( 1 + k + k 3 / B ) 。
较大的 merger 中,右子 merger 调用 k 3 / 2 次;所有左调用总共至多 k 3 / 2 + 2 k 次,因为生成的项最终进入输出或留在总容量 2 k 2 的中间缓冲中。检查缓冲合计花 O ( k 2 ) 次访问。因此其缺失数满足
Q m e r g e ( k ) ≤ ( 2 k 3 / 2 + 2 k ) Q m e r g e ( k ) + O ( k 2 ) . Frigo 等人的 Lemma 4.3 以缓存可容纳子问题为基例,在归纳式中保留支付缓冲检查的低阶余量,得到
Q m e r g e ( k ) = O ( 1 + k + k 3 B ( 1 + log M k ) ) . 上面的一般整数版本使 q , s = Θ ( k ) ,保留 Θ ( k 2 ) 空间与 O ( k 3 / 2 ) 次子调用;尾批次被缓冲,其同阶界使用相应的上下取整和常数基例。
对排序,在分析中遇到 N ≤ a M 时,整个子问题及当时活跃的 merger 可驻留缓存,因而只付 O ( 1 + N / B ) ;这只是分析基例,代码仍按固定阈值递归。其余情况的整幂写法为
Q ( N ) ≤ N 1 / 3 Q ( N 2 / 3 ) + O ( N B log M N ) . 除以 N 后,各递归层中的对数依次按 1 , 2 / 3 , ( 2 / 3 ) 2 , … 缩小;其和为常数倍的 log M N ,再加缓存基例的扫描量,得到开头的界。一般长度按同样的平衡分段和尾段规则处理。
失败边界与实现条件
缓存替换按 ideal-cache 的最优策略分析。真实 LRU、组相联缓存和预取器可能改变常数;“cache-oblivious”表示代码不调参,不表示物理存储没有块或任意替换策略都保留同一证明。
记录若为可变长对象,B 个记录一块的换算不再成立。比较器若访问外部大对象,比较本身也会产生未计入的 I/O。稳定性不是结构自动给出的性质;需要在键相等时以原始位置打破平局。
普通归并排序 理路 归并排序 Merge sort 递归排序两半并线性合并的稳定比较排序算法。 的二路扫描每一层都顺序,但仍有 Θ ( log N ) 层。知道 M , B 的多路外存 mergesort 可直接选 M / B 路;Funnel Sort 用递归 merger 同时适配未知的多级缓存,代价是更复杂的缓冲调度与较大的常数。
推论与应用
该算法证明缓存无关设计也能达到比较外存排序的最优 I/O 量级,并可由同一代码适配多级缓存。它更适合作为递归布局与缓冲分析的范型;实际实现若更重视常数、SIMD 或显式设备参数,已知 M , B 的多路归并往往更直接。
参考资料
Matteo Frigo, Charles E. Leiserson, Harald Prokop and Sridhar Ramachandran, Cache-Oblivious Algorithms , ACM Transactions on Algorithms, 2012,§4、Lemmas 4.1–4.3 与 Theorems 4.4–4.5:批量 merger、循环缓冲、缓存递推及排序界。
Harald Prokop, Cache-Oblivious Algorithms , MIT Master's Thesis, 1999.
Jeffrey Scott Vitter, Algorithms and Data Structures for External Memory , Foundations and Trends in Theoretical Computer Science, 2008.