目录
一.二叉树的顺序结构
普通的二叉树是不适合用数组来存储的,因为可能会存在大量的空间浪费。而完全二叉树更适合使用顺序结 构存储。现实中我们通常把堆(一种二叉树)使用顺序结构的数组来存储,需要注意的是这里的堆和操作系统 虚拟进程地址空间中的堆是两回事,一个是数据结构,一个是操作系统中管理内存的一块区域分段。
二.堆的概念及结构
堆的性质:
1.堆中某个节点的值总是不大于或不小于其父节点的值;
2.堆总是一棵完全二叉树。
三.堆的实现
1.向上调整算法
将所要添加的数据放到二叉树的最后,依次与符合条件的父节点进行数据交换(子节点大于父节点或者子节点小于父节点),直到不再符合条件或者数据已经到达根节点。(在这里我们以创建小堆为例)还要注意的是在这里我们所要传的参数不是结构体,而是结构体中的数组,这是为了便于在之后的堆排序便于操作。
void AdjustUp(HPDataType* a, int child) { int parent = (child - 1) / 2; while (child>0) { if (a[child] < a[parent]) { Swap(&a[child], &a[parent]); child = parent; parent = (child - 1) / 2; } else { break; } } }
2.向下调整算法
向下调整需要我们将最上面的根节点依次与符合条件的子节点进行数据交换(子节点大于父节点或者子节点小于父节点),直到不再符合条件或者数据已经到达叶子节点。(在这里我们以创建小堆为例)
void AdjustDown(HPDataType* a, int n, int parent) { int child = 2 * parent + 1; while (child < n)//child>n说明孩子已经不存在,调整到叶子了。 { if (child + 1 < n && a[child + 1] < a[child])//确保可以检测到最后一片叶子 { child++; } if (a[parent] > a[child]) { Swap(&a[parent], &a[child]); parent = child; child = 2 * parent + 1; } else { break; } } }
3.添加数据
这一步需要我们将一个数据插入到一个堆中。因为我们在这里使用的是顺序结构,所以在开头需要我们自己动态申请一块空间,在将数据添加进去,然后我们要用到上面的向上调整算法将要添加的数据向上调整到对应的位置上,使这一串的数据形成一个堆。最后在世size+1就可以了。
void HPPush(HP* php, HPDataType x) { assert(php); if (php->size == php->capacity) { int newcapacity = php->capacity == 0 ? 4 : php->capacity * 2; HPDataType* tmp = (HPDataType*)realloc(php->a,sizeof(HPDataType) * newcapacity); if (tmp == NULL) { perror("realloc"); return; } php->a = tmp; php->capacity = newcapacity; } php->a[php->size] = x; php->size++; AdjustUp(php->a, php->size - 1); }
4.删除数据
由于删除叶子节点是没有任何的意义的,所以这里的杉树数据删除的是根节点的数据。这时候我们要先将根节点的数据和最后 一个叶子节点的数据进行交换,然后size-1,最后将交换后的根节点的数据使用向下调整的算法将数据调整到正确的位置,使它形成一个堆。
void HPPop(HP* php) { assert(php); assert(php->size > 0); Swap(&php->a[0], &php->a[php->size - 1]); php->size--; AdjustDown(php->a, php->size - 1, 0); }
5.完整代码
Heap.c
#include"Heap.h" void Swap(HPDataType* p1, HPDataType* p2) { HPDataType ret = *p1; *p1 = *p2; *p2 = ret; } void HPInit(HP* php) { assert(php); php->a = NULL; php->capacity = php->size = 0; } void AdjustDown(HPDataType* a, int n, int parent) { int child = 2 * parent + 1; while (child < n)//child>n说明孩子已经不存在,调整到叶子了。 { if (child + 1 < n && a[child + 1] < a[child])//确保可以检测到最后一片叶子 { child++; } if (a[parent] > a[child]) { Swap(&a[parent], &a[child]); parent = child; child = 2 * parent + 1; } else { break; } } } void HPPop(HP* php) { assert(php); assert(php->size > 0); Swap(&php->a[0], &php->a[php->size - 1]); php->size--; AdjustDown(php->a, php->size - 1, 0); } void AdjustUp(HPDataType* a, int child) { int parent = (child - 1) / 2; while (child>0) { if (a[child] < a[parent]) { Swap(&a[child], &a[parent]); child = parent; parent = (child - 1) / 2; } else { break; } } } void HPPush(HP* php, HPDataType x) { assert(php); if (php->size == php->capacity) { int newcapacity = php->capacity == 0 ? 4 : php->capacity * 2; HPDataType* tmp = (HPDataType*)realloc(php->a,sizeof(HPDataType) * newcapacity); if (tmp == NULL) { perror("realloc"); return; } php->a = tmp; php->capacity = newcapacity; } php->a[php->size] = x; php->size++; AdjustUp(php->a, php->size - 1); } HPDataType HPTop(HP* php) { assert(php); assert(php->size > 0); return php->a[0]; } bool HPEmpty(HP* php) { assert(php); return php->size == 0; } void HPDestroy(HP* php) { assert(php); free(php->a); php->a = NULL; php->capacity = php->size = 0; }
Heap.h
#include<stdio.h> #include<assert.h> #include<stdlib.h> #include<stdbool.h> typedef int HPDataType; typedef struct Heap { HPDataType* a; int size; int capacity; }HP; void Swap(HPDataType* p1, HPDataType* p2); void AdjustUp(HPDataType* a, int child); void AdjustDown(HPDataType* a, int n, int parent); void HPInit(HP* php); void HPDestroy(HP* php); void HPPush(HP* php, HPDataType x); void HPPop(HP* php); HPDataType HPTop(HP* php); bool HPEmpty(HP* php);
test.c
void test() { //创建一个小堆 int arr[] = { 28,15,19,25,18,34,65,49,27,20,}; HP hp; HPInit(&hp); for (int i = 0; i < (sizeof(arr) / sizeof(int)); i++) { HPPush(&hp, arr[i]); } for (int i = 0; i < (sizeof(arr) / sizeof(int)); i++) { printf("%d ", hp.a[i]); } printf("\n"); }
四.堆的应用
1.堆排序
1.建堆
首先,我们来考虑如何创建一个堆
a.向上调整建堆
先来看代码:
void HeapSort(int* a,int n) { int i = 0; for(i = 1; i < n; i++) { AdjustUp(a, i); } printf("\n"); }
观察代码,我们可以看见代码使用了一个向上调整的算法依次从第二个数据开始向上将数据进行向上调整 ,这样就可以完成一个堆的创建。我们最开始想到的方法可能是将数据依次插入但是这样需要动态开辟出一块空间来,这样就大大地提高了空间复杂度,所以我们就直接使用向上调整算法来进行建堆,这也是之前写向上调整算法的时候没有直接传结构体,而是传结构体中的数组指针的原因。
向上调整建堆的时间复杂度:
因为堆是完全二叉树,而满二叉树也是完全二叉树,此处为了简化使用满二叉树来证明(时间复杂度本来看的 就是近似值,多几个节点不影响最终结果):
首先我们先来算一下一个高度位h的二叉树的数据个数是多少?
我们假设二叉树的高度为h,二叉树的数据个数为N。
接下来我们看一下一共需要向上调整的次数:
由于第一层不需要调整,所以我们直接从第二层开始计算就可以了。
b.向下调整建堆
向下调整建堆需要我们从最后一个叶子节点的父节点开始依次向下调整,直到调整到根节点为止。
void HeapSort(int* a,int n) { int i = 0; for (i = (n - 1 - 1) / 2; i >= 0; i--) { AdjustDown(a, n, i); } for (int i = 0; i < n; i++) { printf("%d ", a[i]); } printf("\n"); }
向下调整建堆的时间复杂度:
这里我们要注意,根据向下调整建堆的原理可以知道我们不需要去调整最后一层叶子节点的位置。
我们对比一下两种方法的时间复杂度可以看出向下调整建堆的效率是比较高的,所以我们要优先使用向下调整去建堆。
其次,我们还需要考虑我们要创建一个大堆还是小堆。
升序:建大堆
降序:建小堆
如果我们想要使用一个堆来实现堆的排序,我们需要按照上面的规则去创建。那么我们为什么不使用小堆去进行升序排列或者使用大堆去进行降序排列呢?
其实这是因为使用小堆去进行升序排列或者使用大堆去进行降序排列会造成父子的关系混乱,在每一次的选出最大或者最小的数据时,还需要再一次地将堆重新排列重新形成一个堆,这样就大大降低的建堆的效率。
2.利用堆删除思想来进行排序
建堆和堆删除中都用到了向下调整,因此掌握了向下调整,就可以完成堆排序。
void HeapSort(int* a,int n) { int i = 0; for(i = 1; i < n; i++) { AdjustUp(a, i); } printf("\n"); int end = n - 1; while (end > 0) { Swap(&a[0], &a[end]); AdjustDown(a, end, 0); --end; } for (int i = 0; i < n; i++) { printf("%d ", a[i]); } }
2.TOP-K问题
TOP-K问题:即求数据结合中前K个最大的元素或者最小的元素,一般情况下数据量都比较大。
比如:专业前10名、世界500强、富豪榜、游戏中前100的活跃玩家等。
对于Top-K问题,能想到的最简单直接的方式就是排序,但是:如果数据量非常大,排序就不太可取了(可能 数据都不能一下子全部加载到内存中)。最佳的方式就是用堆来解决,基本思路如下:
1. 用数据集合中前K个元素来建堆
前k个最大的元素,则建小堆
前k个最小的元素,则建大堆
2. 用剩余的N-K个元素依次与堆顶元素来比较,不满足则替换堆顶元素将剩余N-K个元素依次与堆顶元素比完之后,堆中剩余的K个元素就是所求的前K个最小或者最大的元素。
#include"Heap.h" #include<time.h> void write() { FILE* pf = fopen("test.txt", "w"); if (pf == NULL) { perror("fopen"); return; } int i = 0; for (i = 0; i < 100; i++) { int num = 0; num = rand() % 100000; fprintf(pf, "%d\n", num); } fclose(pf); pf = NULL; } void HeapSort() { FILE* pf = fopen("test.txt", "r"); if (pf == NULL) { perror("fopen"); return; } int num = 0; int k = 0; printf("请输入要筛选的个数\n"); scanf("%d", &k); HPDataType* heap = (HPDataType*)malloc(k*sizeof(HPDataType)); if (heap == NULL) { perror("malloc"); return; } int i = 0; for (i = 0; i < k; i++) { fscanf(pf, "%d", &num); heap[i] = num; } for (i = (k - 2) / 2; i >= 0 ; i--) { AdjustDown(heap, k, i); } while (fscanf(pf, "%d", &num) >0) { if (num > heap[0]) { heap[0] = num; AdjustDown(heap, k, 0); } } for (i = 0; i < k; i++) { printf("%d\n", heap[i]); } fclose(pf); pf = NULL; } int main() { srand((unsigned int)time(NULL)); write(); HeapSort(); return 0; }
思路,用rand函数随机生成任意个数据并且将它们写入一个文件中,再将动态申请k*int个空间的容量,将文件中的数据的前k个数据插入到我们所申请的空间中,并且将它们使用向下调整进行排序形成一个小堆。再接着依次读取后n-k个数据与小堆的堆顶数据进行比较,如果读取的数据比堆顶的数据大,则将读取的数据取代堆顶的数据,并且将其向下调整,最后就可以得到最大的前k个数据。(如果我们想要测试一下程序选出的数据是否是最大的前k个数据,可以手动的改动文件中的数据,再运行程序看是否将改动的数据筛选出来)
这个算法的优点就是只需要用到k*int个空间就可以将一串非常多的数据挑选出最大(或者最小)的前k个数据,这样就大大的节省了空间。