排序算法(四):优先队列、二叉堆以及堆排序

优先队列

我们经常会碰到下面这种情况,并不需要将所有数据排序,只需要取出数据中最大(或最小)的几个元素,如排行榜。

那么这种情况下就可以使用优先队列,优先队列是一个抽象数据类型,最重要的操作就是删除最大元素和插入元素,插入元素的时候就顺便将该元素排序(其实是堆有序,后面介绍)了。

 

 

二叉堆

    二叉堆其实是优先队列的一种实现,下面主要讲的是用数组实现二叉堆。

先上一个实例:

如有一个数组A{9,7,8,3,0,6,5,1,2}

 

用二叉树来表示数组更直观:

 

 

从这张图我们可以总结一些规律:

  1. 当一个二叉树的每个结点都大于等于它的两个子节点时,称为堆有序
  2. 根节点是堆有序的二叉树中的最大结点
  3. 在数组中,位置为K的结点的父节点,位置为K/2,它的两个子节点位置分别为:2K和2K+1(下标从1开始,A[0]不使用)

 

上面这三点应该非常好理解

 

 

下面就引出一个问题,怎样让一个数组变成堆有序呢?

首先,需要介绍两个操作:

  1. 由下至上的堆有序化(上浮)

当插入一个结点,或改变一个结点的值时,上浮指的是交换它和它的父节点以达到堆有序

在上面的堆有序的图中,如果我们把0换成10,那么上浮的操作具体为:

(1)10比它的父节点7大,所以交换

(2)交换后,10比它的父节点9还要打,交换

之后得到的二叉树如下图:

 

代码如下(需要注意,下标是从1开始,A[0]保留不用,以下所有代码相同):

//index based on 1

    public void swim(Integer[] a,Integer key) {

        while(key > 1 && a[key/2] < a[key]) {

            change(a,key/2,key);

            key /= 2;

        }

    }

 

 2.  由上至下的堆有序化(下沉)

由上浮可以很容易得出下沉的概念:

当插入一个结点,或改变一个结点的值时,下沉指的是交换它和它的较大子节点以达到堆有序。

在原来的二叉树中,如果将根节点9换成4,操作如下:

(1)4与它的最大子节点8交换位置

(2)4与它的最大子节点6交换位置

交换后的二叉树如下图:

代码如下:

    //index based on 1

    public void sink(Integer[] a,Integer key) {

        Integer max = key*2;

        while(key*2 < a.length - 1) {

            if(a[key*2] < a[key*2 + 1]) {

                max = key*2 + 1;

            } else {

                max = key*2;

            }

 

            if(a[key] > a[max])

                break;

            

            change(a,key,max);

            key = max;

        }

    }

 

 

 

那么将一个数组构造成有序堆,相应的也有两种方法:使用上浮以及使用下沉:

初始数组如下:

        

Integer[] a = {null,2,1,5,9,0,6,8,7,3};

 

上浮构造有序堆:

    从数组左边到右边依次使用上浮,因为根节点A[1]没有父节点,所以从A[2]开始:

    public void buildBinaryHeapWithSwim(Integer[] a) {

        for(int k=2;k<a.length;k++) {

            swim(a,k);

        }

    }

 

 

    结果如下:

   

 a: [null ,9 ,7 ,8 ,5 ,0 ,2 ,6 ,1 ,3] 读者有兴趣可以自己画一下二叉树,看是否有序

 

 

    

    下沉构造有序堆:

代码:

    public void buildBinaryHeapWithSink(Integer[] a) {

        //index based on 1

        for(int k=a.length/2;k>=1;k--) {

            sink(a,k);

        }

    }

 

为什么使用下沉只需要遍历数组左半边呢?

因为对于一个数组,每一个元素都已经是一个子堆的根节点了,sink()对于这些自对也适用。如果一个结点的两个子节点都已经是有序堆了,那么在该结点上调用sink(),可以让整个数组变成有序堆,这个过程会递归的建立起有序堆的秩序。我们只需要扫描数组中一半的元素,跳过叶子节点。

    a: [null ,9 ,7 ,8 ,3 ,0 ,6 ,5 ,1 ,2]

 

 

可以看到使用下沉和上浮构造出来的有序堆并不相同,那么用哪一个更好呢?

答案是使用下沉构造有序堆更好,构造一个有N个元素的有序堆,只需少于2N次比较以及少于N次交换。

证明过程就略过了。

 

 

堆排序

前面说了那么多,终于要说到堆排序了,其实前面的优先队列和二叉堆都是为了堆排序做准备。

现在我们知道如果将一个数组构造成有序堆的话,那么数组中最大的元素就是有序堆的根节点

那么很容易想到一个排序的思路:

第一种:将数组构造成有序堆,将根节点拿出来,即将A[1]拿出(因为A[0]不用,当然也可以使用,读者可以自己编程实现),对剩下的数组再构造有序堆……

不过第一种思路只能降序排列,并且需要构造一个数组用来存放取出的最大元素,以及最大的弊端是取出最大元素后,数组剩下的其它所有元素需要左移。

那么第二种办法就可以避免以上的问题:

第二种:先看图:

先来解释下这幅图:

  1. 一开始先将数组构造成一个有序二叉堆,如图1
  2. 因为有序二叉堆的最大元素就是根节点,将根节点和最后一个元素交换。
  3. 从index=1到index=a.lenth-1开始调用sink方法重新构造有序二叉堆。(即第二步交换过的最大元素不参与这次的构造)
  4. 经过第三步后,得到数组中第二大的元素即为根节点。
  5. 再次交换根节点和倒数第二个元素

    …….

 

    这样循环下去,即得到按升序排序的数组

 

代码:

    public void heapSort(Integer[] a) {

        for(int k=a.length/2;k>=1;k--) {

            sink(a,k);

        }

        

        Integer n = a.length - 1;

        while(n > 0) {

            change(a,1,n--);

            //去除最后一个元素,即前一个有序堆的最大元素

            sink(a,1,n);

        }

    }

 

 

 

注意在while循环中,sink()方法多了一个参数,这个参数的目的是去掉上一个有序堆的最大元素。

 

全部代码如下:

public class HeapSort extends SortBase {

 

    /* (non-Javadoc)

     * @see Sort.SortBase#sort(java.lang.Integer[])

     */

    @Override

    public Integer[] sort(Integer[] a) {

        // TODO Auto-generated method stub

        print("init",a);

        heapSort(a);

        print("result",a);

        return null;

    }

    

    public void buildBinaryHeapWithSink(Integer[] a) {

        //index based on 1

        for(int k=a.length/2;k>=1;k--) {

            sink(a,k);

        }

    }

    

    public void buildBinaryHeapWithSwim(Integer[] a) {

        for(int k=2;k<a.length;k++) {

            swim(a,k);

        }

    }

    

    public void heapSort(Integer[] a) {

        for(int k=a.length/2;k>=1;k--) {

            sink(a,k);

        }

        

        Integer n = a.length - 1;

        while(n > 0) {

            change(a,1,n--);

            //去除最后一个元素,即前一个有序堆的最大元素

            sink(a,1,n);

        }

    }

    

    //index based on 1

    public void swim(Integer[] a,Integer key) {

        while(key > 1 && a[key/2] < a[key]) {

            change(a,key/2,key);

            key /= 2;

        }

    }

    

    //index based on 1

    public void sink(Integer[] a,Integer key) {

        Integer max = key*2;

        while(key*2 < a.length - 1) {

            if(a[key*2] < a[key*2 + 1]) {

                max = key*2 + 1;

            } else {

                max = key*2;

            }

 

            if(a[key] > a[max])

                break;

            

            change(a,key,max);

            key = max;

        }

    }

    

    public void sink(Integer[] a,Integer key,Integer n) {

        Integer max = key*2;

        while(key*2 < n) {

            if(a[key*2] < a[key*2 + 1]) {

                max = key*2 + 1;

            } else {

                max = key*2;

            }

 

            if(a[key] > a[max])

                break;

            

            change(a,key,max);

            key = max;

        }

    }

    

    public static void main(String[] args) {

        Integer[] a = {null,2,1,5,9,0,6,8,7,3};

        //(new HeapSort()).sort(a);

        (new HeapSort()).buildBinaryHeapWithSink(a);

        print("a",a);

    }

    

}

 

 

 

堆排序的平均时间复杂度为NlogN

posted @ 2015-09-07 12:35  @瞪着太阳的乌鸦  阅读(2606)  评论(2编辑  收藏  举报