堆
堆的概念
堆是什么,完全二叉树在上一篇树的文章中提到过,堆也就是利用了完全二叉树的思想,但是它的结构定义没有像树一样定义,只是它的逻辑结构和完全二叉树一样;
堆分为大顶堆和小顶堆:
大顶堆:
一个结点的左右子孩子的值都小于它自己的值,如图:
小顶堆:
一个结点的左右还在都大于它自己的值,如图:
现在把这个小顶堆的数据带入到数组中,从上到小,从左到右分别带入到数组中,如图:
为什么要把数组的0位置空出来,因为方便写代码的时候方便,只是我学到的一种编程技巧,这样可以使得代码更简洁;
优先队列:
堆还有一个名字叫优先队列,其实优先队列和堆的数据结构是相同的,优先队列,你直接可以理解字面上的意思,优先,要么就是最大的或者最小的先出队,而堆弹出元素也是从堆顶弹出,入堆也是从堆的最后一个位置进行入堆,而队列也是一样的;所以他们的数据结构是一样的,他的操作会在文章后面会讲。
它的数据结构:
结构定义:
物理结构:
他的物理结构有,第一个数据域,堆元素的个数,堆存储元素个数的最大值;结构体封装如下:
typedef struct priority_queue { int cnt, size;//堆里现在的元素个数和堆的大小 void *data;//数据域 } priority_queue;
逻辑结构:
他的逻辑结构就是,优先(要么定义的是大顶堆,要么定义的是小顶堆),这个就是以情况而定来的逻辑,不过添加元素和弹出元素都是一样的,只不过相当于在代码中大于变小于或者小于变大于,中心的逻辑思想是没有变化的;还有一点,现在把堆看为完全二叉树,他的结点的值必须满足要么大于两个子孩子,要么小于两个子孩子;
在这个图中,是一个小顶堆,根节点小于他的两个子孩子,3小于他的两个子孩子;所以必须要维持这个性质,才会不破坏堆的数据结构;
结构操作:
入堆:
现在以这个大顶堆为例,入堆元素31,由于堆是完全二叉树,那么他就应该接在元素17的左子树上,也就是队列的入队,应该从队尾进行入队;
然后开始进行比较,由于是大顶堆,如果它大于他的父节点就与父节点的值进行交换;最终变为:
然后就完成了入堆操作;
弹出:
堆又名优先队列嘛,出队都是从头出,那堆也应该从头弹出嘛,那么如果进行弹出呢,将堆头的元素被堆尾的元素覆盖,然后将堆尾删除后,在进行排序维护堆的结构:
现在要将35进行弹出那么将17覆盖掉35,删除结点17:
利用大顶堆的性质进行从高往低进行去维护排序,最终结果如下:
最后完成弹出35;
代码实现:
实现的是大顶堆:
#include <stdio.h> #include <time.h> #include <stdlib.h> //代码实现的大顶堆 #define swap(a, b) {\ __typeof(a) __a = a; a = b; b = __a;\ } typedef struct priority_queue { int cnt, size;//堆里现在的元素个数和堆的大小 int *data;//数据域 } priority_queue; priority_queue *init(int n) {//向计算机借空间 priority_queue *q = (priority_queue *)malloc(sizeof(priority_queue)); q->cnt = 0; q->size = n; q->data = (int *)malloc(sizeof(int) * (n + 1)); return q; } int empty(priority_queue *q) {//判空操作 return q->cnt == 0; } int top(priority_queue *q) {//获取堆顶元素 return q->data[1]; } //插入堆元素 int push(priority_queue *q, int val) { if (q->cnt == q->size) return 0; q->data[++q->cnt] = val;//向堆尾入堆 int ind = q->cnt;//获取当前入堆元素的下标 while (ind >> 1 && q->data[ind] > q->data[ind >> 1]) {//下标右移一位就是对应的父节点的坐标 //这个过程需要去模拟一下才能明白 swap(q->data[ind >> 1], q->data[ind]); ind >>= 1; } return 1; } //弹出堆顶元素 int pop(priority_queue *q) { if (empty(q)) return 0; q->data[1] = q->data[q->cnt--];//堆顶元素被堆尾元素覆盖 int ind = 1;//标记当前堆顶进行向下排序 while (ind << 1 <= q->cnt) { int l = ind << 1, r = ind << 1 | 1, temp = ind;//l代表左孩子,r代表有孩子的坐标,temp记录需要交换位置的坐标 if (q->data[temp] < q->data[l]) temp = l;//如果左孩子的元素大于父节点,temp记录左孩子坐标 if (r <= q->cnt && q->data[temp] < q->data[r]) temp = r; //如果有右孩子,如果右孩子大于temp现在位置的值,是temp位置因为现在temp可能是父节点也可能是左孩子 if (temp == ind) break;//如果temp == ind 说明没有发生交换,说明父节点大于左右孩子的值 swap(q->data[temp], q->data[ind]);//交换值 ind = temp; } return 1; } void output(priority_queue *q) { printf("priority_queue(%d) = [", q->cnt); for (int i = 1; i <= q->cnt; i++) { i != 1 && printf(", "); printf("%d", q->data[i]); } printf("]\n"); return ; } void clear(priority_queue *q) { if (!q) return ; free(q->data); free(q); return ; } int main() { srand(time(0)); int op, val; priority_queue *q = init(10); for (int i = 0; i < 20; i++) { op = rand() % 4; val = rand() % 100; switch (op) { case 0: case 1: case 2: { printf("%d push in priority_queue is %d\n", val, push(q, val)); } break; case 3: { int num = top(q); printf("%d pop in priority_queue is %d\n", num, pop(q)); } } output(q); } clear(q); return 0; }
堆排序:
我的理解是实现了大顶堆,可以去排序升序的序列,小顶堆可以去实现排序降序序列;
下面是代码实现:
#include <stdio.h> #include <stdlib.h> #include <time.h> #define swap(a, b) {\ __typeof(a) __a = a; a = b; b = __a;\ } void Updown_val(int *arr, int n, int ind) {//将一部分形成大顶堆 while (ind << 1 <= n) { int l = ind << 1, r = ind << 1 | 1, temp = ind; if (arr[l] > arr[temp]) temp = l; if (r <= n && arr[r] > arr[temp]) temp = r; if (temp == ind) break; swap(arr[temp], arr[ind]); ind = temp; } return ; } void heap_sort(int *arr, int n) { for (int i = n >> 1; i >= 1; i--) {//从下往上形成大顶堆,最终堆顶是最大的元素 Updown_val(arr, n, i); } for (int i = n; i > 1; i--) { swap(arr[1], arr[i]);//每次将目前堆中最大的元素向后交换 Updown_val(arr, i - 1, 1);//交换后,再次进行维护堆的性质 } return ; } void output(int *arr, int n) { for (int i = 1; i <= n; i++) { printf("%d ", arr[i]); } putchar(10); return ; } int main() { srand(time(0)); int val, n = 10; int arr[100]; for (int i = 1; i <= n; i++) { val = rand() % 100; arr[i] = val; } output(arr, n); heap_sort(arr, n); output(arr, n); return 0; }
下面是画图理解:
这是最初arr数组的情况,转化为了二叉树的形式,是无序的:
for (int i = n >> 1; i >= 1; i--) {//从下往上形成大顶堆,最终堆顶是最大的元素 Updown_val(arr, n, i); }
现在执行这段代码,现在n是等于10的,右移就是5,i = 5,也就是元素2的位置进行排序,也就是函数Updown_val进行排序,然后排序后的结果
i--,i = 4,也就是62的位置;
这样一直进行下去,最后形成一个标准的大顶堆:
然后执行代码:
for (int i = n; i > 1; i--) { swap(arr[1], arr[i]);//每次将目前堆中最大的元素向后交换 Updown_val(arr, i - 1, 1);//交换后,再次进行维护堆的性质 }
将堆顶元素排到堆尾去,然后再次排序时,将排序的数组的长度进行-1,第一次得到的结果位:
现在69再队尾了,也不参与后面的排序了,然后一直这样进行下去,直到排完最终结果就是这样:
最后形成单调递增序列,完成排序;