目录
一、堆的概念和结构
堆的概念:堆是一种特殊的数据结构,是一种完全二叉树。满足一下性质:
1. 父节点的值总是大于等于(或小于等于)它的子节点的值,称为大根堆或小根堆。
2. 堆中的最大元素(或最小元素)总是在根节点上(根据堆的种类不同)。
堆的结构图:
二、小根堆的实现
创建一个顺序表用来存储堆。由于堆是完全二叉树,用数组可以依次将堆上的所有数据进行保存,并不会出现错位指向的情况。二叉树在逻辑上是一种链式结构,虽然数组在物理上是线性结构,但存储的数据可以通过下标对堆的父子结点进行访问。
typedef int HPDataType;
typedef struct Heap
{
HPDataType* a;
int capacity;
int size;
}HP;
-
初始化
void HeapInit(HP* php)
{
assert(php);
php->a = NULL;
php->capacity = php->size = 0;
}
-
交换两个数据
void Swap(HPDataType* pchild, HPDataType* pparent)
{
assert(pchild && pparent);
HPDataType tmp = *pchild;
*pchild = *pparent;
*pparent = tmp;
}
-
向上排序算法
根据堆的性质,新插入的数据作为叶子结点,如果小于它的父亲结点,将二者进行交换。依次向上遍历,直到根节点。
void AdjustUp(HPDataType* a, int child)
{
assert(a);
int parent = (child - 1) / 2;
while (child)
{
if (a[child] < a[parent])
{
Swap(&a[child], &a[parent]);
child = parent;
parent = (child - 1) / 2;
}
else
{
break;
}
}
}
-
向下排序算法
将根节点和数组最后一个元素交换位置,也就是将堆从根节点处断开,根结点的两端依旧是两个小根堆。数组的最后一个元素将变成原来的根节点的数据,数组的元素个数减1,也就实现了根结点的删除。但新的数组并不满足堆的性质,为了保持堆的性质,找到根结点,将根节点和它的两个左右子节点进行比较,和最小的子节点进行交换。不断重复,直到叶子结点或父节点都小于其子节点
void AdjustDwon(HPDataType* a , int size, int parent)
{
int child = parent * 2 + 1;
while (child < size)
{
if (child + 1 < size && a[child + 1] < a[child])
{
child++;
}
if (a[child] < a[parent])
{
Swap(&a[child], &a[parent]);
parent = child;
child = parent * 2 + 1;
}
else
{
break;
}
}
}
-
插入
新插入数据会保存在数组的最后,新的数组将不会呈现出堆的特性。为了保证拆入数据之后,数组依然作为堆,在插入数据之后调用向上排序算法。
void HeapPush(HP* php, HPDataType x)
{
assert(php);
if (php->capacity == php->size)
{
int newcapacity = php->capacity == 0 ? 4 : 2 * php->capacity;
HPDataType* tmp = (HPDataType*)realloc(php->a, sizeof(HPDataType) * newcapacity);
if (tmp == NULL)
{
perror("realloc fail");
exit(-1);
}
php->a = tmp;
php->capacity = newcapacity;
}
php->a[php->size] = x;
AdjustUp(php->a, php->size);
php->size++;
}
-
删除
删除堆上数据时,如果直接将根节点(数组首元素)删除,在重新创建堆,效率会很低,为了保证更高效,调用向下排序算法。
void HeapPop(HP* php)
{
assert(php && php->a);
assert(!HeapEmity(php));
Swap(&(php->a[0]), &(php->a[php->size - 1]));
php->size--;
AdjustDwon(php->a, php->size, 0);
}
-
判断堆是否为空
bool HeapEmity(HP* php)
{
assert(php);
return php->size == 0;
}
-
返回根节点的数据
HPDataType HeapTop(HP* php)
{
assert(php);
assert(!HeapEmity(php));
return php->a[0];
}
-
打印
void HeapPrint(HP* php)
{
assert(php && php->a);
for (int i = 0; i < php->size; i++)
{
printf("%d ", php->a[i]);
}
printf("\n");
}
-
销毁
void HeapDestory(HP* php)
{
assert(php);
free(php->a);
php->capacity = php->size = 0;
}
三、建堆的时间复杂度
将一个无序数组变为堆的形式的时候,我们既可以使用向上排序算法也可以使用向下排序算法来实现。使用向上排序算法的时候,最坏的情况下没次进入的数据都需要额外建堆,那么时间复杂度为O(N*logN)。而使用向下排序算法时,由于向下排序算法的特殊性,要保证每次进入数据之后,都可以将原来的堆从根结点处分为两个堆,不用在新建堆。时间复杂度为O(N)。
四、用堆解决经典Topk问题
Topk问题:从给定的N个数据中找到前K个数。
解法1:排序。以笔者目前掌握的排序方法,最快的排序方法为堆排序。时间复杂度为O(N*logN)
解法2:建堆,在弹出前K个根节点。时间复杂度为O(N + K * logN)
解法3:建数量为k的小大堆。以求前k最大值为例:建小堆,根为堆中最小的数,将N-K依次和根比较,大于替换重建堆,能保证堆中的数据永远是前k。时间复杂度为O(K + (N - K) * logK)
解法2在数据量小的时候时间复杂度时优于解法3的,但可能存在数据量过大,内存没有那么大的空间。解法3在保证效率的同时,兼顾了最小的空间复杂度。
代码实现
void PrintTopK(int* a, int n, int k)
{
int* KMinHeap = (int*)malloc(sizeof(int) * k);
assert(KMinHeap);
//建堆
for (int i = 0; i < k; i++)
{
KMinHeap[i] = a[i];
}
for (int i = (k - 2) / 2; i >= 0; i--)
{
AdjustDwon(KMinHeap, k, i);
}
//将N-K个数和堆根比较
for (int i = k; i < n; i++)
{
if (KMinHeap[0] < a[i])
{
KMinHeap[0] = a[i];
AdjustDwon(KMinHeap, k, 0);
}
}
//弹出
for (int i = k - 1; i >= 0; i--)
{
Swap(&KMinHeap[0], &KMinHeap[i]);
AdjustDwon(KMinHeap, --k, 0);
printf("%d ", KMinHeap[k]);
}
}