什么是堆
堆(英语:Heap)是特殊的数据结构的统称。堆通常是一个可以被看做一棵树的数组对象。
初识堆的结构
经过上面的介绍我们知道,堆通常是一个可以被看做一棵树的数组对象
,那我们一起来看一看堆到底是什么样子,它又有什么特殊的性质?
解释上图的小根堆 — — — 上面的是堆的逻辑结构
堆的存储结构
我们感受过堆的结构之后,我们来看一下堆的一些具体性质
(1)堆中某个节点的值总是不大于或不小于其父节点的值;
(2)堆总是一棵完全二叉树。(这里后面的文章中会讲解)
(3)我们知道堆的存储结构就是数组,对于数组我们通常采用下标进行一些操作:
知道儿子位置寻找父亲位置:parent = (child - 1) / 2;
知道父亲的位置寻找儿子的位置: child = parent / 2 +1(或者 + 2,因为一个父亲有左右两个儿子)
那么堆到底有什么作用
- 堆排序
- Top k (巨大数据集里面寻找前k个最大数或者最小数)
- 优先级队列(本篇不提及,后面的文章会专门讲解)
建堆
这里建堆有两种算法:向上调整法 和 向下调整法
向上调整算法 — — — 建小根堆
现在有这么一个无序数组,需要建成一个堆,这里我们先采用向上调整算法进行操作
- 首先将数组第一个数据入堆,因为堆里现在只有一个数据,所以ta现在可以看成一个大根堆或者一个小根堆
- 现在往堆里进如第二个数据,要满足小根堆的条件,就要让小的值做父亲,大的值做儿子。
按照这个算法继续下去,出现了一个新的情况:
当数据 7 进堆时,我们发现ta不满足小根堆的性质,我们对ta进行向上调整:
我们继续入堆,继续发现问题:
继续发现问题
完成小根堆的建立
完成上述过程的代码
// ------------------------ .h文件 -----------------------------
#pragma once
#include <stdio.h>
#include <assert.h>
#include <time.h>
#include <stdlib.h>
#include <string.h>
typedef struct Heap
{
int* arr;
int size;
int capacity;
}Heap;
//初始化
void HeapInit(Heap* php);
//入堆
void HeapPush(Heap* php, int x);
//向上调整算法
void AdjustUp(int* arr, int size);
// --------------------------- .c文件 -----------------------------------
#include "AdjustUp.h"
//初始化
void HeapInit(Heap* php)
{
assert(php);
php->arr = NULL;
php->capacity = 0;
php->size = 0;
}
//入堆
void HeapPush(Heap* php, int x)
{
assert(php);
if (php->size == php->capacity)
{
int newcapacity = (php->capacity == 0 ? 4 : php->capacity * 2);
int* temp = (int*)realloc(php->arr, sizeof(int) * newcapacity);
if (temp == NULL)
{
perror("malloc faile");
return;
}
php->capacity = newcapacity;
php->arr = temp;
}
php->arr[php->size] = x;
php->size++;
//向上调整算法
AdjustUp(php->arr, php->size - 1);
}
void Swap(int* tmp1, int* tmp2)
{
int temp = *tmp1;
*tmp1 = *tmp2;
*tmp2 = temp;
}
//向上调整算法
void AdjustUp(int* arr, int child)
{
assert(arr);
int parent = (child - 1) / 2;
while (child > 0)
{
if (arr[child] < arr[parent])
{
Swap(&arr[child], &arr[parent]);
child = parent;
parent = (child - 1) / 2;
}
else
{
break;
}
}
}
// 主函数
#include "AdjustUp.h"
int main()
{
Heap hp;
HeapInit(&hp);
int arr[] = { 32,14,3,15,23,7,16,11,2 };
for (int i = 0; i < sizeof(arr) / sizeof(arr[0]); i++)
{
HeapPush(&hp, arr[i]);
}
return 0;
}
向上调整算法
我们在上面的学习过程中,知道了向下调整算法,向下调整算法可以建堆,可以建立大根堆或者小根堆。接下来我们试一试更有挑战性的算法。
现在我们给出一个数组,逻辑上看做一颗完全二叉树。我们通过从根节点开始的向下调整算法可以把它调整成一个小堆。
向下调整算法有一个前提:左右子树必须是一个堆(大堆或者小堆),才能调整。
向下调整算法基本思想: (以建成小堆为例)
- 从根节点开始,选出左右孩子节点中值较小的一个
- 让父亲与较小的孩子比较,若父亲大于此孩子,那么交换;若父亲小于此孩子,则不交换
结束条件:
1、父亲<=小的孩子则停止
2、调整到叶子节点(叶子节点特征为没有左孩子,就是数组下标超出了范围,就不存在了)
向下调整算法的具体步骤
我们先将这里给出的数组进行完全二叉树化。如下图
我们知道向下调整算法的主要要求就是结点的左右子树是堆,如果我们现在从根节点开始进行向下调整,肯定不符合条件!
例外:如果你像上面向上调整算法一样进行malloc空间,那么肯定是可以的,只是这样在日常代码中不会用到
因此我们现在的问题就是怎么在原有的数组上面进行向下调整算法?
既然我们从根节点进行调整不满足条件,那么我们就从叶子结点开始向下调整,调整到根结点的左右子树的时候,因为根节点的左右子树通过向下调整已经成为堆,所以我们就可以对根结点进行向下调整。
我们从哪里开始进行算法的开始?
从图中我们可以发现 22 和 7 这两个叶子结点本身各自就可以看成堆,所以我们不需要对它们进行向下调整。
所以我们可以发现,向下调整的开始就是最后一个结点的父亲 37 开始。
再进行调整
再调整,再发现问题
循环计数i–,寻找到24 进行调整
当我们走到根节点的左右子树的时候,我们已经发现左右子树已经是堆了,所以我们现在就是可以进行进行最后的调整了
向下调整算法的代码 — — — 建小堆
void AdjustDown(int* arr, int size, int parent)
{
int child = parent * 2 + 1;
while (child < size)
{
//寻找小的儿子
if (child + 1 < size && arr[child] > arr[child + 1])
{
child++;
}
//交换
if (arr[child] < arr[parent])
{
Swap(&arr[parent], &arr[child]);
parent = child;
child = parent * 2 + 1;
}
else
{
//一旦不满足,说明小堆已经形成
break;
}
}
}
利用向上调整算法进行数组的原地建堆
#include <stdio.h>
void Swap(int* p1, int* p2)
{
int tmp = *p1;
*p1 = *p2;
*p2 = tmp;
}
//向下调整算法 ————— 建小堆
void AdjustDown(int* arr, int size, int parent)
{
int child = parent * 2 + 1;
while (child < size)
{
//寻找小的儿子
if (child + 1 < size && arr[child] > arr[child + 1])
{
child++;
}
//交换
if (arr[child] < arr[parent])
{
Swap(&arr[parent], &arr[child]);
parent = child;
child = parent * 2 + 1;
}
else
{
//一旦不满足,说明小堆已经形成
break;
}
}
}
void HeapSort(int* arr, int n)
{
//最后一个结点是 (n-1), 最后一个结点的父亲是 (n-1)/2
for (int i = (n - 1 - 1) / 2; i >= 0; i--)
{
AdjustDown(arr, n, i);
}
}
int main()
{
int arr[] = { 49, 38, 65, 97, 4, 13, 27, 49, 55, 76 };
int len = sizeof(arr) / sizeof(arr[0]);
SmallRootHeap(arr, len);
return 0;
}
不懂的时候,大家可以走读一下代码,我在这里给大家提供一下一组数据边看代码,边理解
我们结束了前面的建堆的算法,现在来看一下堆排序,就很简单
如果我们现在要利用堆进排序,那么直接可以想到的方法就是每一次建堆的时候直接进行堆顶数据的弹出,因为堆顶的数据是这一组数据的最大值或者最小值。但是这种方法太慢了,想一下每求一次堆顶数据弹出之后,堆顶之后的数据就已经打破了之前的亲子关系了,我们就不能再使用关系寻找父亲、儿子,所以为了解决关系被打破的局面,我们只好将剩下的数据进行重新建堆,这种方法我们在这里就不赘述了
假设我们现在要进行降序的排列,堆排序方法讲解:
- 首先我们将无序数组原地进行建堆
建小堆还是大堆?
大家肯定会说降序嘛,那就建大堆,大堆刚好堆顶是最大的
这种方法确实可以,但是那个堆顶最大的数据你怎么处理,后面怎么求第二大的数据?
如果你把堆顶最大的数据弹出去,是不是就需要一个数组保存比每一次弹出的数字,这样空间消耗太大;其次,当你弹出堆顶的最大的数据,你下一步要求第二大数据怎么求?到头来还是要将剩下的数据重新建堆,重新求堆顶元素。
因此上面建大堆的方法不仅时间上消耗很大,空间上也是一样。
所以我们就建小堆!!!
- 将无序数组进行建小堆之后,我们知道现在堆顶是数组里面最小的元素,为了不打破根结点之后的亲子关系,符合降序特点,我们将最后一个结点的数据和堆顶的数据进行交换,这样最后一个数据就是最小的数据,
重要的来了:我们将最后一个结点进行交换后,交换后的最后一个结点已经是有序的了,在存储结构上,现在已经是数组里面出在正确位置上的元素了,所以!!!我们不把最后一个元素算进完全树里面(我这里说完全二叉树是因为还没有进行结点调整,所以不能说成堆)
- 我们进行首尾交换之后,现在的树还不是堆,但是根节点的左右子树是成型的小堆,因此我们在这里使用向下调整算法将新树调整成小堆
- 再重复交换和向下调整算法
- 最终数组就是降序的
降序— — — 堆排序代码
可直接使用
// ------------------------------------ .h文件 --------------
#pragma once
//向下调整算法:这种算法的前提是根节点的左右子树均为大堆或者小堆才可以,数组为乱序的,无法直接从根结点开始向下调整
#include <stdio.h>
#include <stdlib.h>
#include <assert.h>
#include <stdbool.h>
//向上调整算法
void AdJustUp(int* arr, int child);
//向下调整算法
void AdjustDown(int* arr, int size, int parent);
// --------------------------------- .c文件 --------------------------
#include "HeapSort.h"
void Swap(int* p1, int* p2)
{
int tmp = *p1;
*p1 = *p2;
*p2 = tmp;
}
//向下调整算法 ————— 建小堆
void AdjustDown(int* arr, int size, int parent)
{
int child = parent * 2 + 1;
while (child < size)
{
//寻找小的儿子
if (child + 1 < size && arr[child] > arr[child + 1])
{
child++;
}
//交换
if (arr[child] < arr[parent])
{
Swap(&arr[parent], &arr[child]);
parent = child;
child = parent * 2 + 1;
}
else
{
//一旦不满足,说明小堆已经形成
break;
}
}
}
//向下调整算法:这种算法的前提是根节点的左右子树均为大堆或者小堆才可以,数组为乱序的,无法直接从根结点开始向下调整
//!!!!所以我们从最后一个结点的父亲开始,一层一层往后调整
void HeapSort(int* arr, int n)
{
//建小堆
//最后一个结点是 (n-1), 最后一个结点的父亲是 (n-1)/2
for (int i = (n - 1 - 1) / 2; i >= 0; i--)
{
AdjustDown(arr, n, i);
}
//首位元素交换
int end = n - 1;
while (end > 0)
{
Swap(&arr[0], &arr[end]);
AdjustDown(arr, end, 0);
end--;
}
}
// ---------------------------------- 主函数.c ---------------------
//堆排序 --- 向下调整算法 建堆 ---
//堆排序 --- 向上调整算法 建堆 --- 将原数组建成堆
#include "HeapSort.h"
int main()
{
//堆排序 --> 降序 --->建小堆,用向上调整算法原数组建小堆,把堆顶的数据放在最后面,使用向下调整算法重新建小堆
int arr[] = { 23, 24, 4, 37, 6, 11, 2, 22, 7};
int len = sizeof(arr) / sizeof(arr[0]);
//建小堆
HeapSort(arr, len);
for (int i = 0; i < len; i++)
{
printf("%d ", arr[i]);
}
return 0;
}
运行截图
升序 — — — 堆排序接口
// -------------------------------------- .h文件 --------------------
#pragma once
#include <stdio.h>
#include <assert.h>
#include <time.h>
#include <stdlib.h>
#include <string.h>
//向上调整算法
void AdjustUp(int* arr, int size);
void HeapSort(int* arr, int n);
void AdjustDown(int* arr, int size, int parent);
// --------------------------------- .c文件 ---------------------------
#include "AdjustUp.h"
void Swap(int* tmp1, int* tmp2)
{
int temp = *tmp1;
*tmp1 = *tmp2;
*tmp2 = temp;
}
//向上调整算法 建大根堆
void AdjustUp(int* arr, int child)
{
assert(arr);
int parent = (child - 1) / 2;
while (child > 0)
{
if (arr[child] > arr[parent])
{
Swap(&arr[child], &arr[parent]);
child = parent;
parent = (child - 1) / 2;
}
else
{
break;
}
}
}
//大根堆
void AdjustDown(int* arr, int size, int parent)
{
int child = parent * 2 + 1;
while (child < size)
{
if (child + 1 < size && arr[child] < arr[child + 1])
{
child++;
}
if (arr[parent] < arr[child])
{
Swap(&arr[parent], &arr[child]);
parent = child;
child = parent * 2 + 1;
}
else
{
break;
}
}
}
void HeapSort(int* arr, int n)
{
//升序 向上调整算法 建大根堆
for (int i = 0; i < n; i++)
{
AdjustUp(arr, i);
}
//将堆顶的数据放在数组的最后的一个位置 交换
int end = n - 1;
while (end > 0)
{
Swap(&arr[0], &arr[end]);
AdjustDown(arr, end, 0);
end--;
}
}
// -------------------- 主函数文件 ------------------------------
#include "AdjustUp.h"
int main()
{
int arr[] = { 23, 24, 4, 37, 6, 11, 2, 22, 7 };
int len = sizeof(arr) / sizeof(arr[0]);
//建小堆
HeapSort(arr, len);
for (int i = 0; i < len; i++)
{
printf("%d ", arr[i]);
}
return 0;
}
升序的运行截图
完成上面的堆排序之后,我们来接受一下 Top K
问题
Top K:在很大的数据量里面寻找前k个最大的数或者最小的数
在解决面前的问题的时候,我们首先想到的是对数据进行堆排序,再取数据即可
这个想法确实是可行,但是在面对庞大的数据量,进行堆排序的时候,不管是什么排序,都需要空间,1G = 1024MB、1MB = 1024KB、1KB = 1024字节,那么十亿整数就有4GB,所以这样空间消耗太大了
TopK思想:
先随机取出N个数中的K个数,将这K个数构造为小顶堆,那么堆顶的数肯定就是这K个数中最小的数了
然后再将剩下的N-K个数与堆顶进行比较,如果大于堆顶,那么说明该数有机会成为TopK,就更新堆顶为该数,此时由于小顶堆的性质可能被破坏,就还需要调整堆;
否则说明这个数最多只能成为Top K+1,因此就不用管它 。
然后就将下一个数与当前堆顶的数作比较,根据大小关系如上面所述方法进行操作,直到N-K个数都遍历完,此时还在堆中的K个数就是TopK了。
// -----------------------------------主函数.c -----------
#define _CRT_SECURE_NO_WARNINGS 1
#include "AdjustUp.h"
//TOPK: 将前k个数据建堆,如果是前k个最大的数,那么进行堆顶和n-k个数据交换,最后剩下的10个堆里的数据是topk
int main()
{
int n = 1000000;
/*srand((time_t)time(NULL));
FILE* fin = fopen("data.txt", "w");
if (fin == NULL)
{
perror("fopen fail");
return 0;
}
for (int i = 0; i < n; i++)
{
int x = rand() % 1000000;
fprintf(fin, "%d\n", x);
}
fclose(fin);
*/
FILE* fout = fopen("data.txt", "r");
if (fout == NULL)
{
perror("fopen fail");
return 0;
}
int* topkHeap = (int*)malloc(sizeof(int) * 10);
if (topkHeap == NULL)
{
perror("malloc fail");
return 0;
}
for (int i = 0; i < 10; i++)
{
fscanf(fout, "%d", &topkHeap[i]);
}
//topk 大 建小堆
//向下调整算法 建小堆
for (int i = (10 - 1 - 1) / 2; i >= 0; i--)
{
AdjustDown(topkHeap, 10, i);
}
int val = 0;
while (!feof(fout))
{
fscanf(fout, "%d", &val);
if (val > topkHeap[0])
{
topkHeap[0] = val;
AdjustDown(topkHeap, 10, 0);
}
}
for (int i = 0; i < 10; i++)
{
printf("%d ", topkHeap[i]);
}
return 0;
}
// --------------------------------- .c 文件
#include "AdjustUp.h"
void Swap(int* tmp1, int* tmp2)
{
int temp = *tmp1;
*tmp1 = *tmp2;
*tmp2 = temp;
}
//向上调整算法 建小根堆
void AdjustUp(int* arr, int child)
{
assert(arr);
int parent = (child - 1) / 2;
while (child > 0)
{
if (arr[child] < arr[parent])
{
Swap(&arr[child], &arr[parent]);
child = parent;
parent = (child - 1) / 2;
}
else
{
break;
}
}
}
//小根堆
void AdjustDown(int* arr, int size, int parent)
{
int child = parent * 2 + 1;
while (child < size)
{
if (child + 1 < size && arr[child] > arr[child + 1])
{
child++;
}
if (arr[parent] > arr[child])
{
Swap(&arr[parent], &arr[child]);
parent = child;
child = parent * 2 + 1;
}
else
{
break;
}
}
}
void HeapSort(int* arr, int n)
{
//升序 向上调整算法 建大根堆
for (int i = 0; i < n; i++)
{
AdjustUp(arr, i);
}
//将堆顶的数据放在数组的最后的一个位置 交换
int end = n - 1;
while (end > 0)
{
Swap(&arr[0], &arr[end]);
AdjustDown(arr, end, 0);
end--;
}
}
// -------------------- .h 文件-----------------
#pragma once
#include <stdio.h>
#include <assert.h>
#include <time.h>
#include <stdlib.h>
#include <string.h>
//向上调整算法
void AdjustUp(int* arr, int size);
void HeapSort(int* arr, int n);
void AdjustDown(int* arr, int size, int parent);