堆的概念和性质
概念
如果有一个关键码的集合K = {k0,k1, k2,…,kn-1},把它的所有元素按完全二叉树的顺序存储方式存储在一个一维数组中,并满足:Ki <= K2i+1 且 Ki<=K2i+2 ,则称为小堆(或大堆)。
将根节点最大的堆叫做最大堆或大根堆,根节点最小的堆叫做最小堆或小根堆。
通俗来讲,堆就是按完全二叉树的顺序存储的一维数组(完全二叉树指前n-1层都是满的,第n层连续的二叉树)。
如果堆顶元素是整个堆最小的,这个堆就叫小堆或小根堆;反之,如果堆顶元素是整个堆最大的,这个堆就叫大堆或大根堆。
性质
堆中某个节点的值总是不大于或不小于其父节点的值;
堆总是一棵完全二叉树。
由父节点求子节点:
child1=parent*2+1
child2=parent*2+2
由子节点求父节点:
parent=(child-1)/2;
做个题练练吧!
1.下列关键字序列为堆的是:()
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
解答:做这道题时需要用到数形结合的思想和堆的性质,详细解答见下图。
根据堆的性质:堆中某个节点的值总是不大于或不小于其父节点的值可以得出,正确答案为A
堆的实现
建堆
如果我们想要使用堆,那么第一步就需要建堆,建堆可以用两种方式:给定一个空数组,每次向里面插入一个数据,或是给定一个数组,利用向上调整或向下调整整理数组直到将数组整理成大堆/小堆的形式。让我们先来看看给定数组的情况。
利用已有数组建堆
利用已有数组建堆又可以分成向上调整建堆和向下调整建堆,在实际使用中,我们推荐使用向下调整建堆,为什么呢?一起往下看吧!
向上调整建堆(建大堆)
向上调整思想:
从第child个子节点开始,和它的父节点进行对比,若父节点小于子节点,则交换二者的位置,逐层向上,直到这条分支上的所有节点都满足父节点大于等于子节点的要求,就进行下一轮对比。
实现:
void adjustUp(dataType* a, int child) {
assert(a);
while (child > 0) {
int parent = (child - 1) / 2;
if (a[parent] < a[child]) {
swap(&a[parent], &a[child]);
child = parent;
}
else break;
}
}
向上调整建堆:
for (int i = 0; i < size; i++) {
adjustUp(a, i);
}
向下调整建堆(建大堆)
向下调整前提:子树必须是大堆(若建小堆,则子树必须是小堆)
向下调整思想:
从第parent个节点出发,与他较大的子节点比较(如果建小堆,则与较小的比较),若父节点小于子节点,则交换位置
实现:
void adjustDown(dataType* a, int n,int parent) {
assert(a);
int child=parent*2+1;
while (child < n) {
if(child+1<n && a[child]<a[child+1]) ++child;
if (a[parent] < a[child]) {
swap(&a[parent], &a[child]);
parent=child;
child=parent*2+1;
}
else break;
}
}
向下调整建堆:
向下调整建堆指从第一个不是叶节点的节点开始,与他的子节点比较,依次向下建堆
为什么从(n-1-1)/2开始?
前面我们提到,向下调整建堆要从最后一个不是叶节点的下标开始,而最后一个不是叶节点的下标即为最后一个叶节点的父节点。因为一共n个节点,则最后一个节点的下标是n-1,它的父节点是(n-1-1)/2。(见堆的性质)
for(int i=(n-1-1)/2;i>=0;--i){
adjustDown(a,n,i);
}
建堆时间复杂度
虽然向上调整建堆和向下调整建堆的时间复杂度都是O(NlogN),但是由于向上调整的时间复杂度是O(NlogN)而向下调整的时间复杂度是O(N),所以向下调整建堆还是比向上调整建堆要更优一些,建议使用向下调整建堆。
初始化空堆及数据的插入删除
初始化空堆
void initHP(HP* php) {
assert(php);
php->a = (dataType*)malloc(sizeof(dataType) * 4);
php->capacity = 4;
php->size = 0;
}
数据插入
插入数据要注意扩容的问题,插入之后要使用向上调整排序
void pushHP(HP* php, dataType x) {
assert(php);
if (php->capacity == php->size) {
int newCap = 2 * php->capacity;
dataType* tmp = (dataType*)realloc(php->a, sizeof(dataType)*newCap);
php->a = tmp;
php->capacity = newCap;
}
php->a[php->size] = x;
php->size++;
adjustUp(php->a, php->size - 1);
}
数据删除
数据删除使用的思想是将根部数据和最后一个叶节点交换,然后让size-1,即删除了数据,之后再用向下调整重新排序即可
void popHP(HP* php) {
assert(php);
swap(&php->a[0], &php->a[php->size - 1]);
php->size--;
adjustDown(php->a,php->size,0);
}
堆销毁
void destroyHeap(HP* php) {
php->capacity = php->size = 0;
free(php->a);
php->a = NULL;
}
堆的应用
堆排序
堆排序问题采用两步走的解决办法(以升序为例)
第一步:建堆(升序建大堆)
第二步:每次交换第一个与最后一个,因为建的是大堆,所以最后一个节点肯定是整个数组最大的,又因为要排升序,所以每次把最大的放到最后,再将排序的数组大小减一,不断重复直到需要排序的数组大小为0时,得到的就是升序啦~文字描述略显苍白,话不多说,让我们直接上代码
void heapSort(dataType* a,int size) {
assert(a);
//向下调整建堆
for (int i = ( size- 1 - 1) / 2; i >= 0; --i) {
adjustDown(a,size, i);
}
//排升序,小的在前,建大堆
while (size) {
swap(&a[0], &a[size - 1]);
--size;
adjustDown(a,size, 0);//把小的往下换
}
}
TOP-K问题
TOP-K问题:求数据集合中前K个最大或最小的元素,一般情况下数据量都比较大。 比如:专业前10名、世界500强、富豪榜、游戏中前100的活跃玩家等。 对于Top-K问题,能想到的最简单直接的方式就是排序,但是:如果数据量非常大,排序就不太可取了(可能 数据都不能一下子全部加载到内存中)。
最佳的方式就是用堆来解决,基本思路如下:
1. 用数据集合中前K个元素来建堆 前k个最大的元素,则建小堆 前k个最小的元素,则建大堆 2. 用剩余的N-K个元素依次与堆顶元素来比较,不满足则替换堆顶元素 将剩余N-K个元素依次与堆顶元素比完之后,堆中剩余的K个元素就是所求的前K个最小或者最大的元素。
//如果要取最大的前5个,先取前5个建小堆,剩下的依次与小堆堆顶比,如果大于堆顶,则这个成为新的堆顶,再向下调整
void PrintTopK(int* a, int n, int k) {
assert(a);
int* topK = (int*)malloc(sizeof(int) * k);
for (int i = 0; i < k; i++) {
topK[i] = a[i];
}
//如果要取最大的前5个,先取前5个建小堆,剩下的依次与小堆堆顶比,如果大于堆顶,则这个成为新的堆顶,再向下调整
for (int i = (k - 2) / 2; i >= 0; --i) {
adjustDown(topK, k, i);
}
for (int i = k; i < n; ++i) {
if (topK[0] < a[i]) {
swap(&topK[0], &a[i]);
adjustDown(topK, k, 0);
}
}
heapSort(topK, k);
for (int i = 0; i <k; ++i) {
printf("%d ", topK[i]);
}
}
int main() {
int a[] = { 15,9,3,7,6,4,10,87,37,12,14,1 };
int size = sizeof(a) / sizeof(a[0]);
PrintTopK(a, size, 5);
}
也可以对文件里的数据进行筛选,具体见下。
void PrintTopK(const char* f,int k)
{
FILE* file = fopen(f, "r");
if (file == NULL) {
perror("fopen fail");
}
// 1. 建堆--用a中前k个元素建堆
int* topK = (int*)malloc(sizeof(int) * k);
for (int i = 0; i < k; i++) {
int val = 0;
fscanf(file, "%d", &val);
topK[i] = val;
}
for (int i = (k - 2) / 2; i >= 0; --i) {
adjustDown(topK, k, i);
}
// 2. 将剩余n-k个元素依次与堆顶元素交换,不满则则替换
int val = 0;
//int ret=fscanf(file, "%d", &val);
while (fscanf(file, "%d", &val) != EOF) {
if (val > topK[0]) {
swap(&val, &topK[0]);
adjustDown(topK, k, 0);
}
//fscanf(file, "%d", &val);
}
//heapSort(topK, k);
fclose(file);
for (int i = 0; i < k; i++) {
printf("%d ", topK[i]);
}
}
void createTopk()
{
int n = 1000;
srand(time(0));
FILE* file = fopen("data.txt", "w");
if (file == NULL) {
perror("fopen fail");
return;
}
for (int i = 0; i < n; i++) {
fprintf(file,"%d\n", rand() % 10000);
}
fclose(file);
}
int main() {
//createTopk();
PrintTopK("data.txt", 10);
return 0;
}