堆与堆排序和优先队列


本文将介绍插入排序

堆

  • 最大堆?最小堆?
    最大堆:堆中每个节点的值 >= 子节点的值
    最小堆:堆中每个节点的值 <= 子节点的值

  • 堆总是一颗完全二叉树
  • 为什么用数组来实现堆?
    二叉堆是一个完全二叉树 把完全二叉树编号后 编号就可以对应数组中的索引
    如果从1开始编号,父节点 = 当前节点索引 / 2 , 左孩子 = 当前节点索引 * 2, 右孩子 = 当前节点索引 *2 + 1
    从1开始编号,用数组来表示一颗完全二叉树,最后一个非叶子节点索引:最后一个节点索引/2,
原理

如果从0开始编号,父节点 = (当前节点索引-1) / 2 , 左孩子 = 当前节点索引 * 2 + 1, 右孩子 = 当前节点索引 *2 + 2
从0开始编号,用数组来表示一颗完全二叉树,最后一个非叶子节点索引:(最后一个节点索引-1)/2,

原理

初始化

1
2
3
4
5
6
7
#define MAXSIZE 100   // 堆的容量 可以用动态数组来扩充

// 最大堆的定义
typedef struct {
int data[MAXSIZE];
int size; // 堆中元素个数
} MaxHeap;

建堆 核心:heapify O(logN)

heapify:将任意数组整理成堆的形状
优化:如果将个元素逐个插入到一个空堆中,这样建堆 复杂度为O(NlogN),heapify的过程,算法复杂度为O(N)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// heapify:将任意数组整理成堆的形状 将每个节点都符合最大堆的性质
// 因为叶节点本身没有子节点所以符合最大堆的性质,只需从最后一个非叶子节点到堆顶节点,依次将每个节点堆化
// 注意:heapfiy之后数组中元素并不是从大到小的顺序
MaxHeap *buildHeap(int *arr, int n) {
// 初始化堆 并且 对堆进行赋值
/*** 堆中元素 ∈ [0,size-1] ***/
MaxHeap *heap = (MaxHeap *) malloc(sizeof(MaxHeap));
heap->size = 0;
for (int i = 0; i < n; ++i) {
heap->data[i] = arr[i];
heap->size++;
}
// heapfiy:最后一个节点的索引为size-1,因为数组从0开始,所以最后一个非叶子节点索引(size-1-1)/2
for (int i = (heap->size - 2) / 2; i >= 0; --i) {
shiftDown(heap, i);
}
return heap;
}

插入 核心:shiftUp O(logN)

基本思想:先将节点插入堆中,然后通过shiftUp操作将该节点放在满足最大堆性质的位置
shiftUp:将待调整节点与其父节点比较,如果待调整节点比其父节点大,交换两者位置,待调整节点继续与其新父节点比较,如果遇到待调整节点 <= 父亲节点,说明满足最大堆的性质,否则一直执行此过程

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
void shiftUp(MaxHeap *heap, int idx) {
// // idx >= 1 代码上讲防止了数组越界
// // 实际意义:idx=0是堆顶元素 其子节点idx要么是1(左孩子)要么是2(右孩子),在idx=2或1的时候已经和堆顶元素进行了比较
// while (idx >= 1 && heap->data[idx] > heap->data[(idx - 1) / 2]) { // 待调整节点比其父节点大,交换两者位置
// swap(heap->data, idx, (idx - 1) / 2);
// idx = (idx - 1) / 2; // 更新待调整节点 在堆中的位置
// }

// 优化:减少赋值次数 交换时需要3次赋值,执行一次swap,赋值3^n
int temp = heap->data[idx];
while (idx >= 1 && temp > heap->data[(idx - 1) / 2]) { // 待调整节点比其父节点大,交换两者位置
heap->data[idx] = heap->data[(idx - 1) / 2];
idx = (idx - 1) / 2; // 更新待调整节点 在堆中的位置
}
heap->data[idx] = temp;
}

删除 核心:shiftDown O(logN)

基本思想:将最后一个元素与堆顶元素互换,此时新的堆顶元素不一定符合最大堆的性质,对其进行shiftDown
shiftDown:将当前元素与自己的左右孩子中最大的那个比较,如果比最大的孩子小,交换两者位置,继续与新的左右孩子比较,直到大于等于左右孩子中最大的,此时符合最大堆的性质

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
// 向最大堆中取出堆顶元素 即堆中的最大元素 O(logN)
// 基本思想:将最后一个元素与堆顶元素互换,此时新的堆顶元素不一定符合最大堆的性质,对其进行shiftDown
int entractMax(MaxHeap *heap) {
if (heap->size <= 0) {
printf("堆中没有元素.\n");
return -1;
}
int max = heap->data[0]; // 堆顶元素
// printf("出堆元素: %d \n", max);
swap(heap->data, 0, heap->size - 1); // 将堆顶元素与堆中最后一个元素互换 堆中元素存在[0,size-1]中,起到了删除堆顶元素的作用
heap->size--; // 更新size
shiftDown(heap, 0); // 让调整新的堆顶元素 使其符合最大堆性质
return max;
}

// 下沉操作 调整索引为idx的节点,使其符合最大堆性质
// 基本思想:将当前元素与自己的左右孩子中最大的那个比较,如果比最大的孩子小,交换两者位置
// 继续与新的左右孩子比较,直到大于等于左右孩子中最大的,此时符合最大堆的性质
void shiftDown(MaxHeap *heap, int idx) {
// while (idx * 2 + 1 <= heap->size - 1) { // 左孩子 = idx *2 +1 如果在堆中 [0,size-1]
//// int j = idx * 2 + 1;
//// // 如果有右孩子 并且右孩子比左孩子大
//// if (j + 1 <= heap->size - 1 && heap->data[j + 1] > heap->data[j]) {
//// j = j + 1; // 更新j为右孩子的索引
//// }
//// if (heap->data[idx] >= heap->data[j]) { // 待调节点 >= 左右孩子最大
//// break; // 满足最大堆性质 结束
//// }
//// swap(heap->data, idx, j); // 交换待调节点与其左右孩子最大的
//// idx = j; // 更新待调节点为 左右孩子最大的
//// }

// 优化:减少赋值次数 交换时需要3次赋值,执行一次swap,赋值3^n
int temp = heap->data[idx];
while (idx * 2 + 1 <= heap->size - 1) { // 左孩子 = idx *2 +1 如果在堆中 [0,size-1]
int j = idx * 2 + 1;
// 如果有右孩子 并且右孩子比左孩子大
if (j + 1 <= heap->size - 1 && heap->data[j + 1] > heap->data[j]) {
j = j + 1; // 更新j为右孩子的索引
}
if (temp >= heap->data[j]) { // 待调节点 >= 左右孩子最大
break; // 满足最大堆性质 结束
}
heap->data[idx] = heap->data[j];
idx = j; // 更新待调节点为 左右孩子最大的
}
heap->data[idx] = temp;
}

一些其他操作

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// 获取最大堆顶元素 即整个堆中最大的元素
int getHeadTop(MaxHeap *heap) {
if (heap->size <= 0) {
printf("堆中没有元素.");
return -9999999;
}
return heap->data[0];
}

// 取出堆中的最大元素,并且替换成元素e
int replace(MaxHeap *heap, int e) {
int max = heap->data[0]; // 堆中最大元素
heap->data[0] = e; // 修改堆顶元素
shiftDown(heap, 0); // 修改后堆顶元素不一定满足最大堆性质
return max;
}

原地堆排序

基本思想:首先将待排序数组heapfiy,即数组中每个元素都满足最大堆的性质

1
2


堆的应用-优先队列

  1. 优先队列与普通队列的区别?
    普通队列:先进先出,后进后出
    优先队列:出队顺序和入队顺序无关,和优先级有关
  1. 为什么使用优先队列?
    动态 的选择优先级最高的任务
    在N个元素中选出前M个元素,快排,归并等都是NlogN ,使用优先队列NlogM

  2. 优先队列可以用什么实现?
入队 出队
普通数组 O(1) O(N) 出队时遍历数组找出优先级最高的
有序数组 O(N) 入队时需要遍历一遍数组,插入适合的位置 O(1) 优先级从大到小
堆 O(logN) 完全二叉树 O(logN) 完全二叉树
文章目录
  1. 1. 堆
    1. 1.1. 初始化
    2. 1.2. 建堆 核心:heapify O(logN)
    3. 1.3. 插入 核心:shiftUp O(logN)
    4. 1.4. 删除 核心:shiftDown O(logN)
    5. 1.5. 一些其他操作
  2. 2. 原地堆排序
  3. 3. 堆的应用-优先队列
|