一、树与二叉树基础概念
1.1 树的基本结构
树是一种非线性的数据结构,由n(n≥0)个节点组成的有穷集合。当n=0时称为空树,非空树具有以下特性:
有且仅有一个根节点
其余节点可分为m(m≥0)个互不相交的有限集合
1.2 二叉树定义与特性
二叉树是每个节点最多有两个子树的树结构,通常称为左子树和右子树。
重要概念:
孩子节点:一个节点的直接下级节点
父节点:拥有孩子节点的上级节点
叶子节点:没有孩子节点的节点(终端节点)
分支节点:至少有一个孩子节点的节点
1.3 完全二叉树与满二叉树
满二叉树:所有层都达到最大节点数的二叉树
完全二叉树:除最后一层外,其他层都是满的,且最后一层节点尽量靠左排列
二、堆的基本概念与特性
2.1 堆的定义
堆是一种特殊的完全二叉树,满足以下性质:
大顶堆:每个节点的值都大于或等于其孩子节点的值
小顶堆:每个节点的值都小于或等于其孩子节点的值
2.2 堆的数组表示
由于堆是完全二叉树,可以用数组高效存储:
节点i的左孩子:2*i + 1
节点i的右孩子:2*i + 2
节点i的父节点:(i-1)/2
三、堆排序算法原理
3.1 算法核心思想
堆排序利用堆的特性进行排序,主要步骤:
-
构建初始堆(大顶堆或小顶堆)
-
将堆顶元素与末尾元素交换
-
调整剩余元素为新堆
-
重复步骤2-3直到排序完成
3.2 排序过程图解
以数组 [4, 10, 3, 5, 1] 构建大顶堆为例:
初始数组: [4, 10, 3, 5, 1]
树形表示:
4
/ \
10 3
/ \
5 1
构建大顶堆过程:
1. 调整节点1(10): 已满足
4
/ \
10 3
/ \
5 1
2. 调整节点0(4): 与10交换
10
/ \
4 3
/ \
5 1
3. 调整节点1(4): 与5交换
10
/ \
5 3
/ \
4 1
最终大顶堆: [10, 5, 3, 4, 1]
四、堆排序详细过程
4.1 建堆过程
从最后一个非叶子节点开始,自底向上调整堆。
4.2 排序过程
-
交换堆顶与末尾元素
-
堆大小减1
-
调整堆结构
-
重复直到堆大小为1
五、堆排序C语言实现
5.1 基础堆排序实现
#include <stdio.h>
void adjustHeap(int arr[], int i, int n) {
int temp = arr[i];
for (int k = 2 * i + 1; k < n; k = 2 * k + 1) {
if (k + 1 < n && arr[k] < arr[k + 1]) k++;
if (arr[k] > temp) {
arr[i] = arr[k];
i = k;
} else break;
}
arr[i] = temp;
}
void heapSort(int arr[], int n) {
for (int i = n / 2 - 1; i >= 0; i--) adjustHeap(arr, i, n);
for (int j = n - 1; j > 0; j--) {
int temp = arr[0];
arr[0] = arr[j];
arr[j] = temp;
adjustHeap(arr, 0, j);
}
}
void printArray(int arr[], int n) {
for (int i = 0; i < n; i++) printf("%d ", arr[i]);
printf("\n");
}
int main() {
int arr[] = {4, 10, 3, 5, 1};
int n = sizeof(arr) / sizeof(arr[0]);
printf("原数组: ");
printArray(arr, n);
heapSort(arr, n);
printf("排序后: ");
printArray(arr, n);
return 0;
}
5.2 优化版本实现
#include <stdio.h>
void swap(int *a, int *b) {
int temp = *a;
*a = *b;
*b = temp;
}
void heapify(int arr[], int n, int i) {
int largest = i;
int left = 2 * i + 1;
int right = 2 * i + 2;
if (left < n && arr[left] > arr[largest]) largest = left;
if (right < n && arr[right] > arr[largest]) largest = right;
if (largest != i) {
swap(&arr[i], &arr[largest]);
heapify(arr, n, largest);
}
}
void optimizedHeapSort(int arr[], int n) {
for (int i = n / 2 - 1; i >= 0; i--) heapify(arr, n, i);
for (int i = n - 1; i >= 0; i--) {
swap(&arr[0], &arr[i]);
heapify(arr, i, 0);
}
}
int main() {
int arr[] = {9, 4, 2, 7, 1, 8, 3};
int n = sizeof(arr) / sizeof(arr[0]);
printf("原数组: ");
for (int i = 0; i < n; i++) printf("%d ", arr[i]);
printf("\n");
optimizedHeapSort(arr, n);
printf("排序后: ");
for (int i = 0; i < n; i++) printf("%d ", arr[i]);
printf("\n");
return 0;
}
六、复杂度分析与性能比较
6.1 时间复杂度分析
建堆过程:O(n)
调整堆:每次调整O(logn),共n-1次
总时间复杂度:O(nlogn)
6.2 空间复杂度分析
空间复杂度:O(1) - 原地排序
6.3 稳定性分析
堆排序是不稳定的排序算法,因为交换堆顶和末尾元素时可能改变相同元素的相对顺序。
七、堆排序与其他排序算法比较
特性 | 堆排序 | 快速排序 | 归并排序 |
---|---|---|---|
时间复杂度 | O(nlogn) | O(nlogn) | O(nlogn) |
空间复杂度 | O(1) | O(logn) | O(n) |
稳定性 | 不稳定 | 不稳定 | 稳定 |
适用场景 | 内存受限 | 通用场景 | 需要稳定 |
八、使用注意事项与最佳实践
8.1 适用场景
-
内存敏感环境:空间复杂度O(1)
-
需要保证最坏情况性能:始终O(nlogn)
-
实时系统:可预测的性能表现
-
大数据处理:适合外部排序
8.2 注意事项
-
不稳定排序:相同元素可能改变顺序
-
常数因子较大:实际运行可能比其他O(nlogn)算法慢
-
缓存不友好:数组访问模式跳跃
-
实现复杂度:相比简单排序较复杂
8.3 最佳实践建议
// 推荐的堆排序模板
void recommendedHeapSort(int arr[], int n) {
if (n <= 1) return;
for (int i = n / 2 - 1; i >= 0; i--) {
int parent = i;
int temp = arr[parent];
int child;
while ((child = 2 * parent + 1) < n) {
if (child + 1 < n && arr[child] < arr[child + 1]) child++;
if (temp >= arr[child]) break;
arr[parent] = arr[child];
parent = child;
}
arr[parent] = temp;
}
for (int i = n - 1; i > 0; i--) {
int temp = arr[0];
arr[0] = arr[i];
arr[i] = temp;
int parent = 0;
int tempVal = arr[parent];
int child;
while ((child = 2 * parent + 1) < i) {
if (child + 1 < i && arr[child] < arr[child + 1]) child++;
if (tempVal >= arr[child]) break;
arr[parent] = arr[child];
parent = child;
}
arr[parent] = tempVal;
}
}
九、堆的实际应用
9.1 优先级队列实现
#include <stdio.h>
#define MAX_SIZE 100
typedef struct {
int data[MAX_SIZE];
int size;
} PriorityQueue;
void initQueue(PriorityQueue *q) { q->size = 0; }
void enqueue(PriorityQueue *q, int value) {
if (q->size >= MAX_SIZE) return;
int i = q->size++;
q->data[i] = value;
while (i > 0 && q->data[i] > q->data[(i-1)/2]) {
int temp = q->data[i];
q->data[i] = q->data[(i-1)/2];
q->data[(i-1)/2] = temp;
i = (i-1)/2;
}
}
int dequeue(PriorityQueue *q) {
if (q->size <= 0) return -1;
int result = q->data[0];
q->data[0] = q->data[--q->size];
int i = 0;
while (2*i+1 < q->size) {
int child = 2*i+1;
if (child+1 < q->size && q->data[child] < q->data[child+1]) child++;
if (q->data[i] >= q->data[child]) break;
int temp = q->data[i];
q->data[i] = q->data[child];
q->data[child] = temp;
i = child;
}
return result;
}
9.2 Top K问题求解
void findTopK(int arr[], int n, int k) {
for (int i = k/2-1; i >= 0; i--) {
int parent = i;
int temp = arr[parent];
int child;
while ((child = 2*parent+1) < k) {
if (child+1 < k && arr[child] > arr[child+1]) child++;
if (temp <= arr[child]) break;
arr[parent] = arr[child];
parent = child;
}
arr[parent] = temp;
}
for (int i = k; i < n; i++) {
if (arr[i] > arr[0]) {
arr[0] = arr[i];
int parent = 0;
int temp = arr[parent];
int child;
while ((child = 2*parent+1) < k) {
if (child+1 < k && arr[child] > arr[child+1]) child++;
if (temp <= arr[child]) break;
arr[parent] = arr[child];
parent = child;
}
arr[parent] = temp;
}
}
printf("前%d大的元素: ", k);
for (int i = 0; i < k; i++) printf("%d ", arr[i]);
printf("\n");
}
十、常见面试题精讲
10.1 基础概念题
-
堆排序的时间复杂度是多少?为什么?
答:O(nlogn),建堆O(n),每次调整O(logn)共n-1次
-
堆排序为什么是不稳定的?
答:交换堆顶和末尾元素时可能改变相同元素的相对顺序
-
大顶堆和小顶堆的区别是什么?
答:大顶堆父节点大于等于子节点,小顶堆父节点小于等于子节点
10.2 编码实现题
// 题目1:使用堆排序找出数组中第k大的元素
int findKthLargest(int arr[], int n, int k) {
for (int i = n/2-1; i >= 0; i--) {
int parent = i;
int temp = arr[parent];
int child;
while ((child = 2*parent+1) < n) {
if (child+1 < n && arr[child] < arr[child+1]) child++;
if (temp >= arr[child]) break;
arr[parent] = arr[child];
parent = child;
}
arr[parent] = temp;
}
for (int i = n-1; i >= n-k; i--) {
int temp = arr[0];
arr[0] = arr[i];
arr[i] = temp;
int parent = 0;
int tempVal = arr[parent];
int child;
while ((child = 2*parent+1) < i) {
if (child+1 < i && arr[child] < arr[child+1]) child++;
if (tempVal >= arr[child]) break;
arr[parent] = arr[child];
parent = child;
}
arr[parent] = tempVal;
}
return arr[n-k];
}
10.3 算法分析题
-
给定10^8个整数,堆排序和快速排序哪个更合适?
答:堆排序,因为保证O(nlogn)且空间O(1),快速排序最坏O(n²)
-
如何证明堆排序是不稳定的?
答:构造包含相同元素的序列,观察排序后相对位置
-
堆排序在什么实际系统中应用广泛?
答:嵌入式系统、实时系统、内存受限环境
10.4 进阶思考题
// 题目:实现多路归并排序中的败者树(基于堆)
void buildLoserTree(int leaves[], int tree[], int k) {
for (int i = 0; i < k; i++) tree[i] = -1;
for (int i = k-1; i >= 0; i--) adjustTree(leaves, tree, k, i);
}
void adjustTree(int leaves[], int tree[], int k, int s) {
int t = (s + k) / 2;
while (t > 0) {
if (s == -1) break;
if (tree[t] == -1 || leaves[s] > leaves[tree[t]]) {
int temp = s;
s = tree[t];
tree[t] = temp;
}
t /= 2;
}
tree[0] = s;
}
十一、性能测试与比较
#include <stdio.h>
#include <time.h>
#include <stdlib.h>
void performanceTest() {
const int SIZE = 100000;
int *arr = (int*)malloc(SIZE * sizeof(int));
for (int i = 0; i < SIZE; i++) arr[i] = rand() % 1000;
clock_t start = clock();
optimizedHeapSort(arr, SIZE);
clock_t end = clock();
printf("堆排序%d个元素时间: %f秒\n", SIZE, (double)(end - start) / CLOCKS_PER_SEC);
free(arr);
}
十二、堆排序的变体与扩展
12.1 二项堆与斐波那契堆
// 二项堆节点结构
typedef struct BinomialNode {
int key;
int degree;
struct BinomialNode *child;
struct BinomialNode *sibling;
struct BinomialNode *parent;
} BinomialNode;
12.2 堆的扩展应用
定时器管理 网络数据包调度
图算法中的优先级队列 操作系统进程调度
总结
堆排序作为一种高效的比较排序算法,以其O(nlogn)的时间复杂度和O(1)的空间复杂度在特定场景下具有重要价值。理解堆的数据结构特性、掌握建堆和调整堆的过程,对于解决Top K问题、实现优先级队列等实际应用具有重要意义。虽然堆排序的常数因子较大且不稳定,但在内存受限或需要保证最坏情况性能的场景下仍然是优秀的选择。