排序算法(五)

2. 选择排序—堆排序(Heap Sort)

堆排序是一种树形选择排序,是对直接选择排序的有效改进。

基本思想:

堆的定义如下:具有n个元素的序列(k1,k2,...,kn),当且仅当满足

时称之为堆。由堆的定义可以看出,堆顶元素(即第一个元素)为最小项(小顶堆)。

若以一维数组存储一个堆,则堆对应一颗完全二叉树,且所有非叶节点的值均不大于(或不小于)其子女的值,根节点的值(堆顶元素)的值是最小(或最大)的。如:

(a)大堆顶序列(96,83,27,38,11,09)

(b)小堆顶序列(12,36,24,85,47,30,53,91)

初始时把要排列的的n个数的序列看做是一颗顺序存储的二叉树(一维数组存储二叉树),调整他们的存储序,使之成为一个堆。将堆顶元素输出,得到n个元素中最小(或最大)的元素,这时堆的根节点的数最小(或最大)。然后对前面的n-1个元素重新调整使之成为堆,输出堆顶元素,得到n个元素中次小(或次大)的元素。依此类推,知道只有俩个节点的堆,并对他们做交换,最后得到n个节点的有序序列,称这个过程为堆排序。

 因此,实现堆排序需要解决俩个问题:

1、如何将n个待排序的数建成堆。

2、输出堆顶元素后,怎样调整剩余n-1个元素,使其成为堆。

 

首先,我们讨论第二个问题:输出堆顶元素后,对剩余的n-1个元素重新建成堆的调整过程。

调整小堆顶的方法(大堆顶也是一样的道理):

1、设有m个元素的堆,输出堆顶元素后,剩下m-1个元素。将堆底元素送入堆顶(最后一个元素与堆顶元素进行交换),堆被破坏,其原因仅是根节点不满足堆的性质。

2、将目前最新的根节点与左、右子数中较小元素进行交换。

3、若与左子数交换:如果左子数堆被破坏,即左子数的根节点不满足堆的性质,则重复方法(2)。

4、若与右子数交换,如果右子数堆被破坏,即右子数的根节点不满足堆的性质。则重复方法(2)。

5、继续对不满足堆性质的子数进行上述交换操作,知道叶子节点,堆被建成。

称这个字根节点到叶子节点的调整过程称为筛选。如图:

 

再讨论对n个元素初始建堆的过程。

建堆方法:对初始序列建堆的过程,就是一个反复进行筛选的过程。

1、n个节点的完全二叉树,则最后一个节点是n/2个节点的子数。

2、筛选从第n/2个节点为根的子数开始,该子数成为堆。

3、之后向前一次对各节点为根的子数进行筛选,使之成为堆,直到根节点。

如图建堆的初始过程:无序序列:(49,38,65,97,76,13,27,49) 

算法的实现:

从算法的描述来看,堆排序需要俩个过程,一是建立堆,二是堆顶与堆的最后一个元素交换位置。所以,堆排序有俩个函数组成,一是建堆的渗透函数,二是反复调用渗透函数实现排序的函数。

public class HeapSort {

    public static void main(String[] args) {
        int[] a = {12,54,76,23,435,98,21,1,2,23,54,76,34,2,78,243};
        heapSort(a, a.length);
        
        for(int i=0; i<a.length; i++){
            System.out.print(a[i]+" ");
        }
    }
    /**
     * 堆排序算法
     * */
    public static void heapSort(int[] a,int length){
        buildingHeap(a, a.length);    //初始堆
        
        for(int i=length-1; i>0; --i){
            int temp = a[i];
            a[i] = a[0];
            a[0] = temp;
            heapAdjust(a, 0, i);
        }
    }
    /**
     * 初始堆进行调整
     * 将a[0..length-1]建成堆
     * 调整完之后第一个元素师序列的最小元素
     * */
    public static void buildingHeap(int[] a,int length){
        //最后一个有孩子的节点的位置i = length/2 - 1
        for(int i=(length/2)-1; i>=0; --i){
            heapAdjust(a, i, length);
        }
    }
    /**
     * 已知a[a...m]除了a[s]外均满足堆的定义
     * 调整a[s],使其成为大顶堆,即将对第s个节点为根的子数帅选
     * 
     * @param a是待调整的数组
     * @param s是待调整的数组元素的位置
     * @param length是数组的长度
     * */
    public static void heapAdjust(int[] a,int s,int length){
        int temp = a[s];
        int child = 2*s+1;    //左孩子节点的位置。
        while(child < length){
            if(child+1 < length && a[child] <a[child+1] ){//如果右孩子大于左孩子(找到比当前待调整节点大的孩子)
                ++ child;
            }
            if(a[s]<a[child]){//如果较大的子节点大于父节点
                a[s] = a[child];//那么把较大的子节点往上移动,替换他的父节点
                s = child;    //重新设置s,即待调整的下一个节点的位置
                child = 2*s+1;
            }else{    //如果当前待调整节点大于他的左右孩子,则不需要调整,直接退出
                break;
            }
            a[s] = temp;//当前待调整的节点放到比其大的孩子的节点的位置上
        }
    }
}

算法分析:

设数深度为k,k=log2n+1。从根到叶的筛选,元素比较次数之多2(k-1)次,交换记录之多k次。所以,在建好堆后,排序过程中的次数不超过下式:

而建堆的比较次数不超过4n次,因此堆排序最坏情况下,时间复杂度也为:O(nlogn)。

郑重声明:本站内容如果来自互联网及其他传播媒体,其版权均属原媒体及文章作者所有。转载目的在于传递更多信息及用于网络分享,并不代表本站赞同其观点和对其真实性负责,也不构成任何其他建议。