一.堆是一种完全二叉树数据结构,分为两种类型:
- 最大堆:每个节点的值(只有非叶子节点才有子节点)都不小于其子节点的值。根节点是最大值。
- 最小堆:每个节点的值(只有非叶子节点才有子节点)都不大于其子节点的值。根节点是最小值。
二.堆排序过程图示:
以升序为例(建大根堆:每一个父节点(parent)都必须大于其儿子节点(child))
我们从最后一个非叶子节点开始调整。
建堆过程解释:
1.最后一个非叶子节点为13,13的儿子节点65,13<65,交换这两个节点。
2.其次非叶子节点为38,38的儿子左节点97比其儿子右节点76大,38<97,交换这两个节点。
3.其次为非叶子节点49,49的儿子左节点97比其儿子右节点65大,49<97,交换这两个节点。
4.非叶子节点49,49的儿子左节点38比其儿子右节点76小,49<76,交换这两个节点。
开始排序阶段过程解释:
1.交换堆顶和堆尾元素。
2.恢复为大堆,不包括97。
3.循环直至所有数据排序成功。
三.简易代码描述:
void Swap(int* a, int* b) {
int temp = *a;
*a = *b;
*b = temp;
}
void AdjustDown(int* a, int n, int parent) {
int child = 2 * parent + 1; // 左子节点索引
if (child >= n) return; // 如果没有子节点,直接返回
// 找到左右子节点中较大的那个
if (child + 1 < n && a[child] < a[child + 1]) {
child++; // 右子节点存在且较大
}
// 如果父节点小于较大的子节点,则交换并继续调整
if (a[parent] < a[child]) {
Swap(&a[parent], &a[child]);
AdjustDown(a, n, child); // 递归调整子树
}
}
void HeapSort(int* a, int n) {
// 构建大根堆
for (int i = (n - 1 - 1) / 2; i >= 0; i--) {
AdjustDown(a, n, i);
}
// 进行排序
for (int end = n - 1; end > 0; end--) {
Swap(&a[0], &a[end]); // 将最大值移到末尾
AdjustDown(a, end, 0); // 调整剩余部分
}
}
四.代码解释:
Swap
: 交换两个元素的函数。AdjustDown
: 从给定的父节点开始,调整堆,使得以该节点为根的子树满足堆性质。HeapSort
: 首先构建大根堆,然后逐步将堆顶元素(最大值)放到数组末尾,并调整剩余部分。
五.部分代码解释:
代码片段一:
for (int i = (n - 1 - 1) / 2; i >= 0; i--) {
AdjustDown(a, n, i);
}
1.堆的构建:
在堆排序中,我们首先需要构建一个大根堆(或最小堆),以便能够逐步提取最大值(或最小值)。这个构建过程的关键是调整每一个非叶子节点,使得整个树满足堆的性质。
2.计算非叶子节点的起始位置
(n - 1 - 1) / 2
这个公式用来计算最后一个非叶子节点的索引。具体解释如下:
假设有一个包含 7 个元素的数组 [3, 5, 1, 8, 9, 6, 2]
,我们来找出最后一个非叶子节点的索引:
- 数组大小
n = 7
。 - 最后一个元素的索引
n - 1 = 6
。 - 倒数第二个元素的索引
n - 1 - 1 = 5
。 - 最后一个非叶子节点的索引 计算为
(n - 1 - 1) / 2 = 5 / 2 = 2
。
所以,最后一个非叶子节点的索引是 2
。
图示说明:
3.调整非叶子节点
从最后一个非叶子节点开始,向前遍历到根节点,并对每个非叶子节点调用 AdjustDown
函数来调整堆。AdjustDown
函数的作用是确保以当前节点为根的子树满足堆的性质,即最大堆中的每个父节点都大于其子节点。
这段代码的目的是从最后一个非叶子节点开始,逐步向前调整堆,以构建一个有效的大根堆。这个过程是堆排序的关键步骤之一。
代码片段二:
for (int end = n - 1; end > 0; end--) {
Swap(&a[0], &a[end]); // 将最大值移到末尾
AdjustDown(a, end, 0); // 调整剩余部分
}
1.for (int end = n - 1; end > 0; end--)
: 这是一个倒序循环,从数组的最后一个元素开始,逐步向前移动,直到第一个元素。这是为了将堆顶的最大元素(即数组的第一个元素)逐渐移动到当前“未排序”的数组末尾位置。
2.Swap(&a[0], &a[end])
: 交换堆顶元素(数组的第一个元素,最大值)和当前“未排序”部分的最后一个元素。这将最大值移到已排序部分的末尾。
3.AdjustDown(a, end, 0)
: 调整堆的剩余部分。交换后,堆的性质可能被破坏,需要通过 AdjustDown
函数来重新调整剩余部分以维持最大堆的性质。
六.堆排序的时间复杂度:
- 构建初始堆:需要对所有非叶子节点进行堆化操作,总体时间复杂度为 (O(n))。
- 排序过程:在每次交换堆顶元素(最大值)和末尾元素后,调整堆的时间复杂度为 (O(\log n)),这个操作重复 (n-1) 次,总时间复杂度为 (O(n \log n))。
因此,堆排序的总时间复杂度是 (O(n \log n)),无论是最坏情况、平均情况还是最佳情况。