1、堆排序(Heap Sort)
[知识点补充]
堆: 完全二叉树,每个节点值始终不小于其子节点值,为大根堆;每个节点值始终不大于其两个子节点的值,为小根堆。
此外完全二叉树的特性,用数组存储,父节点与其左右孩子节点的编号容易表示,例如: k 号节点,其左孩子节点的编号为 (2*k),右孩子节点的编号为 (2*k+1),在数组中就用下标表示。
一般用数组来表示堆,注:数组的下标起始为 0 !
例: 一个堆结构的数组 (画个图就很明朗了)
- 下标为 i 的节点的父节点下标为 (i-1)/2
- 左右子节点的下标为 (2i + 1) , (2i + 2)
堆排序的基本思想
如果初始序列是堆,则可以通过反复执行如下操作,最终得到一个有序序列
1、”输出根” : 将根(第一个元素)与当前子序列中的最后一个元素交换
2、调整堆 : 将”输出根”之后的序列调整为堆.如果初始序列不是堆,则首先要将其先调整为堆,然后在按上述步骤实现
实现堆排序有两个核心的操作:
- 如何将一个序列建成一个堆?
- 如何在”输出根”后,调整剩余元素成为一个新堆?
操作2的解决方法:
在输出堆顶元素之后,以堆中最后一个元素替代交换之,此时根节点的左,右子树均为堆,则仅需自上至下的进行调整即可,这个种自堆顶至叶子的调整过程称为” 筛选 “。
以大根堆为例:大根堆堆顶元素与最后一个元素互换(输出根),当前的”堆顶”肯定不满足堆的定义,开始自上而下的调整,因为需要建立大根堆,那么先比较当前”堆顶”的左右孩子,选择其中较大者,与”堆顶“互换,再从互换调整的节点的子树着手,同样的自上而下的调整(见下面图例过程更明朗)
操作1的解决方法:
从一个无序序列建堆的过程就是一个反复” 筛选 “的过程。若将此序列看成是一个完全二叉树,则最后一个非终端结点是 第 ⌞n/2⌟ 个元素,由此“筛选”只需从第 ⌞n/2⌟ 个元素开始。
语言很苍白,用图例来解决 (以大根堆为例)
操作2图例 (筛选过程)
上面的图例只展示了第一次的输出根,调整堆的过程,整个的堆排序是在这个堆结构的序列上不断输出根,调整堆,直至输出最后一个根。
2 . 操作1图例 (堆化序列过程)
给定的无序序列不一定就是堆结构,那么如何将一个无序序列建成一个堆呢?
- 堆化整个无序序列,过程是一个反复”筛选”的过程 (反复调整堆的过程)
- 建堆过程要从下往上逐棵子树地进行筛选,
即根的下标按照从n/2到1的次序将各子树调整为堆。
代码:
//堆排序 Heap_sort
// 3个步骤
//子树的调整-----即子树根结点的筛选
//无序数组的堆化
//堆排序(输出根节点,以及调整堆)
//实现小根堆
#include <iostream>
using namespace std;
void Swap(int &a,int &b)
{
cout<<b<<" ";
int temp=a;
a=b;
b=temp;
}
void Sift(int a[],int i,int n)
{//数组中下标为i的节点进行调整,对应的子树进行"筛选调整" ,n为节点的总数
int temp=a[i];
int j=2*i+1; //i 的 左孩子
while(j<n) //孩子数小于n
{
if(j+1<n && a[j+1]<a[j]) //找左右子节点更小者
{
j=j+1;
}
if(temp<a[j]) //如果根最小,没必要调整了
{
break;
}
a[i]=a[j]; //根节点换成小的
i=j; //可能 根节点调整后造成下面的节点仍需要调整
j=2*i+1;
}
a[i]=temp; //最后给temp安个家
}
//无序数组堆化(小根堆处理)
void HeapAdjust(int a[],int n)
{
for(int i=n/2-1;i>=0;i--) //i大于等于0,等于不要丢,第一个节点也需要调整
{
Sift(a,i,n);
}
}
void Heap_sort(int a[],int n)
{
HeapAdjust(a,n);
for(int i=n-1;i>0;i--)
{
Swap(a[i],a[0]);
Sift(a,0,i);
}
}
int main()
{
int a[10]={3,2,5,4,1,9,6,8,19,11};
int n=10;
Heap_sort(a,10);
return 0;
}
总结之大白话 : 堆排序
整个实现过程:
1. 堆化整个序列
2. 输出根 & 调整堆(筛选过程) ; 反复执行直至输出最后一个根
核心部分就是“筛选过程”的实现算法
堆化序列也是基于”筛选过程”实现的,至于是逐个子树自下而上进行”筛选”,并且是从下标第 ⌞n/2⌟ 个到1的次序,(因为不需要从叶子节点开始调整)可以自行推导一下,理解之后记忆住就行