什么是堆?
按照完全二叉树的层序遍历顺序存储的元素集合,当每一个根结点大于左右孩子结点时为大堆,小于左右孩子结点时为小堆
小堆:
大堆:
以下举例以大根堆为例
堆的向下调整
调整前提:逻辑上表示的完全二叉树,其左右子树已经满足堆的性质
-
先判断需要调整的结点有无左右孩子,由于是完全二叉树,如果不存在左孩子,右孩子一定不存在。假设需要调整的结点存放在数组中的下标为root,则可以根据二叉树的特点计算出左右孩子存储在数组中的下标
- 左孩子下标:root * 2 + 1
- 右孩子下标:root * 2 + 2
-
左右孩子是否存在的问题可以转换为,左右孩子的数组下标是否越界,一旦越界表明孩子不存在
-
如果左右孩子都不存在,说明为叶子结点,调整结束;当左右孩子都存在时,找出左右孩子里面的较大者与根进行比较,若较大者比根大,进行数组元素交换;若只有左孩子,将左孩子与根比较,若比根大做交换
void Swap(int *a, int *b)
{
int tmp = *a;
*a = *b;
*b = tmp;
}
void AdjuestDown(int array[], int size, int root)
{
int left, right, max, i;
while (1)
{
left = root * 2 + 1;
right = root * 2 + 2;
if (left >= size)
{
break;
}
if (right < size && array[right] > array[left])
{
max = right;
}
else
{
max = left;
}
if (array[root] < array[max])
{
Swap(array + root, array + max);
root = max;
}
else
{
break;
}
}
}
堆的向上调整
根据该结点找到该结点的双亲,假设该结点下标为child,则其双亲结点下标为(child - 1) / 2,比较二者大小,若该结点大交换两者元素,若该结点满足大堆的性质,调整结束;交换过后则继续比较交换后的结点与其双亲,直到根结点调整结束
void AdjustUp(int array[], int child)
{
int parent = 0;
while (child >= 0)
{
parent = (child - 1) / 2;
if (array[parent] < array[child])
{
Swap(array+parent, array+child);
}
else
{
break;
}
child = parent;
}
}
堆的创建
借助向下调整,为了满足向下调整的前提条件,从最后一个非叶子节点开始向下调整,一直调整到根结点结束
而最后一个非叶子结点的下标可以通过数组元素个数计算的到,size-1为最后一个元素的下标,根据左/右孩子找到他的双亲即为最后一个非叶子结点,下标为:(size - 2)/ 2
void BuildHeap(Heap *h)
{
int i = 0;
int start = (h->size - 2) / 2;
for (i=start; i>=0; i--)
{
AdjuestDown(h->array, h->size, i);
}
}
堆的插入
将需要插入的元素插入到堆的尾部,即数组的最后一个元素后面,然后对该元素做一次向上调整即可
void PushBack(Heap *h, int d)
{
assert(h);
h->array[h->size] = d;
h->size++;
}
void HeapPush(Heap *h, int data)
{
PushBack(h, data);
AdjustUp(h->array, h->size - 1);
}
弹出堆顶元素
让堆尾的元素覆盖掉堆顶的元素,堆的个数减一后,将堆顶的元素做一次向下调整即可
void HeapPop(Heap *h)
{
h->array[0] = h->array[h->size - 1];
h->size--;
AdjuestDown(h->array, h->size, 0);
}
堆的应用-海量数据TopK问题
首先将海量数据的前K个建立成一个小堆,然后依次从K+1个开始用剩下的数据与创建的堆顶元素作比较,如果比堆顶元素大,就直接替换掉堆顶元素,然后堆顶元素做一次向下调整,循环比较剩下的所有元素后,堆中所保存的元素即为海量数据索要查找的TopK
void AdjuestDownSmall(int array[], int size, int root)
{
int left, right, min, i;
while (1)
{
left = root * 2 + 1;
right = root * 2 + 2;
if (left >= size)
{
break;
}
if (right < size && array[right] < array[left])
{
min = right;
}
else
{
min = left;
}
if (array[root] > array[min])
{
Swap(array + root, array + min);
root = min;
}
else
{
break;
}
}
}
void BuildHeapSmall(Heap *h)
{
int i = 0;
int start = (h->size - 2) / 2;
for (i=start; i>=0; i--)
{
AdjuestDownSmall(h->array, h->size, i);
}
}
void TopK(int array[], int k, int size)
{
int i = 0;
Heap heap;
HeapInit(&heap);
for (i=0; i<k; i++)
{
PushBack(&heap, array[i]);
}
BuildHeapSmall(&heap);
for (i=k; i<size; i++)
{
if (array[i] > HeapTop(&heap))
{
heap.array[0] = array[i];
AdjuestDownSmall(heap.array, heap.size, 0);
}
}
for (i=0; i<k; i++)
{
printf("%d ", heap.array[i]);
}
printf("\n");
}
在普通数组多次找最值,时间复杂度一直都保持在O(n),但是在堆里找最值,第一次只需弹出堆顶元素即可,其后将剩余元素继续调整成堆,仅需要做一次向下调整即可,时间复杂度为O(logn),这也就是堆真正存在的原因