堆与优先队列实质上是同一种东西,可以认为优先队列是堆的别名
1. 完全二叉树
完全二叉树的性质:
如下的左右两个形式都可以表示完全二叉树,因为完全二叉树可以顺序编码,所以可以建立一个顺序结构进行存储,右边可以理解为代码表现形式。通常将 0 号位空出来存储根结点,找某个结点的左右孩子时也是通过编号,比如 1 号位结点的左孩子为 ( 2 ∗ 1 + 1 ) = 3号结点,右孩子为 4 号节点
2. 堆与优先队列
堆在思维逻辑上是一棵完全二叉树,但是堆之所以叫堆,因为它特有的性质:任何一个三元组(根节点,左孩子,右孩子)而言,根节点要么是最大的,要么是最小的
对于任何一个三元组,根节点是最小的,就叫做 小顶堆,反之就是 大顶堆
堆能解决的问题 :
- 求所有元素的最大值或最小值(根节点)
- 找到所有元素中的次大值或次小值(二叉树的第二层)
2.1 堆的尾部插入调整
在大顶堆中插入 13,下面是两种实现方式:(左边是完全二叉树,右边是顺序表)
过程:自下向上调整结点以满足堆的性质
- 对于数组来说就是交换了 7 和 13 的位置
- 交换 11 和 13的位置
- 交换 13 和 12 的位置
平均时间复杂度为 O(logn),即二叉树的树高
2.2 堆的头部弹出调整
堆弹出元素只能删除堆顶元素
1. 删除堆顶元素,将堆尾元素放到堆顶
2. 自上向下调整堆内元素,使得满足堆的性质(三元组),12 > 7 ,于是将12 放到堆顶
3. 交换 7 和 11
堆为什么又叫优先队列呢?
答:堆删除元素是从堆顶删除,堆插入元素是从堆尾插入,这和队列的性质一样;而叫做优先队列是因为每次从堆顶删除元素,删除的都是所有元素中的最大值或最小值,这是优先的性质
2.3 队列 vs 堆 vs 优先队列
优先队列的结构定义和队列相同,但是入队和出队之后要进行堆的性质的维护
2.4 代码实现
priority_queue.h
#ifndef __PRIORITY_QUEUE_H__ #define __PRIORITY_QUEUE_H__ #include <stdio.h> #include <stdlib.h> #include <stdbool.h> #define MAXSIZE 100 #define swap(a, b) { \ __typeof(a) __c = (a); \ (a) = (b); \ (b) = __c; \ } // 从1开始存储,根结点下标为1 #define FATHER(i) ((i) / 2) #define LEFT(i) ((i) * 2) #define RIGHT(i) ((i) * 2 + 1) // 堆---数据类型 typedef struct PriorityQueue { int *__data, *data; // 从1开始存储 int size; // 数组大小 int n; // n:数组中实际元素个数 } PriorityQueue; // 初始化一个堆 PriorityQueue *init_PQ(int size); // 判空 bool empty(PriorityQueue *p); // 判满 bool full(PriorityQueue *p); // 获取堆顶元素 bool top(PriorityQueue *p, int *top_val); // 向上调整(递归) void up_update_1(int *data, int i); // 向上调整(非递归) void up_update_2(int *data, int i); // 向下调整 void down_update(int *data, int i, int n); // 入堆 bool push(PriorityQueue *p, int x); // 出堆 bool pop(PriorityQueue *p); // 销毁堆 void destroy_PQ(PriorityQueue *p); // 打印堆 void output(PriorityQueue *p); #endif
priority_queue.c
#include "priority_queue.h" // 初始化一个堆 PriorityQueue *init_PQ(int size) { PriorityQueue *p = (PriorityQueue*)malloc(sizeof(*p)); p->__data = (int*)malloc(sizeof(int) * (size)); p->data = p->__data - 1; p->size = size; p->n = 0; return p; } // 判空 bool empty(PriorityQueue *p) { if (p == NULL || p->__data == NULL || p->n == 0) { return true; } return false; } // 判满 bool full(PriorityQueue *p) { if (p == NULL || p->__data == NULL || p->n == p->size) { return true; } return false; } // 获取堆顶元素 bool top(PriorityQueue *p, int *top_val) { if (empty(p)) { return false; } *top_val = p->data[1]; return true; } // 向上调整(递归) void up_update_1(int *data, int i) { if (i == 1) { return; } if (data[i] > data[FATHER(i)]) { swap(data[i], data[FATHER(i)]); up_update_1(data, FATHER(i)); } } // 向上调整(非递归) void up_update_2(int *data, int i) { while (i > 1 && data[i] > data[FATHER(i)]) { swap(data[i], data[FATHER(i)]); i = FATHER(i); } } // 向下调整 void down_update(int *data, int i, int n) { while (LEFT(i) <= n) { int ind = i, l = LEFT(i), r = RIGHT(i); if (data[l] > data[ind]) { ind = l; } if (r <= n && data[r] > data[ind]) { ind = r; } if (ind == i) { break; } swap(data[i], data[ind]); i = ind; } } // 入堆 bool push(PriorityQueue *p, int x) { if (full(p)) { return false; } p->n++; p->data[p->n] = x; up_update_1(p->data, p->n); return true; } // 出堆 bool pop(PriorityQueue *p) { if (empty(p)) { return false; } p->data[1] = p->data[p->n]; p->n--; down_update(p->data, 1, p->n); return true; } // 销毁堆 void destroy_PQ(PriorityQueue *p) { if (p == NULL) { return; } if (p->__data != NULL) { free(p->__data); } free(p); } // 打印堆 void output(PriorityQueue *p) { if (p == NULL || p->data == NULL || p->n == 0) { return; } printf("PQ(%d) : ", p->n); for (int i = 1; i <= p->n; i++) { printf("%d ", p->data[i]); } printf("\n"); }
main.c
#include "priority_queue.h" int main(int argc, char *argv) { int op, x; PriorityQueue *p = init_PQ(MAXSIZE); while (1) { scanf("%d", &op); if (op == 0) { break; } if (op == 1) { // 入堆 scanf("%d", &x); printf("insert %d to priority_queue : \n", x); push(p, x); output(p); } else { int top_val; if (top(p, &top_val)) { // 获取堆顶元素 printf("pop : %d\n", top_val); } pop(p); // 出堆 output(p); } } destroy_PQ(p); // 销毁堆 return 0; }
3. 堆排序
要实现从小到大的排序,就要建大顶堆;
从大到小的排序,就要建小顶堆
3.1 线性建堆法
线性建堆法把原来建堆的时间复杂度 O(nlogn) 降低至 O(n),但是堆排序的时间复杂度整体还是 O(nlogn)
普通建堆(向上调整)
线性建堆(向下调整)
3.2 口诀
大顶堆
1、将堆顶元素与堆尾元素交换
2、将此操作看做是堆顶元素弹出操作
3、按照头部弹出以后的策略调整堆
3.3 普通堆排序和线性堆排序代码实现
heap_sort.h
#ifndef __HEAP_SORT_H__ #define __HEAP_SORT_H__ #include <stdio.h> #include <stdlib.h> #include <time.h> #define swap(a, b) { \ __typeof(a) __c = (a); \ (a) = (b); \ (b) = __c; \ } // 从1开始存储,根结点下标为1 #define FATHER(i) ((i) / 2) #define LEFT(i) ((i) * 2) #define RIGHT(i) ((i) * 2 + 1) // 随机化得到一个数组 int* getRandData(int n); // 向上调整(递归) void up_update_1(int *data, int i); // 向上调整(非递归) void up_update_2(int *data, int i); // 向下调整 void dowm_update(int *data, int i, int n); // 将大顶堆调整成从小到大排序 void heap_sort_final(int *data, int n); // 普通建堆(向上建堆) void normal_heap(int *arr, int n); // 线性建堆(向下调整) void linear_heap(int *arr, int n); // 打印数组 void print_arr(int *arr, int n); #endif
heap_sort.c
#include "heap_sort.h" // 随机化得到一个数组 int* getRandData(int n) { int *arr = (int*)malloc(sizeof(int) * n); for (int i = 0; i < n; i++) { arr[i] = rand() % 100; } return arr; } // 向上调整(递归) void up_update_1(int *data, int i) { if (i == 1) { return; } if (data[i] > data[FATHER(i)]) { swap(data[i], data[FATHER(i)]); up_update_1(data, FATHER(i)); } } // 向上调整(非递归) void up_update_2(int *data, int i) { while (i > 1 && data[i] > data[FATHER(i)]) { swap(data[i], data[FATHER(i)]); i = FATHER(i); } } // 向下调整 void down_update(int *data, int i, int n) { while (LEFT(i) <= n) { int ind = i, l = LEFT(i), r = RIGHT(i); if (data[l] > data[ind]) { ind = l; } if (r <= n && data[r] > data[ind]) { ind = r; } if (ind == i) { break; } swap(data[i], data[ind]); i = ind; } } // 将大顶堆调整成从小到大排序 void heap_sort_final(int *data, int n) { if (data == NULL) { return; } for (int i = n; i >= 2; i--) { swap(data[1], data[i]); down_update(data, 1, i - 1); // 向下调整 } } // 普通建堆(向上调整) void normal_heap(int *arr, int n) { int *data = arr - 1; for (int i = 2; i <= n; i++) { up_update_1(data, i); // 向上调整 } heap_sort_final(data, n); } // 线性建堆(向下调整) void linear_heap(int *arr, int n) { int *data = arr - 1; for (int i = n / 2; i >= 1; i--) { down_update(data, i, n); } heap_sort_final(data, n); } // 打印数组 void print_arr(int *arr, int n) { for (int i = 0; i < n; i++) { printf("%d ", arr[i]); } printf("\n"); }
main.c
#include "heap_sort.h" int main(int argc, char *argv[]) { srand(time(0)); int n; scanf("%d", &n); int *arr = getRandData(n); print_arr(arr, n); // normal_heap(arr, n); // 向上建堆 linear_heap(arr, n); // 线性建堆(向下调整) print_arr(arr, n); return 0; }
4. 哈夫曼编码优化(通过小顶堆方式优化步骤2)
4.1 构造算法:
贪心算法:构造哈夫曼树时首先选择权值小的叶子节点
1、首先,统计得到每一种字符的概率
2、每次将最低频率的两个结点合并成一棵子树
3、经过了 n-1 轮合并,就得到了一棵哈夫曼树
4、按照左0,右1的形式,将编码读取出来
4.2 代码实现:
heap_haffman.c
#include <stdio.h> #include <stdlib.h> #include <stdbool.h> #define MAX_CHAR_NUM 128 //创建二叉树结构体定义 typedef struct Node { char ch; int freq; //概率(频次) struct Node *lchild; struct Node *rchild; }Node; //创建堆结构体定义 typedef struct Heap { Node **__data, **data; int n, size; }Heap; //初始化一个堆 Heap* getNewHeap(int size) { Heap *h = (Heap*)malloc(sizeof(Heap)); h->__data = (Node**)malloc(sizeof(Node*) * size); h->data = h->__data - 1; h->n = 0; h->size = size; return h; } //判满(堆) bool fullHeap(Heap *h) { if (h == NULL || h->__data == NULL || h->n == h->size) return true; return false; } //判空(堆) bool emptyHeap(Heap *h) { if (h == NULL || h->__data == NULL || h->n == 0) return true; return false; } //获取堆顶元素 Node* top(Heap *h) { if (emptyHeap(h)) return NULL; return h->data[1]; } //比较两者的freq int cmpHeap(Heap *h, int i, int j) { return h->data[i]->freq < h->data[j]->freq; } //交换 void swap_node(Node **m, Node **n) { Node *temp = *m; *m = *n; *n = temp; } //向上调整 void up_maintain(Heap *h, int i) { while (i > 1 && cmpHeap(h, i, i / 2)) { swap_node(&h->data[i], &h->data[i / 2]); i = i / 2; } return ; } //向下调整 void down_maintain(Heap *h, int i) { while (i * 2 <= h->n) { int ind = i, l = i * 2, r = i * 2 + 1; if (cmpHeap(h, l, ind)) ind = l; if (r <= h->n && cmpHeap(h, r, ind)) ind = r; if (ind == i) return ; swap_node(&h->data[i], &h->data[ind]); i = ind; } return ; } //入堆 bool pushHeap(Heap *h, Node *n) { if (fullHeap(h)) return false; h->n += 1; h->data[h->n] = n; up_maintain(h, h->n); return true; } //出堆 bool popHeap(Heap *h) { if (emptyHeap(h)) return false; h->data[1] = h->data[h->n]; h->n -= 1; down_maintain(h, 1); return true; } //摧毁堆 void clearHeap(Heap *h) { if (h == NULL) return ; free(h->__data); free(h); return ; } //得到一个树结点 Node* getNewNode(int freq, char ch) { Node *p = (Node*)malloc(sizeof(Node)); p->ch = ch; p->freq = freq; p->lchild = p->rchild = NULL; return p; } //找出最小frep int find_min_node(Node **node_arr, int n) { int ind = 0; for (int j = 1; j <= n; j++) if (node_arr[ind]->freq > node_arr[j]->freq) ind = j; return ind; } Node* buildHaffmanTree(Node **node_arr, int n) { Heap *h = getNewHeap(n); for (int i = 0; i < n; i++) pushHeap(h, node_arr[i]); for (int i = 1; i < n; i++) { Node *node1 = top(h); popHeap(h); Node *node2 = top(h); popHeap(h); Node *node3 = getNewNode(node1->freq + node2->freq, 0); node3->lchild = node1; node3->rchild = node2; pushHeap(h, node3); } Node *ret = top(h); clearHeap(h); return ret; } //销毁二叉树 void clear(Node *root) { if (root == NULL) return ; clear(root->lchild); clear(root->rchild); free(root); return ; } char *char_code[MAX_CHAR_NUM] = {0}; void extractHaffmanCode(Node *root, char buff[], int k) { buff[k] = 0; if (root->lchild == NULL && root->rchild == NULL) { printf("%c : %s\n", root->ch, buff); return ; } buff[k] = '0'; extractHaffmanCode(root->lchild, buff, k + 1); buff[k] = '1'; extractHaffmanCode(root->rchild, buff, k + 1); return ; } int main(int argc, char *argv[]) { char s[10]; int n, freq; scanf("%d", &n); Node **node_arr = (Node**)malloc(sizeof(Node *) * n); for (int i = 0; i < n; i++) { scanf("%s%d", s, &freq); //%s会忽略换行符 node_arr[i] = getNewNode(freq, s[0]); } Node *root = buildHaffmanTree(node_arr, n); char buff[1000]; extractHaffmanCode(root, buff, 0); for (int i = 0; i < MAX_CHAR_NUM; i++) { if (char_code[i] == NULL) continue; printf("%c : %s\n", i, char_code[i]); } clear(root); return 0; }