前言
普通的二叉树是不适合用数组来存储的,因为可能会存在大量的空间浪费。而完全二叉树更适合使用顺序结 构存储。
现实中我们通常把堆 ( 一种二叉树 ) 使用顺序结构的数组来存储,需要注意的是这里的堆和操作系统 虚拟进程地址空间中的堆是两回事,一个是数据结构,一个是操作系统中管理内存的一块区域分段。
1.堆的概念及结构
如果有一个关键码的集合 K = { k0, k1,k2,…,k(n-1)},把它的所有元素按完全二叉树的顺序存储方式存储 在一个一维数组中,并满足:Ki <=K(2*i+1)且Ki<=K(2*i+2)(Ki >=K(2*i+1)且Ki>=K(2*i+2) i =0 , 1 , 2…,则称为小堆 (或大堆) 。将根结点最大的堆叫做最大堆或大根堆,根结点最小的堆叫做最小堆或小根堆。(这里的数字都是对应的下标)
堆的性质:
堆中某个结点的值总是不大于或不小于其父结点的值;
堆总是一棵完全二叉树。
2.堆的创建和功能实现
2.1堆的基本结构的创建和相关函数声明。
#pragma once #include <stdio.h> #include <stdlib.h> #include <assert.h> #include <stdbool.h> typedef int Datatype; typedef struct Heap { Datatype* a; int size; int capacity; }HP; // 堆的初始化 void HeapInit(HP* hp); // 堆的销毁 void HeapDestory(HP* hp); // 堆的插入 void HeapPush(HP* hp, Datatype x); // 堆的删除 void HeapPop(HP* hp); // 取堆顶的数据 Datatype HeapTop(HP* hp); // 堆的数据个数 bool HeapSize(HP* hp); // 堆的判空 int HeapEmpty(HP* hp); //向上调整 void Adjustup(Datatype* a, int child); //向下调整 void Adjustdown(Datatype* a, int n, int parent); //数据交换 void Swap(Datatype* p1, Datatype* p2);
2.2 各函数的实现与讲解
2.1堆的初始化和销毁
堆的初始化和销毁与以前的动态数组实现顺序表和栈的初始化和销毁基本是一样的,在这里小编就不多解释了。
// 堆的初始化 void HeapInit(HP* hp) { assert(hp); hp->a = NULL; hp->size = hp->capacity = 0; } // 堆的销毁 void HeapDestory(HP* hp) { assert(hp); free(hp->a); hp->a = NULL; hp->size = hp->capacity = 0; }
2.2堆数据的插入
补充向上调整法
随便给出一个数组,这个数组逻辑上可以看做一颗完全二叉树,但是还不是一个堆,现在我们通过算法,把它构建成一个堆。根结点左右子树不是堆,我们怎么调整呢?
向上调整法。
通过比较新插入元素与其父节点的值来判断是否需要进行交换。如果新插入元素的值大于父节点的值,就将它们进行交换,并更新索引值。这样,逐步向上调整,直到新插入元素找到了合适的位置,保证了堆的性质。
//向上调整 void Adjustup(Datatype* 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 HeapPush(HP* hp, Datatype x) { assert(hp); if (hp->size == hp->capacity) { int newcapacity = hp->capacity == 0 ? 4 : hp->capacity * 2; Datatype* temp = (Datatype*)realloc(hp->a, sizeof(Datatype) * newcapacity); if (temp == NULL) { perror("realloc fail"); return; } hp->a = temp; hp->capacity = newcapacity; } hp->a[hp->size] = x; hp->size++; Adjustup(hp->a, hp->size-1); }
例如数组a[]={1, 5, 3, 8, 7, 6},依次插入并建立大堆后的顺序是:
- 插入 1,堆变为 [1]
- 插入 5,堆变为 [5, 1]
- 插入 3,堆变为 [5, 1, 3]
- 插入 8,堆变为 [8, 5, 3, 1]
- 插入 7,堆变为 [8, 7, 3, 1, 5]
- 插入 6,堆变为 [8, 7, 6, 1, 5, 3]
所以,建立大堆后的顺序是 [8, 7, 6, 1, 5, 3]。
2.3堆的删除
补充向下调整法
在这里惯性思维是直接删除头顶数据,然后重新建堆,通过向上调整法,但是我们需要从最后一个元素依次遍历向上调整。
这里我们采用向下调整法。
删除堆是删除堆顶的数据,将堆顶的数据根最后一个数据一换,然后删除数组最后一个数据,再进行向下调整算法。
本张图是小堆的删除。大堆的删除方式是一样的。
因为堆数据插入是建立大堆的,所以这里的代码是大堆的向下调整。
//向下调整 void Adjustdown(Datatype* a, int n, int parent) { //假设法,假设左孩子大 int child = parent * 2 + 1; while (child < n ) { if (child + 1 < n && a[child + 1] > a[child])//a[child+1]<a[child] child = child + 1; if (a[child] > a[parent]) { //a[child]<a[parent] Swap(&a[child], &a[parent]); parent = child; child = parent * 2 + 1; } else break; } }
堆的删除
// 堆的删除 void HeapPop(HP* hp) { assert(hp); assert(hp->size > 0); Swap(&hp->a[0], &hp->a[hp->size - 1]); hp->size--; Adjustdown(hp->a, hp->size, 0); }
最后我们会发现删除的数据是从大到小排列的,这里就可以牵扯到堆排序的应用,小编在下节会讲解的。
2.4其他函数实现(交换,判空,堆顶数据,数据个数)
//数据交换 void Swap(Datatype* p1, Datatype* p2) { Datatype temp = *p1; *p1 = *p2; *p2 = temp; } // 取堆顶的数据 Datatype HeapTop(HP* hp) { assert(hp); assert(hp->size > 0); return hp->a[0]; } // 堆的数据个数 int HeapSize(HP* hp) { assert(hp); return hp->size; } // 堆的判空 bool HeapEmpty(HP* hp) { assert(hp); return hp->size == 0; }
3.代码测试
#include "Heap.h" void TestHeap1() { int a[] = { 4,2,8,1,5,6,9,3,2,23,55,232,66,222,33,7,66,3333,999 }; //int a[] = { 1,5,3,8,7,6 }; HP hp; HeapInit(&hp); for (int i = 0; i < sizeof(a) / sizeof(int); i++) { HeapPush(&hp, a[i]); } printf("堆的大小为%d\n", HeapSize(&hp)); int i = 0; while (!HeapEmpty(&hp)) { printf("%d ", HeapTop(&hp)); //a[i++] = HPTop(&hp); HeapPop(&hp); } printf("\n"); } /* // 找出最大的前k个 int k = 0; scanf("%d", &k); while (k--) { printf("%d ", HeapTop(&hp)); HeapPop(&hp); } printf("\n"); HeapDestory(&hp); }*/ int main(){ TestHeap1(); return 0; }
4.堆的选择题(方便大家理解)
1. 下列关键字序列为堆的是:(A)
A 100 , 60 , 70 , 50 , 32 , 65 B 60 , 70 , 65 , 50 , 32 , 100 C 65 , 100 , 70 , 32 , 50 , 60
D 70 , 65 , 100 , 32 , 50 , 60 E 32 , 50 , 100 , 70 , 65 , 60 F 50 , 100 , 70 , 65 , 60 , 32
2. 已知小根堆为 8 , 15 , 10 , 21 , 34 , 16 , 12 ,删除关键字 8 之后需重建堆,在此过程中,关键字之间的比较次数是(C)。
A 1 B 2 C 3 D 4
3. 最小堆 [ 0 , 3 , 2 , 5 , 7 , 4 , 6 , 8 ], 在删除堆顶元素 0 之后,其结果是(C)
A [ 3 , 2 , 5 , 7 , 4 , 6 , 8 ] B [ 2 , 3 , 5 , 7 , 4 , 6 , 8 ]
C [ 2 , 3 , 4 , 5 , 7 , 8 , 6 ] D [ 2 , 3 , 4 , 5 , 6 , 7 , 8 ]