上一篇文章讲过了快速排序,这篇文章主要讲讲堆排序
2.5、堆排序算法
1、什么是堆
堆是一种树结构,更准确的说是完全二叉树。并且每个非叶节点的数据大于或等于(小于或等于)其左、右子结点的数据,这样堆结构的根结点为最大值(最小值),堆称为最大堆(最小堆)。所以可以归纳为:堆是一个完全二叉树,非叶结点与左右子结点的数据有一定的大小关系。
2、什么是堆排序
完整的堆排序需要经过反复的两个步骤:构造堆结构和堆排序输出。
用通俗的话来说就是:
(1)将一组数据放到一个完全二叉树中去,再通过数据交换,使完全二叉树变成堆。
(2)根据堆的定义我们知道,堆的根结点是最大值(最小值),这样我们将跟结点的值输出,就得到了这一组数据的最大值(最小值)。
(3)使剩余的数据再次构成一个堆,然后输出跟结点。
(4)不断重复上述步骤就可以实现原数据按顺序输出。那现在的问题就是如何构造堆以及如何堆排序输出。
3、如何构造堆
构造堆结构就是把原始的无序数据按照前面堆结构的定义进行调整。
(1)将原始的无序数据放到一个完全二叉树的各个结点中去。
(2)然后由完全二叉树的最下层的非叶结点开始逐层向上进行调整(因为叶结点没有左右子树,所以一定是符合条件的),直到所有结点(在调整过程中某些非叶结点可能需要经过多次调整)最后都满足堆结构的条件为止。整个过程其实就是将最大值(最小值)不断向上移动,直到移到跟结点为止。
(3)在调整过程中,假设父结点与左结点的大小关系不合规则,需要进行值得交换。交换之后父结点与左右结点的大小关系符合要求了,但是此时如果左结点是一个非叶结点(也有子结点),那么左结点与该左结点的子结点之间就有可能不符合规则,即还需要对子结点进行调整,直到叶结点。
下面给出一个实际的例子来分析构造堆的过程。
(1)将原始数据构成一个完全二叉树。
(2)最后一个非叶结点为结点4,从该结点进行堆化操作。该结点只有左子树且49>38。按照最大堆的定义,是使4和8这两个结点互换位置
(3)上述完全二叉树中倒数第2个非叶结点是结点3,显然结点3及其子结点已经构成堆结构,无需对该结点进行堆化操作。
(4)上述完全二叉树中倒数第3个非叶结点是结点2,对该结点进行堆化操作。由于结点2的两个子结点中结点5的值较大,因此使结点2与结点5进行比较。因为65<97,此时需要将结点2与结点5互换数据。这样结点2及其子及结点构成堆
(5)上述二叉树中倒数第4个非叶结点是结点1,对该结点进行堆化操作,由于结点1的两个子节点中结点2的值较大。所以使结点1与结点2进行比较。67小于97,因此将结点1与结点2互换数据。这时结点2的值已经改变,需要重新对结点2及其子结点进行堆化操作,因为结点1与结点2互换数据可能破坏了结点2和其子结点构成的堆。 这样结点1及其子结点构成堆结构。
4、堆排序输出
(1)根据堆结构的特点,堆结构的根结点也就是最大值。这里按照从小到大排序,因此将其放到数组的最后,将结点8与结点1互换。
(2)结点8的值38换到跟结点之后,对除最后一个结点外的其他结点结点重新执行前面介绍的构造堆的过程,重新得到堆。
(3)重复上述过程,取此时堆结构的根结点(最大值)进行互换,放在数组的后面。此时结点1与结点7互换。
(4)然后重复将剩余的数据构造堆结构,取根结点与最后一个结点交换。结果如下图。
(5)再进一步执行堆排序输出,结点1与结点3互换,结果如下图
(6)堆最后的两个数据进行处理,得到最终的结果
5、具体的代码实现
具体代码在这就不解释了,代码中的注释写的非常详细。
/**
* @author Mr0
*
*/
public class P4_6_Select_HeapSort {
public static void heapSort(int a[]){
buildHeap(a);
int n = a.length;
for(int i=n-1;i>=1;i--){
/*将最大值与堆的最后一个数(在内存中表现为数组中下标最大的值)互相交换位置*/
swap(a,0,i);
/*将完全二叉树的最后一个叶节点(即上行代码执行交换操作中的最大值)去掉,剩下的完全二叉树 跟节点不再是最大值,调用Heapify方法,将完全二叉树堆化,注意这里的堆化与调用buildHeap进行第一次的堆化是不同的,这里的完全二叉树只有跟节点与左右子节点是不合要求的,其他节点都是符合要求的,所以直接调用heapify方法即可完成堆化*/
Heapify(a,0,i);
}
}
/**
* @param a
* 将一个完全二叉树堆化(最大堆)
* 核心思想:找到二叉树中最后一个叶节点的父节点(第一个非叶节点),比较父节点与左右节点(左右节点至少存在一个)的值,
* 这时会出现两种情况:1、左右节点的某一个值比父节点大,此时将较大的值与父节点互换,记录下进行了互换操作的左右节点的id,
* 再调用Heapify对以左(右)节点为跟节点的子二叉树进行堆化 2、左右节点都比父节点小,此时父节点与子节点之间没有值得交换
* 不需要对以左(右)节点为父节点的子二叉树进行堆化,循环到下一个非叶节点。再循环1、2的判断过程
* 最终就完成了完全二叉树的堆化。
*/
public static void buildHeap(int a[]){
int n= a.length;
for(int i=(n/2-1);i>=0;i--){ //n/2-1一定是完全二叉树的最后一个非叶节点,通过for循环一直到跟节点
Heapify(a,i,n);
}
}
/**
* @param a
* @param id
* @param max
* 将以id为下标的节点所对应的子树堆化
*/
public static void Heapify(int a[],int id,int max){
int left = id*2+1;
int right = id*2+2;
int largest = 0;
if(left<max && a[left]>a[id]){
largest = left;
}else{
largest = id;
}
if(right<max && a[right]>a[largest]){
largest = right;
}
if(largest != id){ //如果根节点与左右子节点进行了值得交换,则需要对交换值后的子节点再次进行堆化操作
swap(a,largest,id); //如果没有交换值,怎不需要对子节点再次进行堆化操作(因为已经执行过了)
for(int i=0;i<a.length;i++){
System.out.print(a[i]+" ");
}
System.out.print("largest="+largest+" ");
System.out.print("i="+id);
System.out.println("");
Heapify(a,largest,max); //对交换值后的子节点进行堆化操作。
}
}
/**
* @param a
* @param b
* @param c
* 交换a数组中bc下标元素的值。
*/
public static void swap(int a[],int b,int c){
int temp = a[b];
a[b] = a[c];
a[c] = temp;
}
public static void main(String[] args){
int[] array= {1,2,3,4,5,6,7,16,9,10,11,12,13,14,15,8};
System.out.println("排序前数组顺序为:");
for(int i=0;i<array.length;i++){
System.out.print(array[i]+" ");
}
System.out.println("");
heapSort(array);
System.out.println("");
System.out.println("排序后的数组顺序为:");
for(int i=0;i<array.length;i++){
System.out.print(array[i]+" ");
}
}
}