1. 定义
1.1 堆
-
性质
堆是完全二叉树(思维逻辑层面)
根节点大于左右孩子为大顶堆;根节点小于左右孩子为小顶堆
-
堆是一种完全二叉树,为什么叫堆?
因为堆在完全二叉树的基础上去维护一种性质:对于任何一个根结点和左右孩子构成的三元组而言,根结点最大或最小(大顶堆、小顶堆) -
数组实现完全二叉树
⭐第 i i i个节点的父节点是 ( i − 1 ) / 2 (i -1) / 2 (i−1)/2 -
为什么完全二叉树能用数组实现?
按层编号,将完全二叉树这种逻辑结构映射到数组中,数组只存储完全二叉树值的信息,关系通过计算得到(区别于指针记录)
-
尾部插入
完全二叉树的最后增加一个元素,数组的尾部增加一个元素 -
头部弹出
弹出二叉树的根节点,弹出数组的第一个元素 -
堆的应用:
- 最值问题
第一大在顶层,第二大在次层,第三大不一定 - 堆排序:大顶堆完成思维逻辑上的弹出操作,数组元素就是从小到大的排序
时间复杂度:和弹出操作无关,和弹出后的调整有关
把数组当成堆来调整,可以调整一层,完全二叉树的层序遍历
- 最值问题
插入时,放到堆尾,自下而上调整
删除时,把堆尾元素放到堆首,自下而上调整
堆的结构思维逻辑上等价于完全二叉树,完全二叉树在程序实现上可以顺序实现(定义一个数组,使用顺序表去实现堆 )
为了维护大顶堆的性质,需要进行调整
插入时堆逻辑思维上的调整
插入时顺序结构中堆的调整
普通数组插入到第一个位置复杂度为o(n),思维逻辑上认为它是一个堆,插入时时间复杂度为o(logn)
类比两层循环嵌套,时间复杂度不一定是o(n^2)
堆尾进入元素,堆首弹出元素,就是个队列
优先:弹出的是最值
1.2 优先队列
- 普通队列:尾进头出
- 优先队列:元素有优先级,最高级先出,通常用堆实现
2. 堆的代码
#include <stdio.h>
#include <stdlib.h>
#include <time.h>
#define swap(a, b) {\
__typeof(a) __tmp = a;\
a = b; b = __tmp;\
}
typedef struct Heap {
int *data;
int size, len;
} Heap;
Heap *initHeap(int n) {
Heap *h = (Heap *)malloc(sizeof(Heap));
h->data = (int *)malloc(sizeof(int) * n); // data下标从0开始
h->size = n;
h->len = 0;
return h;
}
void freeHeap(Heap *h) {
if (!h) return ;
free(h->data);
free(h);
return ;
}
int push(Heap *h, int val) {
if (!h) return 0;
if (h->len == h->size) return 0;
h->data[h->len] = val;
int idx = h->len;
while (idx && h->data[idx] > h->data[(idx - 1) / 2]) { // idx > 0
swap(h->data[idx], h->data[(idx - 1) / 2]);
idx = (idx - 1) / 2;
}
h->len++;
return 1;
}
void downAdj(int *arr, int idx, int len) {
while (idx * 2 + 1 < len) { // 至少存在左孩子(最后一个节点下标len-1)
int tmp = idx, l = idx * 2 + 1, r = idx * 2 + 2;
if (arr[l] > arr[tmp]) tmp = l;
if (r < len && arr[r] > arr[tmp]) tmp = r;
if (idx == tmp) break; // 父节点就是最大的,堆已经平衡了
swap(arr[idx], arr[tmp]);
idx = tmp;
}
return ;
}
int isEmpty(Heap *h) {
return !h || !h->len;
}
int pop(Heap *h) {
int ret = h->data[0];
h->data[0] = h->data[h->len - 1];
h->len--; // 先h->len--,后向下调整
downAdj(h->data, 0, h->len);
return ret;
}
void showHeap(Heap *h) {
if (!h) return ;
printf("Heap:[");
for (int i = 0; i < h->len; i++) {
i && printf(" ");
printf("%d", h->data[i]);
}
printf("]\n");
}
int main() {
srand(time(0));
#define HEAPLEN 10
Heap *h = initHeap(HEAPLEN);
int cnt = HEAPLEN;
while (cnt--) {
int val = rand() % 100;
push(h, val);
}
showHeap(h);
printf("pop: [");
while (!isEmpty(h)) {
printf("%d ", pop(h));
}
printf("]\n");
freeHeap(h);
#undef HEAPLEN
return 0;
}
3. 堆排序
-
时间复杂度O(nlogn),是不稳定排序
-
基本思路
①无序序列先完成大顶堆的调整(线性建堆法)
②弹出堆顶到堆尾
③向下调整
#include <stdio.h>
#include <stdlib.h>
#include <time.h>
#define swap(a, b) {\
__typeof(a) __tmp = a;\
a = b; b = __tmp;\
}
void downAdj(int *arr, int idx, int len) {
while (idx*2+1 < len) {
int tmp = idx, l = idx*2+1, r = idx*2+2;
if (arr[l] > arr[tmp]) tmp = l;
if (r < len && arr[r] > arr[tmp]) tmp = r;
if (tmp == idx) break;
swap(arr[idx], arr[tmp]);
idx = tmp;
}
return ;
}
void heap_sort(int *arr, int len) {
// 线性建堆法:最后一个孩子下标len-1
for (int i = (len-2)/2; i >= 0; i--) downAdj(arr, i, len);
for (int i = len-1; i > 0; i--) {
swap(arr[0], arr[i]);
downAdj(arr, 0, i);
}
return ;
}
void showArr(int *arr, int len) {
printf("arr:[");
for (int i = 0; i < len; i++) {
i && printf(",");
printf("%d", arr[i]);
}
printf("]\n");
return ;
}
int main() {
srand(time(0));
#define ARRLEN 10
int arr[ARRLEN];
for (int i = 0; i < ARRLEN; i++) {
arr[i] = rand() % 100;
}
showArr(arr, ARRLEN);
heap_sort(arr, ARRLEN);
showArr(arr, ARRLEN);
#undef ARRLEN
return 0;
}