什么是堆?
(二叉)堆是一个数组,可以被看成一个近似的完全二叉树,如下图:图(a)是堆的存储结构,图中红色线表示逻辑结构中的父子关系指向;图(b)是堆的逻辑结构。

堆的分类
最大堆:除了根以外的所有结点(假设结点下标为i)都满足:Arrparent(i) >= Arri
最小堆:除了根以外的所有结点(假设结点下标为i)都满足:Arrparent(i) <= Arri
堆的性质
1.堆中某个节点的值总是不大于或不小于其父节点的值
2.堆总是一棵完全二叉树。
3.堆的高度:该节点到叶结点最长简单路径上边的数目
维护堆的性质代码及执行过程(AdjustDown)
适用场景,用于堆的插入或删除时维护堆的性质
最大堆:使得下标i为根结点的子树重新遵循最大堆的性质
假设:Arr.length:表示数组元素个数;Arr.heap-size:表示堆元素个数
如下图:Arr.heap-size当前=10,在执行MAX-HEAPAdjustDown(Arr,2)时,(a):下标为2的结点违背了最大堆的性质,(b):找到孩子中最大的孩子与下标为2的结点元素做交换,同时下标为4的结点也违背了最大堆的性质,(c):找到孩子中最大的结点与下标为4的结点元素做交换,最后在调用MAX-HEAPAdjustDown(Arr, 9),不再有数据交换就返回。

MAX-HEAPAdjustDown的时间复杂度为O(h),h为树的高度(该节点到叶结点的最长简单路径上边的数目)
最小堆反之
堆的创建
我们采用自底向上的方法利用MAX-HEAPAdjustDown把一个大小为n=Arr.length的数组转换为最大堆。我们从(二叉)堆最后一个非叶结点往前遍历调用MAX-HEAPAdjustDown算法建堆。

时间复杂度为O(n)
注意:必须采用自底向上的方法,因为我们先处理下层子树,保证子树已经是最大堆,上层调用堆化才有效。
堆排序算法
什么是堆排序算法?
就是将一个已经满足最大堆或者最小堆的数组排序,让数组最后的结果按从高到低或者从高到低排序。我们以最大堆为例,将数组中的元素排序。如下图:由于数组第一个元素是最大的,我们将其与最后一个元素交换位置,使得最大元素到达她正确的位置,堆的大小减一,然后调用MAX-HEAPAdjustDown算法使得由堆尾交换来的结点向下调整来维持最大堆的性质,持续这样做直到堆的大小为1时结束,此时数组如图(g)所示是有序数组

时间复杂度为O(nlgn)
升序要不要建小堆? 答案是**建大堆**。理由是:小堆的堆顶最小,应该待在结果的第一位,可我们后面要把它跟末尾元素交换------最小的元素会被扔到最后面去,那就成降序了。
参见后面的向下调整算法,堆排序就是"先垫出来一个大堆,然后把堆顶一次次扔到数组尾部":
cpp
void HeapSort(int* a, int n)
{
// 1. 建大堆:从最后一个非叶节点倒着向下调整
for (int i = (n - 1 - 1) / 2; i >= 0; --i)
AdjustDown(a, n, i);
// 2. 交换堆顶与末尾,缩小范围,再向下调整
for (int end = n - 1; end > 0; --end)
{
Swap(&a[0], &a[end]);
AdjustDown(a, end, 0); // 注意第二个参数是 end,即"当前堆的元素个数"
}
}
复杂度:建堆 O(N) + n 次删除 O(N·logN) = O(N·logN),空间 O(1)。
最大堆的模拟实现
前文为了便于理解,数组下标是从1开始的,现在我们使用数组下标为0开始模拟堆的实现
堆的结构体
cpp
typedef int HeapDataType;//堆中存储的数据类型
//实现小堆
typedef struct Heap
{
HeapDataType* _a;//适用动态开辟空间
int _size;//记录数组元素个数
int _capacity;//记录数组还能用于存储数据的空间大小
}Heap;
为什么堆不用链表?因为堆要求"完全二叉树",而完全二叉树的节点编号是连续的,用数组存不会浪费任何一格,还能 O(1) 随机访问任意节点的父/子。链表做不到这一点,也没必要做到。
核心调整算法
注意:在调整之前,堆已经是最大堆了
cpp
//向下调整,用于最大堆在删除堆顶元素后或建堆维持堆的性质使用
//向下调整最多走 树的高度 = logN 步
void AdjustDown(HeapDataType* a, int n, int root)//参数:a表示堆的地址,n表示堆的大小,root表示根的下标
{
//首先找到孩子中最大的值与父亲做比较,出现情况:比父亲大,交换;反之,已经满足了堆的性质结束
int parent = root;
int child = parent * 2 + 1;
while (child < n)
{
if (child + 1 < n && a[child + 1] > a[child])//child + 1 < n判断左孩子算法存在,否则会越界
{
++child;//左孩子比右孩子大
}
if (a[child] > a[parent])
{
Swap(&a[child], &a[parent]);
parent = child;
child = parent * 2 + 1;
}
else
{
break;
}
}
}
//向上调整,用于最大堆在堆尾插入新元素后,调整堆
///向上调整最多走 树的高度 = logN 步
void AdjustUp(HeapDataType* a, int child)//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;
}
}
}
|------|---------|-----------|
| | 比较对象 | 使用场景 |
| 向下调整 | 和更大的孩子比 | 建堆,删除堆顶元素 |
| 向上调整 | 只和父亲比 | 插入元素 |
其他算法
cpp
//交换元素代码
void Swap(HeapDataType* p1, HeapDataType* p2)
{
//交换
HeapDataType tmp = *p1;
* p1 = *p2;
*p2 = tmp;
}
//HeapInit:把一棵乱序二叉树"整理"成堆
void HeapInit(Heap* php, const HeapDataType* a, int n)//参数:php表示堆的地址,a表示数组地址,n表示数组大小
{
assert(php);//判空
assert(n >= 0);
//为堆开辟空间
php->_a = (HeapDataType*)malloc(sizeof(HeapDataType) * n);
if (php->_a == NULL)
{
perror("malloc fail");//开辟空间失败
return;
}
memcpy(php->_a, a, sizeof(HeapDataType) * n);//复制数组
php->_size = n;
php->_capacity = n;
//建堆:从最后一个非叶子结点倒着往前调,掉用向下调整算法
for (int i = (n - 1 - 1) / 2; i >= 0; --i)
{
AdjustDown(php->_a, php->_size, i);
}
//注意 i >= 0 不能写成 i > 0,否则根节点漏掉;也不能顺手写成 unsigned,i 减到 -1 时会翻成一个巨大的正数,直接死循环。
}
//向堆中插入元素,尾插
void HeapPush(Heap* php, HeapDataType x)
{
assert(php);
if (php->_size == php->_capacity) // 满了就扩容
{
int newCapacity = php->_capacity == 0 ? 4 : php->_capacity * 2;
HeapDataType* tmp = (HeapDataType*)realloc(php->_a, sizeof(HeapDataType) * newCapacity);
if (tmp == NULL)
{
perror("realloc fail");
return; // realloc 失败时原内存仍在
}
php->_a = tmp;
php->_capacity = newCapacity;
}
php->_a[php->_size] = x; // 尾插,保证物理上仍是完全二叉树
php->_size++;
AdjustUp(php->_a, php->_size - 1);//数组是从0开始的所有要减1
}
//删除删除堆顶元素
void HeapPop(Heap* php)
{
assert(php);
assert(php->_size > 0);
Swap(&php->_a[0], &php->_a[php->_size - 1]);
php->_size--;
AdjustDown(php->_a, php->_size, 0);
}
HeapDataType HeapTop(Heap* php)
{
assert(php);
assert(php->_size > 0);
return php->_a[0];
}
int HeapSize(Heap* php) { assert(php); return php->_size; }
bool HeapEmpty(Heap* php) { assert(php); return php->_size == 0; }
void HeapDestory(Heap* php)
{
assert(php);
free(php->_a);
php->_a = NULL;
php->_capacity = php->_size = 0;
}
模拟实现路径:堆的模拟实现(堆的模拟实现 · 28bd342 · Sylvia/C_code - Gitee.com)
实验结果:
