文章目录
一、堆的结构及实现(重要)
1.1 二叉树的顺序结构
普通的二叉树是不适合用数组来存储的,因为可能会存在大量的空间浪费。而完全二叉树更适合使用顺序结构存储。在现实中我们通常把堆 (一种完全二叉树) 使用顺序结构的数组来存储,需要注意的是这里的堆和操作系统虚拟进程地址空间中的堆是两回事,一个是数据结构,一个是操作系统中管理内存的一块区域分段。
1.2 堆的概念及结构
堆(Heap)是计算机科学中一类特殊的数据结构的统称。堆通常是一个可以被看做一棵完全二叉树的数组对象。堆总是满足下列性质:
- 堆中某个结点的值总是不大于或不小于其父结点的值;
- 堆总是一棵完全二叉树。
堆是非线性数据结构,相当于一维数组,有两个直接后继。
【大根堆和小根堆】:
根结点最大的堆叫做大根堆,树中所有父亲都大于或等于孩子。
根结点最小的堆叫做小根堆,树中所有父亲都小于或等于孩子。
【思考】这个大根堆和小根堆有什么特点呢?
最值总在 0 号位,根据这个特点我们就可以做很多事情,比如TopK问题 (在一堆数据里面找到前 K 个最大 / 最小的数),生活中也有很多实例,比如点餐软件中有上千家店铺,我想选出该地区好评最多的十家川菜店,我们不用对所有数据排序,只需要取出前 K 个最大 / 最小数据。使用堆排序效率也更高。
1.3 堆的实现
1.3.1 堆的向下调整算法
下面给出一个数组,逻辑上看做一颗完全二叉树。我们通过从根节点开始的向下调整算法可以把它调整成一个小堆。向下调整算法有一个前提:该节点的左右子树必须是一个 (大 / 小) 堆,才能调整。
int array[] = {
27,15,19,18,28,34,65,49,25,37 }; // 根节点的左右子树都是小堆
上面的数组,因为根节点的左右子树都是小堆,所以我们从根节点开始调整,将其调成小堆。
向下调整算法思路(调成小堆):
- 从根节点开始,不断往下调。
- 选出根节点的左右孩子中「最小的孩子」,与「父亲」进行比较。
- (1) 如果父亲小于孩子,就不需处理了,整个树已经是小堆了。
- (2) 如果父亲大于孩子,就跟父亲交换位置,并将原来小的孩子的位置当成父亲继续向下进行调整,直到调整到叶子结点为止。
向下调整算法过程演示(调成小堆,把大的节点往下调整):
向下调整算法代码:
// 向下调整算法,建小堆,把大的节点往下调整
// 前提是:左右子树都是小堆
void AdjustDown(int* a, int size, int parent)
{
// 指向左孩子,默认左孩子最小
int child = parent * 2 + 1;
while (child < size)
{
// 1. 选出左右孩子最小的那个,先判断右孩子是否存在
if (child + 1 < size && a[child] > a[child + 1])
{
child++; // 指向右孩子
}
// 2. 最小的孩子与父亲比较
if (a[parent] > a[child]) // 如果父亲大于孩子
{
// 父亲与孩子交换位置
Swap(&a[parent], &a[child]);
// 更新父子下标,原先最小的孩子作为父亲,继续往下调
parent = child;
child = parent * 2 + 1;
}
else // 如果父亲小于孩子,说明已经为小堆了,停止调整
{
break;
}
}
}
1.3.2 向下调整算法的时间复杂度
我们以满二叉树计算,最坏情况下,向下调整算法最多进行满二叉树的高度减1次比较,则说明向下调整算法最多调整满二叉树的高度减1次,n 个节点的满二叉树高度为 log2(n+1),估算后所以时间复杂度为 O(log2n)。
1.3.3 堆的创建(向下调整)
下面给出一个数组,这个数组逻辑上可以看做一颗完全二叉树,但不是一个堆,我们需要通过算法把它构建成一个堆。如果根节点左右子树不是一个 (大 / 小) 堆,我们应该怎么调整呢?
我们倒着调整,从下到上,从「倒数第一个非叶子节点的子树」开始,依次遍历完所有非叶子节点,分别对每个子树进行「向下调整」成 (大 / 小) 堆,一直调整到「根节点」,就可以建成一个 (大 / 小) 堆。
为什么要倒着调整呢?因为这样我们可以把「倒数第一个非叶子节点的子树」的左右子树看成是一个 (大 / 小) 堆,此时才能去使用向下调整算法。比如下图中的黄色填充的子树,3 的左子树 6 就可以看成是一个大堆。
【实例】:将下面的数组建成一个大堆
int a[] = {
1,5,3,8,7,6 };
建堆过程演示(以建大堆为例):从下到上,依次遍历完所有非叶子节点,分别对每个子树进行向下调整。
依次对 每一步 中,方框内的树 进行 向下调整 为一个 大堆。
![]()
建堆代码:
// 交换函数
void Swap(int* a, int* b)
{
int tmp = *a;
*a = *b;
*b = tmp;
}
// 向下调整算法,建大堆,把小的节点往下调
// 前提是:左右子树都是大堆
void AdjustDown(int* a, int size, int parent)
{
// 指向左孩子,默认左孩子最大
int child = parent * 2 + 1;
while (child < size)
{
// 1. 选出左右孩子最大的那个,先判断右孩子是否存在
if (child + 1 < size && a[child]