堆是一个特殊的二叉树,所以学习堆排序之前,必须先了解一些关于树的基础知识:
一、堆排序基础知识(树)
树是一种数据结构,就像一棵倒置的树。计算机中的文件目录就类似于树结构,一个文件夹里面可以有多个文件夹,每个文件夹分下去又可以有多个文件夹。
树的根:上图的A就是树的根节点,每棵树只有一个根节点;
树的节点:图中的A到Q都是树的节点;
树的叶子节点:每个分支延伸到末端的都是树的叶子节点,例如图中的B、C、H、I、K、L、M、N、P、Q
树的深度(高度):从根节点到叶子节点最长的路径,该路径上节点的总数就是树的深度,例如上图,最长的路径是A——E——J——P或者Q,那么这个树的深度或者高度就是4。
孩子节点和父节点:孩子节点和父节点是相对的。A可以称为E的父节点,E是A的子节点。
节点的度:一个节点有多少个子节点,就是它的度,例如图中的F,有3个子节点,就称F节点的度是3。
树的度:树的度不是看树的根结点分了多少叉路,而是看整棵树里面分最多叉的结点的度,当然这里根结点分了6个岔路,所以它的度也是6
二叉树:度不超过2的树,也就是说每个节点的子节点最多只有2;
满二叉树:一个二叉树,如果每一层的节点数都达到最大值,则这个二叉树就是满二叉树;
完全二叉树:一个二叉树,叶子节点只能出现在最下层和次下层,并且最下面一层的节点都集中在该层最左边
树的存储方式:
1、链式存储方式
2、顺序存储方式:下面会用到这种方式,也就是用个列表来存储树结构。
列表存储树结构:
如果有一棵完全二叉树如下,我们可以怎么用一个列表把这些节点上的数字存起来?
注意,这里很重要:
我们可以按照从根结点开始,逐层往下,并且从左往右把每个结点存放在一个列表中,这样一个列表看起来有什么规律呢?
列表下面的数字是每个数字在列表中的位置。
我们可以把每个父节点和子节点的位置找出来:
9到它的子节点8和7的位置:0 ——> 1, 2
8到它的子节点6和5的位置:1 ——> 3, 4
7到它的子节点0和1的位置:2 ——> 5, 6
6到它的子节点2和4的位置:3 ——> 7, 8
5到它的子节点3的位置:4 ——> 9
可以总结2个规律:
A、当父节点的位置为 i 时,左孩子结点位置为:2 i + 1, 右结点的位置为:2 i + 2
B、当子节点的位置为 i 时,父节点的位置为: ( i - 1)//2
这2个规律很重要,我们在堆排序中,会用到。
二、什么是堆呢?
像下面这样的,首先是一棵完全二叉树,其次它是逐层递减的,根节点最大,往下的数字一层比一层小。这种叫做大根堆
根结点最大,逐层递减的,称为大根堆;
根结点最小,逐层递增的,称为小根堆;
接下来的堆排序中,我们使用大根堆来推演。因为大根堆排完序出来的列表是个升序的列表。
三、堆排序的整体逻辑是:
1、把一个完全二叉树构造成堆;
2、完成构造堆之后,堆顶就是最大的值了,把它先拎出来放到列表中;
3、接着把最后一个结点的值放到堆顶的位置,再通过一次向下调整,又可以推举出最大的值作为堆顶,并且把刚刚临时放置到堆顶的值找一个合适的位置给它放下,让这个二叉树还是满足堆的要求;
4、重复2和3的过程,最后出来的列表就是一个排好序的列表;
四、堆排序的2个核心问题:
1、怎么把一个完全二叉树构造成堆?
假设我们现在有一个无序的列表
[5, 9, 13, 14, 3, 10, 6, 7, 20, 8]
它可以看成一个不满足堆条件的完全二叉树,如下图:
我们可以先划分成多个子树,从最下层的子树不断调整,最后完成整棵树的调整,让它满足堆的条件。
如下可以分成5步去调整这个无序二叉树。
第1步:先找到最后一个子树,也就是以3为根节点的子树(3-8),拿根节点和孩子节点对比,大的往上移动,小的往下移,所以3和8交换位置;
第2步:接着往左找跟它同级别的子树做调整,也就是以14为根节点的这棵子树(14-7-20),先比较左右两个孩子节点7和20,找其中大的子节点与根节点对比,所以20和14交换位置;
调整后变成下面这样:
第3步:调整以13为根节点的子树(13-10-6),同样的逻辑判断后,13比2个子节点都大,所以不用调整;
第4步:调整以9为根节点的子树,从左右两个孩子节点找出大的和9对比,20比8大,就拿20跟9对比,20比9大,所以20上移,9往下移。9下移后,还要跟14和7对比,由于9小于14,所以9应该放到14的位置。
调整后变成下面这样:
第5步:调整整棵树,20比13大,所以5先跟20交换,5比14小,所以5又跟14交换,5又比9小,所以5又和9交换位置;
调整后变成下面这样:
由此我们完成了将一个无序的完全二叉树构成一个堆,对应的列表也就完成调整
调整前:[5, 9, 13, 14, 3, 10, 6, 7, 20, 8]
调整后:[20, 14, 13, 9, 8, 10, 6, 7, 5, 3]
由于上面完成了堆的构建,于是根节点 20 必定是最大值,如果我们能够把20先拿掉,接着继续调整这个树,又推选出一个最大值出来,按照这个逻辑不断推选出最大值,那么最后这些拿出来的最大值构成的列表就是有序的列表了。
问题是,拿掉最大值之后,怎么调整这棵树才能让它用最快的速度变成一个堆?
2、怎么完成一次向下调整?
这里我们需要了解堆的另一个特性。
当一个完全二叉树满足以下条件时,可以通过一次向下调整,让它变成一个堆:
完全二叉树除了根节点,下面的所有子树都满足堆的条件(孩子节点都小于父节点)
如上图的完全二叉树,除了根节点之外,根节点的左右两个子树,都满足堆的要求。
此时就只需要调整根节点,让它跟比2个孩子节点中比较大的交换位置,并且让它往下找到一个合适的位置放下,此时它就由一个二叉树都变成一个堆了。
调整步骤:
1、先找个临时变量tmp把根节点的值存起来(tmp=3)
2、8和7对比,8比较大,接着拿8和tmp对比,8胜出,于是8上移到根节点,tmp暂时先放在原来8的位置;
3、如果直接把3跟8交换,明显3比6小,不能作为6的父节点,所以我们要继续往下找,先找到6和5中比较大的值,跟tmp对比,此时6比3大,所以6继续上移。继续往下一层对比,3比4小,4也往上移,由于原来4的位置已经没有更小的节点了,所以tmp最后就落在原来4的位置上。
最终调整为:
由此完成了一次向下调整。
所以回到我们在1里面构造的堆,最后有一个问题:
拿掉最大值之后,怎么调整这棵树才能让它用最快的速度变成一个堆?
堆有一个向下调整性质:
当一个完全二叉树,除了根节点,左右两个子树都满足堆的性质时,可以通过一次调整根节点的位置,让整个二叉树变成堆。
怎么调整呢?
1、先找到这棵树的最后一个节点,暂时把它放在根节点的位置
上图变成:
2、接着就可以拿着这个根节点,不断往下对比,推选出更大的值上来,直到找到3 应该放的位置。
具体操作:看3的左右两个孩子结点6和7,由于7比较大,所以7上移到根节点的位置,3放到7的位置,接着3还得跟0和1对比,3比较大,所以它放在原来7的位置就行了。
(注意边界条件是:当3下移到某个位置的2个孩子节点都比它小或者是往下已经没有节点可以跟3比较了,此时可以停止比较,确定3的位置)
于是我们又得到一个最大值7,把它拿掉,继续循环把2放到根节点的位置,执行堆的向下调整,按这个步骤就可以一步步把堆的数按大到小一个个拿出来。
由此我们完成了堆排序的两个最核心的步骤:
1、构造堆
2、堆的一次向下调整
关于堆排序整体代码,等下篇继续分析