堆的这部分内容比较多,我会分成几个部分进行讲解。所有的讲解基本上是 基于《算法导论》
堆(二叉堆)说白了其实就是个数组 , 它可以被近似的看作完全二叉树,树上的每一个节点对应数组中的每一个元素。
这里简单的介绍一下完全二叉树 , 完全二叉树是从左往右依次填充 , 这里举一个例子方便大家理解
像这样 , 其中第六个节点不是从左向右依次填充的 , 所以这棵树不是完全二叉树
如果改成这样,就是完全二叉树
二叉树就简单介绍到这里 , 这里可以画一个二叉树和堆的关系图 , 方便大家更好的理解堆
这是上述二叉树所创建的堆
这里介绍两种堆——最大堆 , 最小堆;
从定义上来看,最大堆就是除去根节点以外,所有的父亲节点大于他的孩子节点
自然 , 我们也不难想象出,最小堆的定义是:除去根节点,所有的父亲节点都小于他的孩子节点
代码表示—— 最大堆 A[parent] >= A[i]
最小堆 A[parent] <= A[i]
最小堆通常用来构造优先队列 , C++STL中的priority_queue的底层实现逻辑其实也就是最小堆
在算法中,当我们既无法适合最大堆或者是最小堆的时候,我们就会统称为堆。
堆实际上就是用数组来表示树,当有n个元素的堆时,我们可以把它看作一个高度为logn的完全二叉树。书上这里并没有详细解释堆为什么可以看作高度为O(logn)的完全二叉树 这里我来简单的说一下。
下标从0开始 , 就比如说3的父亲是下标为0的1 , 这里我们可以用公式(2 - 1)/ 2 = 0,求得3的父亲节点是下标为0的1.这样举个例子,大家应该会让大家理解起来更加轻松。
如果是一颗满二叉树,他的节点的个数为2^n - 1 。
推导过程如下,第一行是1个节点 ,第二行是2个节点 , 第三行是4个节点
分别为2^0 , 2^1 , 2^2 ,可是使用一点点高中知识——等比数列的求和公式 , 自然也就可以推出来一颗满二叉树的节点个数。自然一颗满二叉树的高度就为 当然,并不是所有的二叉树都是满二叉树,所以一颗二叉树的高度可以近似的看作O(logn)
这里其实还有两个性质需要我插上一嘴
当数组从0开始的时候
left child = parent * 2 + 1——左孩子的位置应当为他的父亲 * 2 + 1的,
right child = parent * 2 + 1 + 1——右孩子的位置应当为他的父亲 * 2 + 2的
根据计算机计算除法是以向下取整为基础 , parent = (child - 1) / 2 这里的孩子是不分左右的
当数组从1开始的时候
left child = parent * 2 right child = parent * 2 + 1
这里我们举个例子来方便大家理解
这里书中就开始将如何维护堆这种数据结构的性质了,这里我们放到后面去讲。
int main() {
int a[] = {4 , 7 , 5 , 6 , 9};
return 0;
}
这里我们先假设以上的a作为一个堆,他的树状结构应该是这样的
这既不是一个最大堆或是最小堆。那么我们应该如何把他变成大堆或者小堆,这里就是堆排序的第一个重点了。这里我们可以对整个堆执以下的操作
这里以最大堆为例子
我们从7 开始进行一个操作,他先和他的两个孩子进行比较 ,在左右孩子中找出较大的一个出来,如果两孩子中大的一个,如果两个孩子中大的那一个大于他的父亲,那么就进行交换。从现在的树来看,就是7应当与6 ,9中(9 >6)的9进行交换,,那么此时,树就会变成这样
此时7没有孩子了,所以此次交换结束。再从原来的7(现在的9)的前一个节点(现在的4)进行刚才的操作。
观察4的左右孩子,找出孩子中最大的(9),如果比4大,就进行交换
再在4中找他的两个孩子节点中最大的那一个(7),7 > 4所以需要进行交换,
这就是一颗已经调整好的树,也就是我们所想要的最大堆.不难看出,其中所有的孩子都是要小于他的父亲,此时存储堆的数组也会变成这样
刚才的操作大家可能有一些问题,这里为什么不从最后一个叶子节点(数组的末端)进行刚才的操作
这样的操作我们可以刻把他简称为down操作
这里我稍作解释,这里我们需要不停的将数组中的数字向下进行调整,因为作为叶子节点,他是没有孩子的,所以我们不需要对叶子节点进行以上的操作。
这算是出了个难题了,我们应当如何找到最后一个父亲节点呢。
这里我们需要用到刚才我们所谈到的公式parent = (child - 1) / 2。这里我们可以找到最后一个节点的父亲节点,这也就是最后一个的父亲节点。这里依然用以上的例子
9的下标为4,那么他父亲的下标就是(4 - 1) / 2,也就是1。在数组中下标为1的元素值就是7,所以我们应当对7和他之前的节点进行down操作。
知道了思路,现在我们就来开始coding吧
代码如下
void AdjustDown(int* a, int n, int root) {
int parent = root;
int child = parent * 2 + 1;//默认为左孩子
while (child <n) {
//选出左右孩子大的那一个
if (child + 1 < n && a[child + 1] > a[child]) {
child++;
}
if (a[child] > a[parent]) {
swap(a[child], a[parent]);
parent = child;
child = parent * 2 + 1;
}
else {
break;
}
}
}
当不能再进行以上的操作的时候,立马停止本次的down操作
for (int i = (n - 1 - 1) / 2; i >= 0; i--) {
AdjustDown(a, n, i);
}
堆排序,堆排序,那么现在知道了这样我们就需要进行排序操作了。
假设我们要进行从小到大的依次排序,这里我们是需要选择最大堆还是最小堆呢?
,并且当我们在排序的时候又应当如何进行某些操作以保证最大堆/最小堆的性质呢?
一篇文章太长也容易让读者丧失之前的兴趣,所以留下两个问腿提供给大家,希望大家在之后进行思考。