堆排序
堆排序(Heap Sort)是一种利用堆这种数据结构设计的高效排序算法。其基本思想是:将序列构建成大顶堆(或小顶堆),每次取堆顶元素放到序列末尾,调整剩余元素为新堆,重复直到全部有序。
点击“执行一步”观察算法状态变化。
- 将待排序序列建成大顶堆
- 取出堆顶元素(最大值),与末尾元素交换,堆长度减 1
- 调整剩余元素为新大顶堆
- 重复步骤 2-3,直到堆长度为 1
伪代码如下:
function heapSort(A, n):
buildMaxHeap(A, n)
for i = n-1 downto 1:
swap(A[0], A[i])
heapify(A, 0, i)
function buildMaxHeap(A, n):
for i = n/2-1 downto 0:
heapify(A, i, n)
function heapify(A, i, n):
largest = i
l = 2*i+1
r = 2*i+2
if l < n and A[l] > A[largest]:
largest = l
if r < n and A[r] > A[largest]:
largest = r
if largest != i:
swap(A[i], A[largest])
heapify(A, largest, n)
- 堆是一棵完全二叉树,用数组存储
- 大顶堆:每个结点的值都大于等于其左右孩子,堆顶是最大值
- 小顶堆:每个结点的值都小于等于其左右孩子,堆顶是最小值
- 结点 i 的左孩子为 2i+1,右孩子为 2i+2,双亲为
// 将以 i 为根结点的子树调整为大顶堆
void Heapify(int A[], int i, int n) {
int largest = i;
int l = 2 * i + 1, r = 2 * i + 2;
if (l < n && A[l] > A[largest]) largest = l;
if (r < n && A[r] > A[largest]) largest = r;
if (largest != i) {
int tmp = A[i]; A[i] = A[largest]; A[largest] = tmp;
Heapify(A, largest, n); // 递归调整子树
}
}
// 建堆:从最后一个非叶子结点开始,自底向上调整
void BuildMaxHeap(int A[], int n) {
for (int i = n / 2 - 1; i >= 0; i--)
Heapify(A, i, n);
}
// 堆排序
void HeapSort(int A[], int n) {
BuildMaxHeap(A, n);
for (int i = n - 1; i > 0; i--) {
int tmp = A[0]; A[0] = A[i]; A[i] = tmp; // 堆顶(最大)移到末尾
Heapify(A, 0, i); // 调整剩余部分
}
}
堆排序与优先队列的关联
堆是优先队列最常用的实现:入队(插入),出队(取最大/最小)。Dijkstra 算法、任务调度、Top K 问题等场景都用到了堆。
- 最好/最坏/平均情况:
- 空间复杂度:
- 稳定性:不稳定排序
习题 1
对数组 进行堆排序,写出初始建堆和第一次交换后的堆结构。
解题思路:
本题考查堆排序的初始建堆与第一次交换。核心思想是先把序列调整成大顶堆使最大元素位于堆顶,再把堆顶与堆尾元素交换、缩小堆并重新向下调整,使最大元素逐趟归位到序列末尾。
详细步骤:
- 初始建堆:[10, 5, 3, 4, 1]
- 第一次交换:[1, 5, 3, 4, 10],再调整为新堆:[5, 4, 3, 1, 10]
- 说明:交换后堆顶为 1 而它的孩子为 5,不满足大顶堆性质,需要向下调整,最终得到新堆 [5, 4, 3, 1, 10],最大元素 10 已归位到序列末尾。
最终答案:
如上,第一次交换后最大元素归位。
习题 2
堆排序的时间复杂度和空间复杂度分别是多少?
解题思路:
本题考查堆排序的复杂度分析。建堆自底向上调整,代价为 ;随后做 次交换与堆调整,每次调整的路径长度不超过 ,故总时间复杂度为 且性能稳定。
详细步骤:
- 时间复杂度 :建堆 ,其后 趟每趟堆调整 。
- 空间复杂度 :原地调整堆结构,只需常数个辅助变量。
- 对比参考:快速排序平均 但最坏可达 ;归并排序稳定为 但需要 辅助空间。
最终答案:
时间 ,空间 。
