数据结构——堆
一、堆的定义与基本操作
堆是一棵完全二叉树,树中每个结点的值都不小于(或不大于)其左右孩子结点的值。其中,如果父亲结点的值大于或等于孩子结点的值,那么称这样的堆为大顶堆,这时候每个结点的值都是以它为根结点的子树的最大值;如果父亲结点的值小于或等于孩子结点的值,那么称这样的堆为小顶堆,这时候每个结点的值都是以它为根结点的子树的最小值。
对完全二叉树来说,比较简洁的实现方法是使用数组来存储完全二叉树。这样结点按层序存储于数组中,其中第一个结点将存储在数组的 1 号位,并且数组 i 号位表示的结点的左孩子就是 2i 号位,而右孩子则是 (2i+1) 号位。于是定义如下:
1 #define maxn 100 2 // heap 为堆,n 为元素个数 3 int heap[maxn], n=10;
下面介绍向下调整方法:总是将当前结点 V 与它的左右孩子比较(如果有的话),假如孩子中存在权值比结点 V 的权值大的,就将其中权值最大的那个孩子结点与结点 V 交换;交换完毕后继续让结点 V 和孩子比较,直到结点 V 的孩子的权值都比结点 V 的权值小或是结点 V 不存在孩子结点。代码如下:
1 // 对 heap 数组在 [low,high] 范围进行向下调整 2 // 其中 low 为欲调整结点的数组下标,high 一般为最后一个元素的数组下标 3 void downAdjust(int low, int high) { 4 int i=low, j=2*i; // i 指向当前交换节点,j指向左孩子 5 while(j <= high) { 6 // 若右孩子存在,且右孩子大于左子树 7 if(j+1 <= high && heap[j+1] > heap[j]) { 8 j=j+1; // 右孩子更大 9 } 10 // 如果最大权值比欲调整结点大 11 if(heap[j] > heap[i]) { 12 swap(&heap[j], &heap[i]); // 交换 13 i = j; // 保持 i 指向欲调整结点,j 指向左孩子 14 j = 2*i; 15 } else { 16 break; // 孩子结点比根结点小,调整结束 17 } 18 } 19 }
那个建堆的过程也就很容易了。假设序列中元素的个数为 n,数组下标在 $\left [ 1,\left \lfloor \frac{n}{2} \right \rfloor \right ]$ 范围内的结点都是非叶子结点。于是可以从 $\left \lfloor \frac{n}{2} \right \rfloor$ 号开始倒着枚举结点,对每个遍历到的结点 i 进行 [i,n] 范围的调整。代码如下:
1 // 建堆 2 void createHeap() { 3 int i; 4 for(i=n/2; i>=1; --i) { // 倒着枚举 5 downAdjust(i, n); // 向下调整 6 } 7 }
另外,如果要删除堆中的最大元素(也就是删除堆顶元素),并让其仍然保持堆的结构,那么只需要最后一个元素覆盖栈顶元素,然后对根结点进行调整即可。代码如下:
1 // 删除堆顶元素 2 void deleteTop() { 3 heap[1] = heap[n--]; // 用最后一个元素覆盖栈顶元素,并让元素个数-1 4 downAdjust(1, n); // 向下调整栈顶元素 5 }
那么,如果想要往堆里添加一个元素,可以把想要添加的元素放在数组最后,然后进行向下调整操作。向上调整总是把欲调整结点与父亲结点比较,如果权值比父亲结点大,那么就交换其与父亲结点,这样反复比较,直到到达堆顶或是父亲结点的权值较大为止。代码如下:
1 // 对 heap 数组在 [low,high] 范围进行向上调整 2 // 其中 low 为欲调整结点的数组下标,high 一般为最后一个元素的数组下标 3 void upAdjust(int low, int high) { 4 int i=high, j=i/2; // i 为欲调整结点,j 为其父亲 5 while(j >= low) { 6 // 如果父亲结点小于欲调整结点 7 if(heap[j] < heap[i]) { 8 swap(&heap[j], &heap[i]); // 交换 9 i = j; // 保持 i 指向欲调整结点,j 指向父亲结点 10 j = i/2; 11 } else { 12 break; // 孩子结点比根结点小,调整结束 13 } 14 } 15 }
在此基础上就很容易实现添加元素的代码了:
1 // 添加元素 x 2 void insert(int x) { 3 heap[++n] = x; // 让元素个数+1,然后将数组末位赋值为 x 4 upAdjust(1, n); // 向上调整插入的结点 5 }
二、堆排序
堆排序是指使用堆结构对一个序列进行排序。此处讨论递增排序的情况。
可以倒着遍历数组,假设当前遍历到 i 号位,那么堆顶元素与 i 号位的元素交换,接着在 [1,i-1] 范围内对栈顶元素进行一次向下调整即可。代码如下:
1 // 堆排序 2 void heapSort() { 3 int i; 4 for(i=n; i>1; --i) { // 倒着枚举 5 swap(&heap[i], &heap[1]); // 交换 heap[i] 与堆顶 6 downAdjust(1, i-1); // 调整堆顶 7 } 8 }
完整测试代码如下:
1 /* 2 堆 3 */ 4 5 #include <stdio.h> 6 #include <string.h> 7 #include <math.h> 8 #include <stdlib.h> 9 #include <time.h> 10 #include <stdbool.h> 11 12 #define maxn 100 13 // heap 为堆,n 为元素个数 14 int heap[maxn], n=10; 15 16 // 交换 a,b 17 void swap(int* a, int* b) { 18 int temp = *a; 19 *a = *b; 20 *b = temp; 21 } 22 23 // 对 heap 数组在 [low,high] 范围进行向下调整 24 // 其中 low 为欲调整结点的数组下标,high 一般为最后一个元素的数组下标 25 void downAdjust(int low, int high) { 26 int i=low, j=2*i; // i 指向当前交换节点,j指向左孩子 27 while(j <= high) { 28 // 若右孩子存在,且右孩子大于左子树 29 if(j+1 <= high && heap[j+1] > heap[j]) { 30 j=j+1; // 右孩子更大 31 } 32 // 如果最大权值比欲调整结点大 33 if(heap[j] > heap[i]) { 34 swap(&heap[j], &heap[i]); // 交换 35 i = j; // 保持 i 指向欲调整结点,j 指向左孩子 36 j = 2*i; 37 } else { 38 break; // 孩子结点比根结点小,调整结束 39 } 40 } 41 } 42 43 // 建堆 44 void createHeap() { 45 int i; 46 for(i=n/2; i>=1; --i) { // 倒着枚举 47 downAdjust(i, n); // 向下调整 48 } 49 } 50 51 // 删除堆顶元素 52 void deleteTop() { 53 heap[1] = heap[n--]; // 用最后一个元素覆盖栈顶元素,并让元素个数-1 54 downAdjust(1, n); // 向下调整栈顶元素 55 } 56 57 // 对 heap 数组在 [low,high] 范围进行向上调整 58 // 其中 low 为欲调整结点的数组下标,high 一般为最后一个元素的数组下标 59 void upAdjust(int low, int high) { 60 int i=high, j=i/2; // i 为欲调整结点,j 为其父亲 61 while(j >= low) { 62 // 如果父亲结点小于欲调整结点 63 if(heap[j] < heap[i]) { 64 swap(&heap[j], &heap[i]); // 交换 65 i = j; // 保持 i 指向欲调整结点,j 指向父亲结点 66 j = i/2; 67 } else { 68 break; // 孩子结点比根结点小,调整结束 69 } 70 } 71 } 72 73 // 添加元素 x 74 void insert(int x) { 75 heap[++n] = x; // 让元素个数+1,然后将数组末位赋值为 x 76 upAdjust(1, n); // 向上调整插入的结点 77 } 78 79 // 堆排序 80 void heapSort() { 81 int i; 82 for(i=n; i>1; --i) { // 倒着枚举 83 swap(&heap[i], &heap[1]); // 交换 heap[i] 与堆顶 84 downAdjust(1, i-1); // 调整堆顶 85 } 86 } 87 88 // 输出堆 89 void print() { 90 int i; 91 for(i=1; i<=n; ++i) { 92 printf("%d ", heap[i]); 93 } 94 printf("\n"); 95 } 96 97 int main() { 98 int i; 99 for(i=1; i<=n; ++i) { 100 scanf("%d", &heap[i]); 101 } 102 createHeap(); // 建堆 103 print(); // 输出堆 104 deleteTop(); // 删除栈顶元素 105 print(); // 输出堆 106 insert(99); // 插入99 107 print(); // 输出堆 108 heapSort(); // 堆排序 109 print(); // 输出排序结果 110 111 return 0; 112 }