9.7.1 堆的定义与基本操作

堆是一棵完全二叉树,树中每个结点的值都不小于(或不大于)其左右孩子结点的值。
大顶堆:父节点值>=孩子结点的值    此时父节点的值为子树最大
小顶堆:父节点值<=孩子结点的值    此时父节点的值为子树最小
(堆一般用优先队列实现,而优先队列默认大顶堆,一下仅讨论大顶堆)

算法笔记 9.7 堆_堆排序

算法笔记 9.7 堆_数组_02

算法笔记 9.7 堆_结点_03

算法笔记 9.7 堆_堆排序_04

算法笔记 9.7 堆_结点_05

定义及向下调整代码:

const int maxn=100;
//heap数组存储堆 n为实际数组元素个数
int heap[maxn],n=10;

//单个结点的向下调整 O(logn)
//对heap下标为low的结点进行调整 high为数组中最后一个元素下标
void downAdjust(int low,int high){
int i=low,j=low*2;//i为欲调整的下标 j为左孩子
while(j<high){//有左孩子
//注意 先判断j+1<high
if(j+1<high&&heap[j+1]>heap[j]) j++;//右孩子更大时j为右孩子下标
if(heap[j]>heap[i]){
swap(heap[i],heap[j]);//调整父子关系
i=j; //调整后可能还需要调整 处理i,j了
j=i*2; //注意每次初始i是欲调整的结点下标 而j是左孩子
}else{
//根就是最大的 直接break,不用调了
break;//return也行 但是不能没有else分支 否则死循环
}
}
}

算法笔记 9.7 堆_结点_06

完全二叉树性质5、6:

5.叶子结点个数为[n/2] (n/2向上取整)  
6.非叶子结点下标范围[1,n/2] (1~n/2向下取整)

建堆代码

//建堆  一开始已经数组heap中已经有了初始序列 建堆只是调整一下数组中元素的位置而已
void CreateHeap(){
//从下往上 第一个非叶子结点开始 向上调整
//n为堆元素个数 全局变量
for(int i=n/2;i>=1;i--){
downAdjust(i,n);
}
}

 

删除结点

删除堆中一个结点  也要考虑调整问题,但是删除后该位置空缺如何调整呢?
给出的策略是:最后一个元素来填空 然后n-- 然后向下调整被删结点位置即可

//删堆
//删除堆中一个结点 也要考虑调整问题,但是删除后该位置空缺如何调整呢?
//给出的策略是:最后一个元素来填空 然后n-- 然后向下调整被删结点位置即可
void DeleteHeap(int i){
heap[i]=heap[n--];
downAdjust(i,n);
}

 

向上调整

对heap数组在[low,high]范围内进行向上调整
low一般置为1 即整棵树的根,high为欲向上调整的结点下标

//插入结点
//向上调整
//对heap数组在[low,high]范围内进行向上调整
//low一般置为1 即整棵树的根,high为欲向上调整的结点下标
void upAdjust(int low,int high){
int i=high,j=i/2;//i为欲调整下标 j为父亲结点
while(j>=low){//父亲在[low,j]范围内 low为1时就整棵树前面的范围
if(heap[j]<heap[i]){
swap(heap[i],heap[j]);
i=j;
j=i/2;
}else{
break;
}
}
}

 

插入结点

算法笔记 9.7 堆_堆排序_07

即:插入元素放在数组最后,然后对最后一个结点向上调整即可

//插入结点 
void insert(int x){
heap[++n]=x;
upAdjust(1,n);
}

 

9.7.2堆排序

相当简单:由于堆顶元素最大,则建立完堆后(必须建立堆,否则更大的元素可能在调整没有走到的子树位置处),先将堆顶元素交换到最后一个元素处(从小打到排序,则最后一个已经排好了,此时只用在1~n-1范围内继续处理堆即可),然后对堆顶元素进行一次1~n-1范围内向下调整,堆顶一定又是次最大的元素(1~n-1的最大),明显重复以上操作即可,当堆中只剩下一个元素,排序完成,完全二叉树又直接是数组存储,此时数组就已经有序了

算法笔记 9.7 堆_数组_08

不断反复即可

堆排序代码

//堆排序 n/2*logn+n*logn==O(nlogn)
void heapSort(){
CreateHeap();//先要建立堆 即对非叶子结点都先分别做一次向下调整
for(int i=n;i>1;i--){
swap(heap[1],heap[i]);
downAdjust(1,i-1);
}
}

 

综合实例:

输入一列数字,堆排序后输出

#include<iostream>
#include<algorithm>
using namespace std;

const int maxn=100;
//heap数组存储堆 n为实际数组元素个数
int heap[maxn],n=10;

//单个结点的向下调整 O(logn)
//对heap下标为low的结点进行调整 high为数组中最后一个元素下标
void downAdjust(int low,int high){
int i=low,j=low*2;//i为欲调整的下标 j为左孩子
while(j<=high){//有左孩子 <=不是小于 high是最大元素下标
//注意 先判断j+1<high
if(j+1<=high&&heap[j+1]>heap[j]) j++;//右孩子时j为右孩子下标
if(heap[j]>heap[i]){
swap(heap[i],heap[j]);//调整父子关系
i=j; //调整后可能还需要调整 处理i,j了
j=i*2; //注意每次初始i是欲调整的结点下标 而j是左孩子
}else{
//根就是最大的 直接break,不用调了
break;//return也行 但是不能没有else分支 否则死循环
}
}
}

//建堆 一开始已经数组heap中已经有了初始序列 建堆只是调整一下数组中元素的位置而已
void CreateHeap(){
//从下往上 第一个非叶子结点开始 向上调整
//n为堆元素个数 全局变量
for(int i=n/2;i>=1;i--){
downAdjust(i,n);
}
}

//删堆
//删除堆中一个结点 也要考虑调整问题,但是删除后该位置空缺如何调整呢?
//给出的策略是:最后一个元素来填空 然后n-- 然后向下调整被删结点位置即可
void DeleteHeap(int i){
heap[i]=heap[n--];
downAdjust(i,n);
}

//向上调整
//对heap数组在[low,high]范围内进行向上调整
//low一般置为1 即整棵树的根,high为欲向上调整的结点下标
void upAdjust(int low,int high){
int i=high,j=i/2;//i为欲调整下标 j为父亲结点
while(j>=low){//父亲在[low,j]范围内 low为1时就整棵树前面的范围
if(heap[j]<heap[i]){
swap(heap[i],heap[j]);
i=j;
j=i/2;//j=i/2 明显O(logn)
}else{
break;
}
}
}

//插入结点
void insert(int x){
heap[++n]=x;
upAdjust(1,n);
}

//堆排序 n/2*logn+n*logn==O(nlogn)
void heapSort(){
CreateHeap();//先要建立堆 即对非叶子结点都先分别做一次向下调整
for(int i=n;i>1;i--){
swap(heap[1],heap[i]);
downAdjust(1,i-1);//heap[1]在i-1的范围内向下调整即可
}
}

int main(){
//千万注意 下标从1开始 范围是[1,n] 0下标不用
for(int i=1;i<=n;i++) cin>>heap[i];
heapSort();
//输出当然也是输出[1,n]
for(int i=1;i<=n;i++) cout<<heap[i]<<" ";
cout<<endl;
return 0;
}

算法笔记 9.7 堆_结点_09

注意,若只是实现堆排序,实现downAdjust、CreateHeap和heapSort三个方法即可(其中upAdjust仅在插入时需要)