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


優先隊列

我們經常會碰到下面這種情況,並不需要將所有數據排序,只需要取出數據中最大(或最小)的幾個元素,如排行榜。

那么這種情況下就可以使用優先隊列,優先隊列是一個抽象數據類型,最重要的操作就是刪除最大元素和插入元素,插入元素的時候就順便將該元素排序(其實是堆有序,后面介紹)了。

 

 

二叉堆

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

先上一個實例:

如有一個數組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


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM