一.回顾
对此二叉树进行顺序存储(按层序遍历存),图中结点的序号表示数组中存放的位置
i为当前结点序号(从1开始,同数组下标),n表示总的元素个数,当前n=12
其中1-6是叶子结点,因此非叶子结点可表示为1~⌊n/2⌋,叶子结点可表示为⌊n/2⌋+1~n
对于某个结点i
①i的左孩子的序号应为2i
②右孩子应为2i+1
③父节点为⌊i/2⌋
④所在层次为⌈ l o g 2 ( i + 1 ) log_2{(i+1)} log2(i+1)⌉或⌊ l o g 2 i log_2i log2i⌋+1
⑤如果2i≤n,表示当前结点i有左孩子
⑥2i+1≤n表示有右孩子
⑦如果当前结点i>⌊n/2⌋,说明当前结点是叶子结点
⑧如果当前结点i≤⌊n/2⌋,说明当前结点是分支节点(非叶子)
二.概念引入:堆
解释:对于一个分支节点i,他的值≥左右孩子的值,称为大根堆。即 根≥左右
解释:对于一个分支节点i,他的值≤任意左右孩子的值,称为小根堆。即 跟≤左右
如图是一个大根堆的存储视角(左)和逻辑视角(右)
从最后一个分支节点开始检查
09→78→17→53
从09开始,如果其值大于左右孩子,则保存不变,继续看上一个分支节点。否则,找到左右孩子中最大的,与之交换
此处09和32交换
检查78,和87换
检查17,和45换
检查53,和87换
完成。刚刚降下来的53不满足大根堆,于是53和78换
注:在代码执行时,需要先判断(如果插入)是否满足大根堆,如果满足再插入53。如果不满足,将大的孩子拿上去,再重复对比其孩子
至此完成了大根堆的建立
代码实现
void BuildMaxHeap(int a[],int len){
for (int i = len / 2; i > 0; i--)//从最后一个非叶子结点开始构建大根堆
HeadAdjust(a, i, len);
}
void HeadAdjust(int a[], int k, int len)//建立大根堆
{
a[0] = a[k];//0作为哨兵,暂存根节点的值
for (int i = 2 * k; i <= len; i=i*2)//len表示数组长度(从1起),即元素个数
{
if (i<len&&a[i] < a[i + 1])
i++;//i指向左右孩子较大的一个,i
if (a[0] >= a[i])
break;
else {
a[k] = a[i];//大的孩子拿上去,当前位置可视为空缺
k = i;//k指向当前空缺位置。能否将a[0]放入,需要再判断待插入元素和其孩子的关系,看是否满足大根堆,因此继续for循环。详见下图(图1)
}
}
a[k] = a[0];//for循环结束,满足大根堆,a[0]放回来
}
四.基于大根堆的 从小到大的选择排序-堆排序
回顾选择排序:选择最大(小)的元素加入有序子序列
将堆顶的87与堆底元素9交换
此时已将最大的元素放到了正确的位置
当前在堆顶的09不满足大根堆,调用HeadAdjust(int a[], int k, int len)建立新的大根堆。注意此时的len变为了7(已经确定位置的元素87无需参与)
09按如上的方法下坠,至此完成了第一趟的处理
第一趟的结果:确定了最大元素的正确位置,将剩余元素恢复成了大根堆
后续的处理同上,经过n-1趟的处理完成了排序
同样小根堆实现的是递减的排序
代码实现(大根堆的堆排序)
#include
#include
using namespace std;
void HeadAdjust(int a[], int k, int len)//建立大根堆
{
a[0] = a[k];//0作为哨兵,暂存根节点的值
for (int i = 2 * k; i <= len; i=i*2)
{
if (i<len&&a[i] < a[i + 1])
i++;//i指向左右孩子较大的一个,i
if (a[0] >= a[i]) //和左右孩子中较大的比
break;
else {
a[k] = a[i];
k = i;//继续下坠
}
}
a[k] = a[0];
}
int main()
{
int a[6] = { 0,432,21,46,4322,645 };
int len = 5;
for (int i = len / 2; i > 0; i--)//从最后一个非叶子结点开始构建大根堆
HeadAdjust(a, i, len);
for (int i = len; i > 1; i--)//a[1]放到最后
{
swap(a[i], a[1]);
HeadAdjust(a, 1, i - 1);//剩余元素再构建
}
for (int i = 1; i <= 5; i++)
{
cout << a[i] << " ";
}
}
五.效率分析
1.时间复杂度
(1)建堆过程:
每次最多对比关键字两次:左右孩子比较,根和大的那个比较
如果只有左孩子,则只需要一次关键字的对比
如果树高为h,当前结点在第i层,则一共需要下坠h-i层,对比关键字不超过2(h-i)
而h=⌊ l o g 2 n log_2n log2n⌋+1
第i层最多有 2 i − 1 2^{i-1} 2i−1个结点,只有第1~h-1层才可能出现元素下坠
因此第一层有 2 1 − 1 2^{1-1} 21−1=1个结点,下坠共需要1×2(h-1)次关键字对比;第二层共有 2 2 − 1 2^{2-1} 22−1=2个结点,下坠共需要2×2(h-2)次关键字对比;第h-1层共有 2 h − 2 2^{h-2} 2h−2,下坠共需要 2 h − 2 2^{h-2} 2h−2×2次关键字对比,带入h,最终结果≤4n
因此建堆的过程,关键字对比次数不超过4n,建堆时间复杂度为O(n)
(2)排序过程
一共需要n-1趟,每趟都要交换两个元素,并进行堆顶元素的下坠,下坠最多h-1层,而每下坠一层最多对比关键字2次。
h=⌊ l o g 2 n log_2n log2n⌋+1,因此下坠的时间复杂度不会超过O( l o g 2 n log_2n log2n),一共n-1趟排序的时间复杂度O(n l o g 2 n log_2n log2n)
因此堆排序的时间复杂度=建堆+排序=O(n l o g 2 n log_2n log2n)
2.空间复杂度O(1)
3.稳定性
if (i<len&&a[i] < a[i + 1]) i++;//i指向左右孩子较大的一个,相等时保持左孩子不变
if (a[0] >= a[i]) break;