堆的概念
堆可以看成是一棵完全二叉树,除最后一层以外,它的每一层都是填满的,最后一层从左到右依次填入。对于堆上的任意一个结点来说,越接近顶部,结点的权值也就越大,并且它的权值大于等于它所在子树的所有结点的权值。我们对具有这样性质的完全二叉树叫做堆。
特别指出的是,如果一颗完全二叉树上每个结点的权值小于等于它所在子树的任意结点的权值,也被称为堆。为了区分这两种堆,我们把根结点权值大于等于树中结点权值的堆称为大顶堆,根结点权值小于等于树中结点权值的堆则称为小顶堆。
从逻辑上看,堆可以看成是一棵完全二叉树,具有 N 个元素的堆,高度为O(log N)
。但实际上,我们并不需要真的维护一棵完全二叉树,而只需用一个数组来存储。堆按从上到下,从左到右的顺序,依次存储在下标从 1 开始的数组里。
- 编号为i的子节点,左孩子编号:
2*i
,右孩子编号:2*i+1
- 可以用连续空间存储(数组)
由于堆是一棵完全二叉树,堆的插入和删除操作的时间复杂度为 O(log N)
,log N
指的就是这颗完全二叉树的高度。
堆有一个重要的性质,称为堆序性,即堆中每个结点的权值都大于等于(或小于等于)其子树任意结点的权值。
为了维护堆的堆序性,在修改堆中结点时会对堆进行调整,调整的时间复杂度为 O(log N)
。堆的调整有两种,分别是自下而上的调整和自上而下的调整,即上滤和下滤。
堆通常用在堆排序里,堆排序是一种高效的排序算法,时间复杂度为O(N log N)
。另外,堆也可以用于实现优先队列。
堆的实现
堆的构造实际上很简单,我们往往用一个数组来存储堆中的元素。在向堆中插入元素时,我们会先将新元素插入到堆的最后,即数组的末尾,之后再调整它的位置来维护堆的堆序性。
堆的插入操作算法流程如下:
- 把新元素保存在数组的最后。
- 找到新插入元素的父节点位置。
- 将新元素与父节点比较大小。
- 若新插入的元素与其父节点大小不符合堆序性则交换它和父节点的位置,并回到步骤2;若符合则插入操作完成。
删除堆顶的操作,我们都知道,堆的堆顶是数组中下标为0的元素。当然我们不能直接删除这个元素,让它之后的一个元素前移一位,显然这样会破坏堆序性。
删除堆顶元素操作的算法流程如下:
- 将堆顶元素和最后一个元素交换。
- 删除堆的最后一个元素。
- 自顶向下调整元素的位置,使之满足堆序性。
堆的基本操作
/*************************************************************************
> File Name: 9.heap.c
> Author: 陈杰
> Mail: 15193162746@163.com
> Created Time: 2021年04月06日 星期二 20时58分18秒
> 堆的基本操作
************************************************************************/
#include<stdio.h>
#include<stdlib.h>
#include<time.h>
#define swap(a, b) {\
__typeof(a) _temp = a;\
a = b, b = _temp;\
}
typedef struct Heap{
int *data;
int cnt;
}Heap;
/*
* 堆的初始化函数
* @param n: 初始化堆的存储大小
* */
Heap *init(int n) {
Heap *h = (Heap *)malloc(sizeof(Heap));
h->data = (int *) malloc(sizeof(int) * (n + 1));
h->data[0] = n; // 堆的第一个元素存储堆的大小
h->cnt = 0;
return h;
}
/*
* 判空
* @param h: 要操作的堆的指针
* */
int empty(Heap *h) {
return h->cnt == 0;
}
/*
* 获取堆顶元素
* @param h: 要操作的堆的指针
* */
int top(Heap *h) {
return h->data[1];
}
/*
* 堆插入元素
* @param h: 要操作的堆的指针
* @param val: 要插入元素的值
* */
int push(Heap *h,int val){
if(h == NULL || h->cnt == h->data[0]) return 0;
h->data[++(h->cnt)] = val;
int ind = h->cnt; // 当前元素设为队尾元素
// 自下往上处理堆序
while(ind >> 1 && h->data[ind] > h->data[ind >> 1]) {
swap(h->data[ind], h->data[ind >> 1]);
ind >>= 1;
}
return 1;
}
/*
* 堆的弹出操作
* @param h: 要操作的堆的指针
* */
int pop(Heap *h) {
if(h == NULL || empty(h)) return 0;
h->data[1] = h->data[h->cnt--];
int ind = 1;
// 自上往下处理堆序
while((ind << 1) <= h->cnt){
int temp = ind, l = ind << 1, r = ind << 1 | 1;
if(h->data[l] > h->data[temp]) temp = l;
if(r <= h->cnt && h->data[r] > h->data[temp]) temp = r;
if(temp == ind) break;
swap(h->data[temp], h->data[ind]);
ind = temp;
}
return 1;
}
/*
* 堆的打印
* @param h: 要操作的堆的指针
* */
void output(Heap *h) {
printf("[");
for(int i = 1; i <= h->cnt; i++) {
i == 1 || printf(" ");
printf("%2d", h->data[i]);
}
printf("]\n");
}
/*
* 堆的清理
* @param h: 要操作的堆的指针
* */
void clear(Heap *h) {
if(h == NULL) return;
free(h->data);
free(h);
}
int main() {
srand(time(0));
#define MAX_OP 20
Heap *heap = init(MAX_OP);
for(int i = 0; i < MAX_OP; i++) {
int val = rand() % 100;
push(heap, val);
printf("insert %d to the heap\n", val);
}
output(heap);
for(int i = 0; i < MAX_OP; i++) {
printf("%d\n", top(heap));
pop(heap);
output(heap);
}
clear(heap);
#undef MAX_OP
return 0;
}
堆排序
最后我们再来了解下堆排序的实现,我们以将一个大根堆由小到大进行排序为例。
堆排序的算法流程如下:
- 将变量 i 设为堆末下标。
- 交换堆顶和第 i 个元素。
- 自上到下调整堆顶到第 i 个元素之间的堆结构,使之保持堆序性。
- 将 i 前移一位,重复步骤2,直至 i等于 1。
/*************************************************************************
> File Name: 9.heap_sort.c
> Author: 陈杰
> Mail: 15193162746@163.com
> Created Time: 2021年03月24日 星期三 20时12分41秒
> 堆排序-线性建堆法
************************************************************************/
#include<stdio.h>
#include <stdlib.h>
#include <time.h>
#define swap(a, b) {\
__typeof(a) _temp = a;\
a = b, b = _temp;\
}
void down_update(int *arr, int n, int ind) {
while((ind << 1) <= n) {
int temp = ind, l = ind << 1, r = ind << 1 | 1;
if(arr[l] > arr[temp]) temp = l;
if(r <= n && arr[r] > arr[temp]) temp = r;
if(temp == ind) break;
swap(arr[ind], arr[temp]);
ind = temp;
}
return;
}
void heap_sort(int *arr, int n) {
arr -= 1; // 编程小技巧
// 建堆
for(int i = n >> 1; i >= 1; i--) {
down_update(arr, n, i);
}
// 排序
for(int i = n; i > 1; i--) {
swap(arr[i], arr[1]);
down_update(arr,i - 1, 1);
}
return;
}
void output(int *arr, int n) {
printf("[");
for(int i = 0; i < n; i++) {
i == 0 || printf(" ");
printf("%d",arr[i]);
}
printf("]\n");
}
int main() {
srand(time(0));
#define MAX_OP 20
int *arr = (int *)malloc(sizeof(int) * MAX_OP);
for(int i = 0; i < MAX_OP; i++) {
arr[i] = rand() % 100;
}
output(arr, MAX_OP);
heap_sort(arr, MAX_OP);
output(arr, MAX_OP);
#undef MAX_OP
free(arr);
return 0;
}
优先队列
利用队列先进先出的性质,可以解决很多实际问题,但对于一些特殊的情况,队列是无法解决的。例如,在医院里,重症急诊患者肯定不能像普通患者那样依次排队就诊。这个时候,我们就需要一种新的数据结构——优先队列,先访问优先级高的元素(例如这里的重症急诊患者)。
在队列中,元素从队尾进入,从队首删除。相比队列,优先队列里的元素增加了优先级的属性,优先级高的元素先被删除。
优先队列内部一般是用堆来实现的。我们知道堆的插入、删除操作的时间复杂度都是O(log n)
,自然优先队列的插入、删除操作的时间复杂度也都是O(log n)
。堆中的堆顶元素就是优先队列的队首元素。
对于大根堆实现的优先队列,总是优先级高的元素先被删除。相对的,对于小根堆实现的优先队列,总是优先级低的元素先被删除。对于后者,我们也称之为优先队列。优先队列还能解决任务调度问题,例如操作系统的进程调度问题。
在 C++ 的 STL 里,有封装好的优先队列priority_queue
,它包含在头文件<queue>
里。优先级可以自己定义,默认优先级是权值大的元素优先级高。优先队列是一种用途广泛的数据结构,它能巧妙高效的解决很多其他数据结构不容易解决的问题。
哈夫曼编码
哈夫曼编码是 1952 年由 David A. Huffman 提出的一种无损数据压缩的编码算法。哈夫曼编码先统计出每种字母在字符串里出现的频率,根据频率建立一棵路径带权的二叉树,也就是哈夫曼树,树上每个结点存储字母出现的频率,根结点到结点的路径即是字母的编码,频率高的字母使用较短的编码,频率低的字母使用较长的编码,这样使得编码后的字符串占用空间最小。
首先统计每个字母在字符串里出现的频率,我们把每个字母看成一个结点,结点的权值即是字母出现的频率,我们把每个结点看成一棵只有根结点的二叉树,一开始把所有二叉树都放在一个集合里,接下来开始如下编码:
- 步骤一:从集合里取出两个根结点权值最小的树
a
和b
,构造出一棵新的二叉树c
,二叉树c
的根结点的权值为a
和b
的根结点权值和,二叉树c
的左右子树分别是a
和b
。 - 步骤二:将二叉树
a
和b
从集合里删除,把二叉树c
加入集合里。 - 重复以上两个步骤,直到集合里只剩下一棵二叉树,最后剩下的就是哈夫曼树了。
我们规定每个有子节点的结点,到左子节点的路径为0,到右子节点的路径为1.每个字母的编码就是根结点到字母对应结点的路径。
/*************************************************************************
> File Name: 5.halfman.c
> Author: 陈杰
> Mail: 15193162746@163.com
> Created Time: 2021年05月15日 星期六 22时53分19秒
************************************************************************/
#include<stdio.h>
#include<stdlib.h>
#include <string.h>
typedef struct Node {
char ch; // 当前结点是否独立成词
double freq;
struct Node *lchild, *rchild;
} Node;
typedef struct Heap {
Node **data;
int n, cnt;
} Heap;
Node *getNewNode(char ch,double freq, Node *lchild, Node *rchild) {
Node *p = (Node *)malloc(sizeof(Node));
p->freq = freq;
p->ch = ch;
p->lchild = lchild;
p->rchild = rchild;
return p;
}
void clear(Node *root) {
if (root == NULL) return;
clear(root->lchild);
clear(root->rchild);
free(root);
}
Heap *init_heap(int n) {
Heap *h = (Heap *)malloc(sizeof(Heap));
h->data = (Node **)malloc(sizeof(Node *) * (n + 5));
h->n = n;
h->cnt = 0;
return h;
}
Node *top(Heap *h) {
return h->data[1];
}
int empty(Heap *h) {
return h->cnt == 0;
}
#define swap(a, b) {\
__typeof(a) _temp = a;\
a = b, b = _temp;\
}
int less(Node *n1, Node *n2) {
return n1->freq < n2->freq;
}
void push(Heap *h, Node *node) {
h->data[++h->cnt] = node;
int ind = h->cnt; // 当前元素设为队尾元素
// 自下往上处理堆序
while(ind >> 1 && less(h->data[ind], h->data[ind >> 1])) {
swap(h->data[ind], h->data[ind >> 1]);
ind >>= 1;
}
return;
}
void pop(Heap *h) {
if(empty(h)) return;
h->data[1] = h->data[h->cnt--];
int ind = 1;
// 自上往下处理堆序
while((ind << 1) <= h->cnt){
int temp = ind, l = ind << 1, r = ind << 1 | 1;
if(less(h->data[l],h->data[temp])) temp = l;
if(r <= h->cnt && less(h->data[r],h->data[temp])) temp = r;
if(temp == ind) break;
swap(h->data[temp], h->data[ind]);
ind = temp;
}
}
int size(Heap *h) {
return h->cnt;
}
void clearHeap(Heap *h) {
if(h == NULL) return;
free(h->data);
free(h);
}
Node* buildHalfman(Heap *h) {
while(size(h) > 1) {
Node *a = top(h); pop(h);
Node *b = top(h); pop(h);
push(h, getNewNode(0, a->freq + b->freq, a, b));
}
return top(h);
}
void getHalfmanCode(Node *root, int k, char *buff, char *code[]) {
if(root == NULL) return;
buff[k] = 0;
if(root->ch) {
code[root->ch] = strdup(buff);
return;
}
buff[k] = '0';
getHalfmanCode(root->lchild, k + 1, buff, code);
buff[k] = '1';
getHalfmanCode(root->rchild, k + 1, buff, code);
}
int main () {
int n;
scanf("%d", &n);
Heap *h = init_heap(n);
for(int i = 0; i < n; i++) {
char ch[10];
double freq;
scanf("%s %lf", ch, &freq);
push(h, getNewNode(ch[0], freq, NULL, NULL));
}
Node *root = buildHalfman(h);
char *code[256] = {0}, buff[100];
getHalfmanCode(root, 0, buff, code);
for(int i = 1; i < 256; i++) {
if(code[i] == 0) continue;
printf("%c %s\n", i, code[i]);
}
clearHeap(h);
return 0;
}
刷题时候的临时堆
/*************************************************************************
> File Name: temp_heap.c
> Author: 陈杰
> Mail: 15193162746@163.com
> Created Time: 2021年07月01日 星期四 21时15分52秒
> 零时简易堆
*************************************************************************/
#include <stdio.h>
#include <stdlib.h>
#define swap(a, b) {\
__typeof(a) _temp = a;\
a = b, b = _temp;\
}
#define MAX_SIZE 10
void down_to_up(int *heap, int ind) {
while(ind >> 1 && heap[ind] > heap[ind >> 1]) {
swap(heap[ind], heap[ind >> 1]);
ind >>= 1;
}
}
void up_to_down(int *heap, int cnt) {
int ind = 1;
while((ind << 1) <= cnt) {
int temp = ind, l = ind << 1, r = ind << 1 | 1;
if(heap[l] > heap[temp]) temp = l;
if(r <= cnt && heap[r] > heap[temp]) temp = r;
if(temp == ind) break;
swap(heap[temp], heap[ind]);
ind = temp;
}
}
int main() {
int ind = 0, heap[MAX_SIZE];
for(int i = 1; i < MAX_SIZE; i++) {
int val = rand() % 100;
// push
heap[++ind] = val;
down_to_up(heap, ind);
}
while(ind) {
printf("max value : %d\n", heap[1]);
heap[1] = heap[ind--];
up_to_down(heap, ind);
}
return 0;
}