堆排序是一种基于堆数据结构的高效排序算法,其核心是通过构建大顶堆(升序)或小顶堆(降序),反复提取堆顶元素并调整堆结构。堆排序有递归和非递归两种实现方式,主要区别在于堆调整(Heapify)函数的实现方式。
一、核心原理回顾
堆是一种完全二叉树(除最后一层外,每层节点均满,最后一层节点靠左排列),分为两种类型:
- 大顶堆:每个父节点的值 ≥ 子节点的值(用于升序排序)。
- 小顶堆:每个父节点的值 ≤ 子节点的值(用于降序排序)。
堆的存储:通常用数组实现,若父节点索引为i,则:
- 左子节点索引 =
2i + 1 - 右子节点索引 =
2i + 2 - 父节点索引 =
(i - 1) / 2(向下取整)
堆排序的基本流程:
- 构建初始堆:将无序数组转换为大顶堆(父节点 >= 子节点)。
- 提取堆顶元素:将堆顶(最大值)与数组末尾元素交换,缩小堆的范围。
- 调整堆结构:对新的堆顶元素执行堆调整,维持大顶堆性质。
- 重复步骤 2-3:直到堆的范围缩小为 1,数组完全有序。
关键操作是堆调整(Heapify):当某个节点的值小于其子节点时,将其与最大的子节点交换,并递归或迭代处理受影响的子树。
示例演示:
以数组 [4, 10, 3, 5, 1] 为例,升序排序过程:
-
初始数组:
[4, 10, 3, 5, 1](对应完全二叉树) -
构建大顶堆:
- 最后一个非叶子节点是索引 1(值 10),无需调整。
- 处理索引 0(值 4):左子节点 5(值 5)、右子节点 2(值 3),最大值为 5 → 交换 4 和 5 →
[5, 10, 3, 4, 1]。 - 再次调整索引 3(值 4):无有效子节点,大顶堆构建完成 →
[10, 5, 3, 4, 1]。
-
排序阶段:
- 第 1 轮:交换堆顶 10 和末尾 1 →
[1, 5, 3, 4, 10],调整堆顶 1 → 重建大顶堆[5, 4, 3, 1, 10]。 - 第 2 轮:交换堆顶 5 和倒数第二元素 1 →
[1, 4, 3, 5, 10],调整堆顶 1 → 重建大顶堆[4, 1, 3, 5, 10]。 - 第 3 轮:交换堆顶 4 和倒数第三元素 3 →
[3, 1, 4, 5, 10],调整堆顶 3 → 重建大顶堆[3, 1, 4, 5, 10](无需调整)。 - 第 4 轮:交换堆顶 3 和倒数第四元素 1 →
[1, 3, 4, 5, 10],排序完成。
- 第 1 轮:交换堆顶 10 和末尾 1 →
二、递归版堆排序
递归版堆调整通过函数自身调用处理子树,代码简洁直观,但可能在数据规模极大时引发栈溢出。
void Heap_Adjust(int arr[], int start, int end) {
int lastindex = start;
int left = 2 * start + 1;
int right = 2 * start + 2;
if (left <= end && arr[left] > arr[lastindex]) {
lastindex = left;
}
if (right <= end && arr[right] > arr[lastindex]) {
lastindex = right;
}
if (lastindex == start)return;
int tmp = arr[start];
arr[start] = arr[lastindex];
arr[lastindex] = tmp;
Heap_Adjust(arr, lastindex, end);
}
void Heap_Sort(int arr[], int len) {
for (int i = (len - 1 - 1) / 2; i >= 0; i--) {
Heap_Adjust(arr, i, len - 1);
}
for (int i = 0; i < len - 1; i++) {
int tmp = arr[0];
arr[0] = arr[len - 1 - i];
arr[len - 1 - i] = tmp;
Heap_Adjust(arr, 0, len - 2 - i);
}
}
三、非递归版堆排序
非递归版堆调整通过循环迭代处理子树,避免了递归调用的栈开销,更适合大规模数据,但代码稍复杂。
void Heap_Adjust(int arr[], int start, int end) {
while (1) {
int lastindex = start;
int left = 2 * start + 1;
int right = 2 * start + 2;
if (left <=end && arr[left] > arr[lastindex]) {
lastindex = left;
}
if (right <=end && arr[right] > arr[lastindex]) {
lastindex = right;
}
if (lastindex == start)break;
int tmp = arr[start];
arr[start] = arr[lastindex];
arr[lastindex] = tmp;
start = lastindex;
}
}
void Heap_Sort(int arr[], int len) {
for (int i = (len - 1 - 1) / 2; i >= 0; i--) {
Heap_Adjust(arr, i, len-1);
}
for (int i = 0; i < len - 1; i++) {
int tmp = arr[0];
arr[0] = arr[len - 1 - i];
arr[len - 1 - i] = tmp;
Heap_Adjust(arr, 0, len - 2 - i);
}
}
四、递归版与非递归版对比
-
时间复杂度
- 建堆阶段:O (n)(整体复杂度,非 O (n log n))。
- 排序阶段:O (n log n)(n-1 次堆调整,每次调整 O (log n))。
- 总时间复杂度:O(n log n)(最好、最坏、平均情况均相同,稳定性强)。
-
空间复杂度
- 仅使用常数级临时变量,空间复杂度为 O(1),属于原地排序。
-
稳定性
- 不稳定排序:堆调整过程中,相等元素的相对位置可能被交换(如父子节点值相等时交换会改变顺序)。
-
适用场景
- 大规模数据排序(时间复杂度稳定,空间效率高)。
- 实时获取最大值 / 最小值的场景(如优先队列)。
- 内存受限的环境(原地排序特性)。
| 特性 | 递归版堆排序 | 非递归版堆排序 |
|---|---|---|
| 实现复杂度 | 代码简洁,逻辑直观(递归调用) | 代码稍复杂,需手动控制循环流程 |
| 空间复杂度 | O (log n)(递归调用栈开销) | O (1)(仅用少量临时变量,原地排序) |
| 性能 | 递归调用有额外开销,大规模数据可能较慢 | 无函数调用开销,性能更优 |
| 栈溢出风险 | 数据规模极大时可能栈溢出(递归深度 O (log n)) | 无栈溢出风险,适合超大规模数据 |
| 可读性 | 高,符合堆调整的递归逻辑 | 中等,需理解循环迭代的子树处理 |
五、选择建议
- 递归版:适合小规模数据或教学场景,代码易写易理解。
- 非递归版:适合大规模数据或生产环境,无栈溢出风险,性能更稳定。
两种版本的核心思想完全一致,仅堆调整的实现方式不同。实际应用中,非递归版因空间效率更高而更常用,尤其是在嵌入式系统或内存受限的环境中。

5513

被折叠的 条评论
为什么被折叠?



