堆排序的核心不在于调用make_heap和sort_heap,而在于手动维护堆结构;二者需成对使用才能完成排序,单独调用make_heap仅构建最大堆而非升序序列。

堆排序的核心是 make_heap 和 sort_heap 吗?
不是必须。标准库确实提供了 make_heap、push_heap、pop_heap、sort_heap 这组函数,但它们只是辅助工具,不等于“实现了堆排序”——真正实现堆排序,关键在于理解并手动维护堆结构。直接调用 sort_heap 前必须先调用 make_heap,而这两者加起来才等价于一次完整的堆排序过程。
常见错误是只调用 make_heap 就以为排好序了,结果发现容器只是满足最大堆性质(根最大),并非升序排列。
-
make_heap把区间转为最大堆(默认),时间复杂度 O(n) -
sort_heap在已建好的堆上执行原地排序,输出升序,时间复杂度 O(n log n) - 二者必须成对使用:
make_heap(v.begin(), v.end()); sort_heap(v.begin(), v.end());
手写 sift_down 时下标从 0 开始还是从 1 开始?
标准 C++ 容器(如 vector)索引从 0 开始,所以堆的父子关系必须按 0-based 公式算:对下标 i 的节点,左子为 2*i+1,右子为 2*i+2,父为 (i-1)/2(整除)。用 1-based 会越界或逻辑错乱,尤其在 vector 边界判断时极易出错。
性能上无差异,但兼容性和可读性差很多。所有 STL 实现和主流教材(如《算法导论》伪代码的 C++ 转译)都采用 0-based。
- 错误写法:
left = i * 2(这是 1-based,对应vector第二个元素,实际是v[1],但你想访问的是v[0]的左子,应为v[1]→ 正确,但后续全偏移) - 正确写法:
left = 2 * i + 1;,且必须检查left - 注意:比较时用
v[left] > v[largest],不是>=,否则稳定性受影响(虽堆排序本身不稳定,但避免意外交换)
为什么建堆用 for (int i = n/2 - 1; i >= 0; i--)?
因为完全二叉树中,下标 ≥ n/2 的节点全是叶子,无需下沉。最后一个非叶子节点下标是 n/2 - 1(整除),从它开始向上逐个 sift_down,才能保证每个子树都满足堆性质。顺序不能反过来:如果从 0 开始,父节点下沉后子树可能已被破坏,导致最终不是堆。
这个循环是建堆 O(n) 时间的关键——不是对每个元素都做 O(log n) 下沉,而是利用大部分节点高度小、下沉距离短的特性。
- 若数组长 10,
n/2 - 1 == 4,从下标 4 开始,即第 5 个元素(0-indexed),它可能是倒数第二层最右节点 - 用
size_t时注意:当n == 0或1,n/2 - 1会变成极大正数(无符号回绕),必须先判空 - 推荐写法:
int last_non_leaf = n > 0 ? n/2 - 1 : -1;,循环条件i >= 0
堆排序原地进行,但 vector 的 pop_back 会影响堆结构吗?
会,而且必须主动管理。堆排序的“原地”指不额外分配 O(n) 空间,但需要把已确定位置的最大值移到末尾,并缩小堆范围。典型做法是:每次 pop_heap(或手写交换 + sift_down)后,用 swap(v[0], v[i]) 把堆顶换到当前未排序区尾部,再将堆长度减一(逻辑上,不真 pop_back)。如果真调用 v.pop_back(),后续 sift_down 访问越界风险极高。
真实场景中,你几乎不会在排序中途修改容器大小;所有操作都在固定大小的内存段内完成,仅靠一个边界变量(如 heap_size)控制有效堆长。
- 错误:
v.pop_back(); make_heap(v.begin(), v.end());—— 每次都重建整个剩余容器,O(n² log n) - 正确:维持
int heap_size = v.size();,每次swap(v[0], v[heap_size-1]); heap_size--;,再对[0, heap_size)调sift_down - STL 的
pop_heap干的就是这事:把堆顶移到末尾,然后在前n-1个元素上调用sift_down
堆排序最难的部分不在代码长度,而在准确控制堆边界和下标运算——稍有不慎,sift_down 就会漏节点或越界,而这种 bug 往往只在特定输入长度下暴露。
C++免费学习笔记(深入):立即使用
在学习笔记中,你将探索 C++ 的入门与实战技巧!











