1.堆的概念及结构
1.1 堆的概念
如果有一个关键码的集合K = {k0,k1, k2,…,kn-1},把它的所有元素按完全二叉树的顺序存储方式存储 在一个一维数组中,并满足:Ki = K2i+1 且 Ki >= K2i+2) i = 0,1,2…,则称为 小堆(或大堆)。将根节点最大的堆叫做最大堆或大根堆,根节点最小的堆叫做最小堆或小根堆。
1.2 堆的性质
(1)堆中某个节点的值总是不大于或不小于其父节点的值;
(2)堆总是一棵完全二叉树。
大根堆实例
89
56 78
32 28 65 49
.........
小根堆实例
12
29 31
34 48 35 69
.........
1.3 堆的底层
逻辑结构
物理结构
2.向上调整算法
push_back数据时,由于堆的底层是一个数组,只需要堆的底层数组++size即可实现,但是只插入数据就好了嘛?显然不是,我们还要保证尾插入这个数据后,小堆(大堆)的性质仍不变,即小堆(大堆)插入数据后,仍是一个小堆(大堆)。于是需要比较插入的这个数据与他的parent,通过向上调整算法,为他找到合适的位置。
简单举一个例子,图示如下:
这个算法思路我们明白了,那具体算法代码如何实现呢?
我们下文均以小堆来举例,那么大堆也是同理。
void AdjustUp(HPDataType* a,int child)
{
int parent=(child-1)/2;
//while(parent>=0)--不能用这个来判断,因为当parent为0的时候,0/2=0,陷入死循环
while(child>0)
{
if(a[child]<a[parent])
{
Swap(&a[child],&a[parent]);
child=parent;
parent=(child-1)/2;
}
else
{
break;
}
}
}
有了向上调整算法,我们就可以轻松实现在堆尾插一个数据。
代码实现如下:
void HeapPush(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, newCapacity * sizeof(HPDataType));
if (tmp == NULL)
{
perror("realloc fail");
exit(-1);
}
php->a = tmp;
php->capacity = newCapacity;
}
php->a[php->size] = x;
php->size++;
AdjustUp(php->a, php->size - 1);
}
向上调整算法的时间复杂度:
O(log2N)
3.向下调整算法
当我们要pop掉堆顶的数据时,由于小堆的堆顶为最小的数,但是孩子位置的两个数没有固定的大小关系,也就是说堆顶元素的左孩子和右孩子不一定谁大谁小,那我们删除堆顶元素之后,应该怎样调整数据位置来维持堆形态呢?
首先我们来了解一下向下调整算法。
(同样我们会以小堆来举例)
假如堆顶元素是一个较大的元素,而堆顶元素的左子树和右子树均为一个小堆,那么我们可以通过向下调整堆顶元素来完成小堆形态的维持。
图示如下:
了解原理之后,我们来学习一下如何通过代码来具体实现呢?
代码实现如下:
void AdjustDown(HPDataType* a,int n,int parent)
{
int minChild = parent * 2 + 1;
//假设左孩子是较小的孩子
while (minChild < n)
{
//找出小的那个孩子
if (minChild + 1 < minChild)
{
minChild++;
//如果右孩子小于左孩子,则右孩子是较小的孩子
}
if (a[minChild] > a[parent])
{
Swap(&a[minChild], &a[parent]);
parent = minChild;
minChild = parent * 2 + 1;
}
else
{
break;
}
}
}
注意:使用向下调整的前提是parent元素的左子树和右子树堆形态相同(即都是小堆或者都是大堆)。
接下来,我们来实现删除堆顶元素。
我们采用一种独特的方法:
(1)我们将堆顶元素和堆中最后一个元素交换(这样做我们就可以使用向下调整,因为堆顶元素的左子树和右子树仍均为小堆)
(2)堆数组--size(目的删掉最后一个元素(此时是原来的堆顶元素))
(3)对堆顶元素使用向下调整算法,直至堆形态建立
具体代码实现如下:
void HeapPop(HP* php)
{
assert(php);
assert(!HeapEmpty(php));
Swap(&php->a[0], &php->a[php->size - 1]);
php->size--;
AdjustDown(php->a, php->size, 0);
}
向下调整时间复杂度:
O(log2N)
4.使用向下调整法建堆
思想:(以用数组{15,1,19,25,8,34,65,4,27,7}建小堆为例)
因为使用向下调整法是有前提的(左子树,右子树堆形态一致),所以我们考虑从数组后端元素开始。假设所有数组元素按所给顺序按层序排成完全二叉树,找到最后一个非叶结点的结点(即最后一个叶结点的parent,本结论读者可以自己尝试画图推出),开始向下调整(因为满足向下调整条件),直到调整到根,则堆建好。
代码如下:
for (int i = (n - 1 - 1) / 2; i >= 0; --i)
{
//n-1为数组最后一个数的下标,(下标-1)/2得到最后一个非叶结点的下标
AdjustDown(a, n, i);
}
5.使用向上调整法建堆
思想:(以用数组{15,1,19,25,8,34,65,4,27,7}建小堆为例)
插入第一个元素15,把15当做堆顶元素,插入第二个元素1,因为1<15,插入1后小堆形态无法维持,对1使用向上调整算法,依次类推,直至数组中所有元素均进堆并且维持小堆形态,则小堆建立好。
代码实现:
for (int i = 1; i < n; ++i)
{
AdjustUp(a, i);
}
6.两种算法建堆时间复杂度对比
6.1使用向下调整法建堆时间复杂度:
假设树的高度为h
调整次数=每一层结点个数*这一层结点最坏向下调整次数
T(N)=2^0*(h-1)+2^1*(h-2)+....2^(h-3)*2+2^(h-2)*1
使用错位相减法求得
T(N)=N-log2(N+1)
6.2使用向上调整法建堆时间复杂度:
假设树的高度为h
调整次数=每一层结点个数*这一层结点最坏向下调整次数
T(N)=2^1*1+2^2*2+....2^(h-2)*(h-2)+2^(h-1)*(h-1)
精确算结果的话,读者可以自行使用错位相减法
这里有一个结论:高度为h,节点数量为N的完全二叉树,那么2^(h-1)=N,h=log2(N+1)
我们这里算一下大概,即只考虑最后一层的调整次数:
2^(h-1)*(h-1)*2/2=2^h*(h-1)/2=(N+1)*(log2(N+1)-1)/2
则时间复杂度大致为O(N*log2N)
由上显然知,向下调整法更优,因此我们选用向下调整法建堆
7.堆排序
大思路:选择排序,依次选数,从后往前排
void HeapSort(int* a, int n)
{
for (int i = (n - 1 - 1) / 2; i >= 0; --i)
{
AdjustDown(a, n, i);
}
//选数
int i = 1;
while (i < n)
{
Swap(&a[0], &a[n - 1]);
AdjustDown(a, n - i, 0);
++i;
}
}
8.TopK问题 -- 选取一堆数据中前K大或者前K小的数
对于选前K大的数这种问题,我们有两种方法,我们先谈思路
第一步:堆排序 O(N*logN)
第二步:堆选数
(1)建大堆? 建N个数大堆,选K次即可(Pop K次) 这种的时间复杂度为O(N+log N*K)
(2)建小堆? 假设N很大,K很小。比如:N=100亿 K=100,那么(1)方法的时间复杂度过高,不实用。
我们可以考虑用前K个数,建K个的小堆,然后依次遍历后续N-K个数,比堆顶的数据大,就替换堆顶数据,向下调整进堆。
则最后堆里面的数据就是最大的前K个。
void CreateDataFile(const char* filename, int N)
{
FILE* fin = fopen(filename, "w");
if (fin == NULL)
{
perror("fopen fail");
return;
}
srand(time(0));
for (int i = 0; i < N; ++i)
{
fprintf(fin, "%d\n", rand()%1000000);
}
fclose(fin);
}
void PrintTopK(const char* filename, int k)
{
assert(filename);
FILE* fout = fopen(filename, "r");
if (fout == NULL)
{
perror("fopen fail");
return;
}
int* minHeap = (int*)malloc(sizeof(int)*k);
if (minHeap == NULL)
{
perror("malloc fail");
return;
}
// 如何读取前K个数据
for (int i = 0; i < k; ++i)
{
fscanf(fout, "%d", &minHeap[i]);
}
// 建k个数小堆
for (int j = (k - 2) / 2; j >= 0; --j)
{
AdjustDown(minHeap, k, j);
}
// 继续读取后N-K
int val = 0;
while (fscanf(fout, "%d", &val) != EOF)
{
if (val > minHeap[0])
{
minHeap[0] = val;
AdjustDown(minHeap, k, 0);
}
}
for (int i = 0; i < k; ++i)
{
printf("%d ", minHeap[i]);
}
free(minHeap);
fclose(fout);
}