堆的理论基础
堆分为大根堆和小根堆。堆的结构是一颗完全二叉树。
大根堆是父结点大于孩子结点。小根堆是父结点小于孩子结点。
这就是一个小根堆
这是一个大根堆
堆的存储结构
堆可以使用一个数组存储,因为它的底层是一颗完全二叉树,并不会造成浪费空间。而孩子结点的下标和父结点下标,存在公式关联。
假如父结点下标为 i : 左孩子结点下标:i * 2 + 1, 右孩子结点下标: i * 2 + 2
假如孩子结点下标为i: 父节点下标:(i - 1) / 2
堆的存储结点结构
typedef struct Heap
{
int* _a;
int _size;
int _capacity;
}HP;
小根堆的实现
初始化
void HeapInit(HP* php)
{
assert(php);
php->_a = NULL;
php->_size = php->_capacity = 0;
}
向下调整
对小根堆向下调整,若想调整完之后是一个小根堆,前提是该树的子树已经是一个小根堆。这里的parent可以接受任意数组下标的任意位置。
假如需要调整的结点到最后一层的高度为n,那么只需要向下移动n - 1步,时间复杂度O(n - 1)
void AdjustDown(int* 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[child], &a[parent]);
parent = child;
child = parent * 2 + 1;
}
else {
break;
}
}
}
向上调整
对数组任意一个位置向上调整,接受的位置就是child。
假如要向上调整的结点处于第n层,那么需要向上走n - 1
void AdjustUp(int* a, int child)
{
int parent = (child - 1) / 2;
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, int x)
{
assert(php);
// 扩容操作
if (php->_size == php->_capacity)
{
int new_capacity = php->_capacity == 0 ? 4 : php->_capacity * 2;
int* temp = (int*)realloc(php->_a, new_capacity * sizeof(int));
if (temp == NULL)
{
perror("realloc fail");
exit(-1);
}
php->_a = temp;
php->_capacity = new_capacity;
}
php->_a[php->_size++] = x;
AdjustUp(php->_a, php->_size - 1);
}
堆删除
堆删除堆顶的元素,先将末尾的元素和堆顶元素交换 ,然后删除末尾元素, 最后向下调整
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);
}
向上调整和向上调整
向上调整和向下调整,都可以用来创建堆,但是两者时间复杂度却不一样,向下调整更优
在使用向上调整和向下调整建堆,是基于一个数组来创建。
下面则是使用向下调整建堆。
void AdjustDown(int* arr, int n, int parent)//向下调整
{
int child = parent * 2 + 1;//左孩子节点下标
while (child < n) //向下调整,child只会越来越大,当超出数组范围时,退出循环
{
if (child + 1 < n && arr[child] < arr[child + 1])//判断是否存在右孩子节点,存在左右孩子结点谁大,要是建小堆,取小的那个元素
child++;
if (arr[parent] > arr[child])//判断父亲结点和孩子结点那个大,若孩子结点大,则进行交换,并继续向下调整
{
swap(&arr[parent], &arr[child]);//两节点进行交换
parent = child;//孩子结点的下标赋给父亲结点
child = parent * 2 + 1;//孩子节点获得新的左孩子下标
}
else
break;
}
}
int CreatHeap(int* arr, int n)//arr为要建堆的数组
{
for (int i = (n - 1 - 1) / 2; i >= 0; i--)//循环的初始值为倒数第一个非叶子节点的下标
{
AdjustDown(arr, n, i);//向下调整
}
}
下面使用向上调整建堆
void AdjustUp(int* arr, int child)
{
int parent = (child - 1) / 2;//获得父亲节点的下标
while (child)
{
if (arr[parent] < arr[child])//孩子结点数据小于父亲结点数据,两数交换
{
swap(&arr[parent], &arr[child]);
child = parent;
parent = (child - 1) / 2;
}
else
break;
}
}
void CreatHeap(int* a, int n)
{
for (int i = 1; i < n; i++)
{
AdjustUp(a, i);
}
}
时间复杂度分析
设高度从1开始,n个结点。
使用向下调整建堆:
使用向上调整建堆:
根据向下调整可得 O(n * logn)
Top K 问题
例如在100 个数中求前k个最小的数。
求最小就建大堆,求最大就建小堆。