如果有一个关键码的集合K = {k0,k1, k2,…,kn-1},把它的所有元素按完全二叉树的顺序存储方式存储在一个一维数组中,并满足:Ki <= K2i+1 且 Ki<=K2i+2 ,则称为小堆(或大堆)。
将根节点最大的堆叫做最大堆或大根堆,根节点最小的堆叫做最小堆或小根堆。
堆中某个节点的值总是不大于或不小于其父节点的值;
堆总是一棵完全二叉树。
现在我们给出一个数组,逻辑上看做一颗完全二叉树。我们通过从根节点开始的向下调整算法可以把它调整成一个小堆。向下调整算法有一个前提: 左右子树必须是一个堆,才能调整。 \color{#FF0000}{左右子树必须是一个堆,才能调整。} 左右子树必须是一个堆,才能调整。
int array[] = {27,15,19,18,28,34,65,49,25,37};
下面是图示过程:
主要思想:
1.让parent标记需要调整的结点(图中27),child标记parent的孩子中值最小的那个结点(图中15)。
2.比较parent结点和child结点的值,若parent小于child,让parent和child交换对应结点的值,不小于直接结束。
3.parent标记结点的新位置(即child处),child再次标记parent的孩子中值最小的那个结点(如果孩子存在的话),以此类推。
具体代码:
void AdjustDown(HPDataType* a, int n, int parent)
{
int child = parent * 2 + 1;
while (child < n)
{
if (child + 1 < n && a[child] > a[child + 1])
{
child++;
}
if (a[child] < a[parent])
{
swap(&a[parent], &a[child]);
parent = child;
child = parent * 2 + 1;
}
else
break;
}
}
主要思想:
1.先用child标记我们要向上调整的结点,然后用parent找到child的父节点
2.比较child结点和parent结点的值,若小于parent则交换两结点的值,不小于直接结束
3.child标记结点的新位置(即parent处),parent标记child的父节点(若存在)以此类推。
图示:
我们再看代码:
void AdjustUp(HPDataType* a,int child)
{
int parent = (child - 1) / 2;
//while(parent >= 0) 不好
while (child > 0)
{
if (a[child] < a[parent])
{
swap(&a[child], &a[parent]);
child = parent;
parent = (child - 1) / 2;
}
else
break;
}
}
当child等于0时,parent = (child - 1)/2,parent依旧等于0,因此循环条件while(parent >= 0) 不好
主要思想:
从第二个结点开始,依次进行堆的向上调整,直到最后一个结点。
图示:
代码:
void HeapCreate(HP* php, HPDataType* a, int n)
{
assert(php);
HPDataType* tmp = (HPDataType*)malloc(sizeof(HPDataType) * n);
if (tmp == NULL)
{
perror("malloc fail");
exit(-1);
}
php->a = tmp;
memcpy(php->a, a, sizeof(HPDataType) * n);
php->size = php->capacity = n;
//向上调整建堆
for (int i = 1; i < n; i++)
{
AdjustUp(php->a, i);
}
}
主要思想:
从下到上,依次找到各个子树的父节点,将各个子树调整为小堆
图示:
代码:
void HeapCreate(HP* php, HPDataType* a, int n)
{
assert(php);
HPDataType* tmp = (HPDataType*)malloc(sizeof(HPDataType) * n);
if (tmp == NULL)
{
perror("malloc fail");
exit(-1);
}
php->a = tmp;
memcpy(php->a, a, sizeof(HPDataType) * n);
php->size = php->capacity = n;
//向下调整建堆
for (int i = (n-1-1)/2; i >= 0; i--)
{
AdjustDown(php->a, n, i);
}
}
因为堆是完全二叉树,而满二叉树也是完全二叉树,此处为了简化使用满二叉树来证明(时间复杂度本来看的就是近似值,多几个节点不影响最终结果):
下面是向下建堆的时间复杂度计算:
因此:向下调整建堆的时间复杂度为O(N)。
下面是向上建堆的时间复杂度计算:
此时间复杂度明显大于向下调整建堆。
因此: 向下调整建堆的时间复杂度更优,为 O ( N ) 。 \color{#FF0000}{向下调整建堆的时间复杂度更优,为O(N)。} 向下调整建堆的时间复杂度更优,为O(N)。
堆排序即利用堆的思想来进行排序,总共分为两个步骤:
1. 建堆:
升序:建大堆
降序:建小堆
2. 利用堆删除思想来进行排序
建堆和堆删除中都用到了向下调整,因此掌握了向下调整,就可以完成堆排序。
以升序为例:
1.将第一个结点(所有元素中值最大的)与最后一个结点交换值,再对第一个结点进行向下调整,这样最大的数就到了最后面,我们只需要排前n-1个数。
2.再将第一个结点(次大的)与倒数第二个结点交换值,对第一个结点进行向下调整,以此类推。
void HeapSort(HP* php)
{
int end = php->size - 1;
//升序建大堆
while (end)
{
swap(&php->a[end], &php->a[0]);
AdjustDown(php->a, end, 0);
end--;
}
}
TOP-K问题:即求数据结合中前K个最大的元素或者最小的元素,一般情况下数据量都比较大
比如:专业前10名、世界500强、富豪榜、游戏中前100的活跃玩家等。
对于Top-K问题,能想到的最简单直接的方式就是排序,但是:如果数据量非常大,排序就不太可取了(可能数据都不能一下子全部加载到内存中)。最佳的方式就是用堆来解决,基本思路如下:
1. 用数据集合中前K个元素来建堆
前k个最大的元素,则建小堆
前k个最小的元素,则建大堆
代码:
//topk问题(数多,且pop少)
void topk()
{
int minheap[5];
FILE* f = fopen("Data.txt", "r");
if (f == NULL)
{
perror("fopen fail");
exit(-1);
}
int k = 5;
for (int i = 0; i < k; i++)
{
fscanf(f, "%d", &minheap[i]);
}
//建小堆
for (int i = (k-1-1)/2; i >=0; i--)
{
AdjustDown(minheap, k, i);
}
//从文件中取
int val = 0;
while (fscanf(f, "%d", &val) != EOF)
{
if (val > minheap[0])
{
minheap[0] = val;
AdjustDown(minheap, k, 0);
}
}
//打印小堆
for (int i = 0; i < k; i++)
{
printf("%d \n", minheap[i]);
}
}
typedef int HPDataType;
typedef struct Heap
{
HPDataType* a;
int size;
int capacity;
}HP;
void HeapInit(HP* php)
{
assert(php);
php->a = NULL;
php->size = 0;
php->capacity = 0;
}
void HeapDestroy(HP* php)
{
assert(php);
free(php->a);
php->size = php->capacity = 0;
}
主要思想:
在数组最后插入一个数据,在对这个数据进行向上调整。
图示:
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, sizeof(HPDataType) * newcapacity);
if (tmp == NULL)
{
perror("realloc error");
exit(-1);
}
php->a = tmp;
php->capacity = newcapacity;
}
php->a[php->size] = x;
php->size++;
AdjustUp(php->a,php->size-1);
}
void HeapPop(HP* php)
{
assert(php);
assert(php->size > 0);
swap(&php->a[0], &php->a[php->size - 1]);
php->size--;
AdjustDown(php->a,php->size,0);
}
HPDataType HeapTop(HP* php)
{
assert(php);
assert(php->size > 0);
return php->a[0];
}