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

優先隊列

咱們常常會碰到下面這種狀況,並不須要將全部數據排序,只須要取出數據中最大(或最小)的幾個元素,如排行榜。java

那麼這種狀況下就可使用優先隊列,優先隊列是一個抽象數據類型,最重要的操做就是刪除最大元素和插入元素,插入元素的時候就順便將該元素排序(實際上是堆有序,後面介紹)了。編程

 

 

二叉堆

    二叉堆實際上是優先隊列的一種實現,下面主要講的是用數組實現二叉堆。數組

先上一個實例:ide

若有一個數組A{9,7,8,3,0,6,5,1,2}ui

 

用二叉樹來表示數組更直觀:spa

 

 

從這張圖咱們能夠總結一些規律:3d

  1. 當一個二叉樹的每一個結點都大於等於它的兩個子節點時,稱爲堆有序
  2. 根節點是堆有序的二叉樹中的最大結點
  3. 在數組中,位置爲K的結點的父節點,位置爲K/2,它的兩個子節點位置分別爲:2K和2K+1(下標從1開始,A[0]不使用)

 

上面這三點應該很是好理解code

 

 

下面就引出一個問題,怎樣讓一個數組變成堆有序呢? blog

首先,須要介紹兩個操做: 排序

  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

相關文章
相關標籤/搜索