算法(12):Mergesort(序)-6.0
核心主线先帮你把骨架立起来归并排序与前几节排序算法的根本不同在于它不是通过交换相邻元素来逐步消除逆序对而是通过“分治”把问题不断拆小在局部有序的基础上构建全局有序。物理上它引入了辅助数组aux作为临时存储空间。它的存在意味着归并排序不是原地排序——它需要额外占用N个引用的内存空间。几个你可能已经注意到但容易与之前经验混淆的地方辅助数组 aux 的位置它在每次合并操作中被反复使用复制原数组到 aux再从 aux 合并回原数组。辅助数组在一次完整的sort调用中被创建在递归过程中复用而不是每次合并都新建一个。merge操作的物理动作将两个已经有序的连续子数组左半[lo, mid]和右半[mid1, hi]合并成一个有序的大子数组。它通过两个指针i和j分别在左右两半中前进每次比较aux[i]和aux[j]将较小的那个写回a[k]。N log N的物理来源合并排序的递归树高度为log N因为每次把数组分成两半每层需要处理的总元素数为N。所以总工作量是N log N次比较以及6N log N次数组访问复制和移动。这与选择/插入排序的N²有本质差异。递归与迭代两种实现自顶向下的递归版本sort递归调用自身和自底向上的迭代版本sz从 1 开始翻倍。两者的比较次数相同但迭代版本在典型系统上慢约 10%原因涉及函数调用开销和缓存行为。稳定性归并排序是稳定的因为合并时当aux[j]与aux[i]相等时merge操作会优先取左半部分i侧的元素从而保持相等元素的原始相对顺序。这与你之前学的选择排序不稳定、希尔排序不稳定形成对比。Q这个merge sort和之前的那种elementary sort的区别是什么按照这种说法来讲的话我唯一能感受到的就是多加了一个辅助数组。但是如果你说跟前几节排序算法的根本不同在于它是先做局部有序然后再做全局有序那我感觉希尔排序其实也是差不多的呀。A你观察到的“希尔排序和归并排序好像都在做局部有序”确实是一个很深的直觉。我来把它们的物理本质差异直接剖开你就明白为什么归并排序被单独划为一个分水岭而希尔排序仍然属于“基础排序Elementary”的范畴。1. “局部有序”的定义完全不同希尔排序的“局部有序”h-有序。它不要求数组存在任何连续的内存块是完全有序的它只要求相隔h的位置之间满足顺序。这是逻辑上的、交错的有序切片。归并排序的“局部有序”左半段[lo, mid]是内存地址连续的一块整体完全有序右半段[mid1, hi]也是内存地址连续的一块整体完全有序。这是物理上的、连续地址的块有序。物理后果归并排序把数组看作“两个已经完整的、连续的物理区域”而希尔排序把数组看作“若干条独立的、交错的逻辑链”。2. 单个元素的移动路径不同希尔排序元素是跨越间隔跳跃移动的。一个元素可以从数组最右端直接跨过h个位置跳到中间再跨过更小的h跳到左边。它的路径是长距离随机跳转。归并排序元素是在连续的内存块内顺序移动的。在合并过程中元素从一个连续的块左或右被复制到另一个连续的块辅助数组或原数组。它的路径是块内的顺序搬运。物理后果希尔排序在长距离跳跃时容易产生缓存未命中因为跳转的地址不连续归并排序在合并时访问的是连续地址段对缓存更友好虽然它需要额外的辅助数组。3. 控制流的逻辑结构不同最关键的一点希尔排序是一个迭代递进的过程。它在不断缩小h但不知道自己离最终有序还有多远也不保证某个小范围内的元素在后续过程中不被再次扰动。它是一种“试探性逼近”。归并排序是一个递归保证的过程。在合并之前左右两半已经被递归完全排序了。归并操作接收的是两个确定有序的块它执行的是一个确定正确的合并动作。归并排序的每一步都是在已知结果确定的基础上向前推进的。物理后果希尔排序的正确性依赖于复杂的数学证明h-排序的性质而归并排序的正确性可以直接通过归纳法证明如果左右半段有序合并后必然整体有序逻辑链极其简单且坚固。4. 最坏情况下的性能保障希尔排序即使使用最优的h序列其最坏情况下的比较次数也远高于N log N目前已知最好的是N^(4/3)或N^(3/2)量级。在某些糟糕的h序列下它甚至退回到N²。归并排序无论输入数据是什么顺序升序、降序、乱序比较次数始终严格受N log N上界约束。物理后果归并排序提供了确定性的性能保障。希尔排序提供了平均情况下比插入排序快但无法在最坏情况下抗衡归并排序。5. 辅助数组带来的质变你提到“唯一能感受到的就是多加了一个辅助数组”这个感受是对的但它的物理含义是归并排序愿意牺牲内存额外N空间来换取“块搬运”的效率。在希尔排序中元素交换是在原数组内部完成的虽然空间节省但每个元素要经过多次比较和交换。在归并排序中它把整个数组当成连续的块来搬运读入aux排序后写回a这种“批量处理”的模式是基础排序无法做到的——因为基础排序从来没有“两个完整的连续有序块”这个概念。总结两者的本质分界线希尔排序是用“跨步跳跃”来快速消除远距离逆序但本质仍是单元素交换或赋值的线性过程。归并排序是用“分治切割”把数组变成两个确定有序的连续块然后用“块归并”把两个块线性合并成一个更大的连续有序块。希尔排序的思维基础是“优化插入排序的移动距离”。归并排序的思维基础是“优化问题规模先解决小块再组合大块”。后者引入了“递归”和“辅助数组”这两个新工具从而把算法复杂度从超平方级别拉到了严格的N log N。Q好吧那我想做mergesort最大的原因是为了提高缓存命中率对吗你能不能再仔细跟我介绍一下缓存命中率这个概念呢我感觉在很多地方见到过他无论是算法的速度计算上还是AI token的价格上我想知道他们是一样的吗。以及我们现在对于算法的时间复杂度计算方式是每一条指令都看作使用同样的时间再去计算执行了几条指令。在这种情况下依旧去讨论缓存命中吗A问题拆得非常好而且你最后那个疑惑“既然算法复杂度假设每条指令等时为何还要讨论缓存”触及了理论模型RAM模型和物理现实硬件架构之间的根本断层。我们逐个击破。1. 归并排序的最大动机是为了提高缓存命中率吗不是。归并排序被发明的最大动机是保证最坏情况下的O(N log N)时间复杂度和稳定性。缓存命中率的提升是它连续内存访问模式带来的一个副产品而不是设计它的首要目标。相比之下快速排序你下一节会学到对缓存更加友好因为它原地分割访问更集中但快排在最坏情况下是O(N²)。归并排序牺牲了“原地性”需要额外数组换取了“确定性的对数线性时间”和“顺序访问模式”。2. 缓存命中率到底是什么硬件物理层面缓存是 CPU 内部的一块极小的、但访问速度极快的静态内存SRAM。主存DRAM虽然大但访问速度慢几十到几百倍。CPU 读取数据时不是按字节一个一个去主存拿的而是以“缓存行Cache Line”为单位通常是 64 字节批量加载。当 CPU 需要读取一个变量时它先检查这个变量所在的 64 字节块是否已经在缓存中缓存命中Cache Hit数据已在缓存中CPU 直接取用耗时约 1-3 个时钟周期。缓存未命中Cache Miss数据不在缓存中CPU 必须暂停等待从主存把这 64 字节加载进缓存耗时约 100-300 个时钟周期慢了 2 个数量级。命中率 命中的访问次数 / 总访问次数。影响命中率的核心物理规律是“局部性原理Locality”空间局部性如果你访问了地址A那么地址A1很可能也会被访问因为 64 字节的一整块都被加载进来了。时间局部性如果你访问了地址A那么短时间内很可能再次访问A数据被留在缓存里了。3. AI Token 价格里的“缓存命中”和 CPU 缓存是同一个东西吗不是同一个物理东西但名字都叫“缓存”逻辑含义一致——都是“避免重复昂贵计算/传输”。CPU 硬件缓存存储原始数据字节。命中意味着省去了从主存取数的时间。AI Token 缓存KV Cache在大语言模型推理中系统会把已经计算好的Key和Value矩阵向量存起来。当用户重复输入相同的前缀比如“请翻译以下英文”系统直接复用之前算好的 K/V跳过重复的矩阵乘法计算。物理差异CPU 缓存是硬件自动管理的CPU 自动搬运和替换你无法直接控制。AI KV 缓存是软件显式存储的系统把中间计算结果存到显存里命中意味着省去昂贵的浮点运算和显存读写。逻辑相同它们都利用了“重复访问”的模式用快速存储缓存/显存挡住慢速存储主存/重新计算。4. 既然算法课算复杂度时假设每条指令等时为什么还要讨论缓存这是最核心的理论与现实碰撞点。算法课的时间复杂度大O记号运行在“随机存取机RAM模型”下。这个模型假设访问任何内存地址的时间都是相同的一个单位时间。这个假设在 20 世纪 70 年代以前大致成立因为那时 CPU 和内存速度差距不大。但现在的物理硬件完全不遵守这个假设。CPU 主频远高于内存总线频率一次缓存未命中的代价几百个时钟周期足够 CPU 执行几百条简单指令。结论是大O复杂度告诉你“趋势”比如归并和快排都是N log N它们处理的指令数量级大致相同。缓存命中率告诉你“常数因子”即那个被大O忽略的C因为缓存未命中会导致 CPU 空转两个同为N log N的算法实际运行时间可能相差 3-5 倍。例如快排的随机访问模式通常比归并排序的顺序写 aux 模式在某些数据量下更贴合缓存所以快排往往比归并排序跑得快尽管比较次数略多。5. 回到归并排序的缓存行为归并排序的访问模式是“严格顺序的批量搬运”复制到aux连续读原数组连续写辅助数组。归并回a连续读辅助数组的两段连续写回原数组。这种模式对缓存非常友好空间局部性极强因为 CPU 会预取接下来的 64 字节。尽管它有aux数组的额外空间开销和复制开销但因为访问是连续的它的常数因子在现代 CPU 上并没有比随机访问如希尔排序或选择排序差太多而且它是严格保证N log N的。这才是归并排序在缓存层面被接受的原因不是因为它“提高”了命中率而是因为它尽管多了一次搬运但搬运得极其规整没有打乱缓存预取机制。