建堆从最后一个非叶子节点(下标(n-1)/2)开始向下调整,因其子树已是合法子堆;若从根开始则子树无序导致调整失效;排序时heapify的堆尺寸需随i缩小至i,否则访问越界。

建堆时为什么从最后一个非叶子节点开始向下调整
因为叶子节点本身已满足堆性质,无需调整;从倒数第二层最右节点开始向上处理,才能保证每次调整的子树都已是合法子堆。若从根节点开始,子树尚未有序,向下调整会失效。
最后一个非叶子节点下标是 (n - 1) / 2(0-indexed 数组),对应父节点索引公式为 (i - 1) / 2,而其左子为 2 * i + 1、右子为 2 * i + 2。这个索引体系必须统一,否则越界或逻辑错位。
- 数组长度为
n时,下标范围是0到n-1,最后一个叶子节点下标是n-1,它父节点就是(n-1-1)/2 = (n-2)/2;但更稳妥写法是(n - 1) / 2(整除),C++ 中对正整数自动截断,二者等价 - 用
i >= 0做循环条件不安全,应明确限定起始:从i = (n - 1) / 2递减到0 - 若误从
i = n - 1开始向下调整,大概率触发2*i+1 >= n越界访问
向下调整(heapify)函数的关键判断顺序
向下调整的核心是“选大(或小)→ 比大小 → 换位置 → 继续沉”。顺序错了,比如先交换再比较,或漏判右子是否存在,就会破坏堆结构。
以最大堆为例,heapify(vector 中必须按如下逻辑执行:
- 先设
largest = i,再分别检查左子l = 2*i + 1和右子r = 2*i + 2是否在[0, n)范围内 - 只在
l arr[largest]时更新largest;同理判断右子——不能写成arr[l] > arr[i],否则无法传递多层影响 - 只有当
largest != i才交换,并递归调用heapify(arr, largest, n);若直接用 while 循环,需手动更新i = largest并继续 - 常见错误:
if (r arr[i])—— 这里和arr[i]比,而非和当前largest比,会导致右子即使比左子小也可能被选中
建堆过程与堆排序中两次 heapify 的区别
建堆阶段调用 heapify 是自底向上批量修正,时间复杂度 O(n);而堆排序主循环中每次取顶后调用 heapify 是自顶向下单次修复,共 n−1 次,总代价 O(n log n)。两者参数和目的不同,不能复用同一份“优化版”逻辑而不加区分。
- 建堆循环:
for (int i = (n-1)/2; i >= 0; i--) heapify(arr, i, n);—— 此时n是整个数组长度,不变 - 排序循环:
for (int i = n-1; i > 0; i--) { swap(arr[0], arr[i]); heapify(arr, 0, i); }—— 注意第二次heapify的第三个参数是i(堆大小收缩),不是n - 若在排序循环中错传
n,heapify会尝试访问已排好序的尾部元素,导致结果混乱 - 建堆不需要交换首尾,排序阶段才需要;混淆这两个动作会导致数组前半部分未重排
用 vector 实现时要注意的内存与边界细节
vector 是动态数组,下标随机访问快,但没内置堆操作,所有逻辑得手写。最容易出问题的是越界访问和迭代器失效,尤其在调试时用 at() 替代 [] 可捕获越界异常。
- 建堆前确保
arr.size() > 0,空容器调用(n-1)/2会因无符号整型回绕成极大值(size_type是 unsigned) - 推荐统一用
int n = static_cast转为有符号整型,避免(arr.size()); (n-1)/2在n==0时出错 - 向下调整中计算子节点时,必须每次都校验
l 和r ,不能只校验一次或依赖短路求值顺序(虽然&&是短路,但逻辑仍要显式清晰) - 如果后续要支持泛型(如
template),比较操作需用std::less或传入 comparator,不能硬写() >
实际写的时候,最常被忽略的是建堆循环的起始下标类型安全,以及排序阶段 heapify 的堆尺寸参数是否随 i 实时缩小——这两处一错,输出就不是排序结果,而是看似随机的错序数组。
C++免费学习笔记(深入):立即使用
在学习笔记中,你将探索 C++ 的入门与实战技巧!











