用Java寫算法之七:堆排序

堆是數據結構中的一種重要結構,瞭解了「堆」的概念和操做,能夠快速掌握堆排序。
html


堆的概念java

堆是一種特殊的徹底二叉樹(complete binary tree)。若是一棵徹底二叉樹的全部節點的值都不小於其子節點,稱之爲大根堆(或大頂堆);全部節點的值都不大於其子節點,稱之爲小根堆(或小頂堆)。算法

在數組(在0號下標存儲根節點)中,容易獲得下面的式子(這兩個式子很重要):數組

1.下標爲i的節點,父節點座標爲(i-1)/2;數據結構

2.下標爲i的節點,左子節點座標爲2*i+1,右子節點爲2*i+2。ide


堆的創建和維護oop

堆能夠支持多種操做,但如今咱們關心的只有兩個問題:post

1.給定一個無序數組,如何創建爲堆?性能

2.刪除堆頂元素後,如何調整數組成爲新堆?this

先看第二個問題。假定咱們已經有一個現成的大根堆。如今咱們刪除了根元素,但並無移動別的元素。想一想發生了什麼:根元素空了,但其它元素還保持着堆的性質。咱們能夠把最後一個元素(代號A)移動到根元素的位置。若是不是特殊狀況,則堆的性質被破壞。但這僅僅是因爲A小於其某個子元素。因而,咱們能夠把A和這個子元素調換位置。若是A大於其全部子元素,則堆調整好了;不然,重複上述過程,A元素在樹形結構中不斷「下沉」,直到合適的位置,數組從新恢復堆的性質。上述過程通常稱爲「篩選」,方向顯然是自上而下。

刪除一個元素是如此,插入一個新元素也是如此。不一樣的是,咱們把新元素放在末尾,而後和其父節點作比較,即自下而上篩選。

那麼,第一個問題怎麼解決呢?

我看過的數據結構的書不少都是從第一個非葉子結點向下篩選,直到根元素篩選完畢。這個方法叫「篩選法」,須要循環篩選n/2個元素。

但咱們還能夠借鑑「無中生有」的思路。咱們能夠視第一個元素爲一個堆,而後不斷向其中添加新元素。這個方法叫作「插入法」,須要循環插入(n-1)個元素。

因爲篩選法和插入法的方式不一樣,因此,相同的數據,它們創建的堆通常不一樣。


大體瞭解堆以後,堆排序就是水到渠成的事情了。


算法概述/思路

咱們須要一個升序的序列,怎麼辦呢?咱們能夠創建一個最小堆,而後每次輸出根元素。可是,這個方法須要額外的空間(不然將形成大量的元素移動,其複雜度會飆升到O(n^2))。若是咱們須要就地排序(即不容許有O(n)空間複雜度),怎麼辦?

有辦法。咱們能夠創建最大堆,而後咱們倒着輸出,在最後一個位置輸出最大值,次末位置輸出次大值……因爲每次輸出的最大元素會騰出第一個空間,所以,咱們剛好能夠放置這樣的元素而不須要額外空間。很漂亮的想法,是否是?

下面是堆排序的示意圖(圖片來自維基百科):


代碼實現

因爲堆是一種數據結構,所以,咱們能夠封裝它爲一個類。固然,也能夠不這麼作。下面的代碼使用篩選法創建了一個堆。

package flyingcat.sort;
/**
 *
 * @author FlyingCat
 * Date: 2013-8-26
 *
 */
public class ArrayHeap {
    private int[] array;
    public ArrayHeap(int[] arr) {
        this.array = arr;
    }
    private int getParentIndex(int child) {
        return (child - 1) / 2;
    }
    private int getLeftChildIndex(int parent) {
        return 2 * parent + 1;
    }
    /**
     * 初始化一個大根堆。
     */
    private void initHeap() {
        int last = array.length - 1;
        for (int i = getParentIndex(last); i >= 0; --i) { // 從最後一個非葉子結點開始篩選
            int k = i;
            int j = getLeftChildIndex(k);
            while (j <= last) {
                if (j < last) {                     
                    if (array[j] <= array[j + 1]) { // 右子節點更大
                        j++;
                    }
                }
                if (array[k] > array[j]) {           //父節點大於子節點中較大者,已經找到最終位置
                    break; // 中止篩選
                } else {
                    swap(k, j);
                    k = j; // 繼續篩選
                }
                j = getLeftChildIndex(k);
            }// loop while
        }// loop i
    }
    /**
     * 調整堆。
     */
    private void adjustHeap(int lastIndex) {
        int k = 0;
        while (k <= getParentIndex(lastIndex)) {
            int j = getLeftChildIndex(k);
            if (j < lastIndex) {
                if (array[j] < array[j + 1]) {
                    j++;
                }
            }
            if (array[k] < array[j]) {
                swap(k, j);
                k = j; // 繼續篩選
            } else {
                break; // 中止篩選
            }
        }
    }
    /**
     * 堆排序。
     * */
    public void sort() {
        initHeap();
        int last = array.length - 1;
        while (last > 0) {
            swap(0, last);
            last--;
            if (last > 0) { // 這裏若是不判斷,將形成最終前兩個元素逆序。
                adjustHeap(last);
            }
        }
    }
    private void swap(int i, int j) {
        int temp = array[i];
        array[i] = array[j];
        array[j] = temp;
    }
}

算法性能/複雜度

堆排序的時間複雜度很是穩定(咱們能夠看到,對輸入數據不敏感),爲O(n㏒n)複雜度,最好狀況與最壞狀況同樣。

可是,其空間複雜度依實現不一樣而不一樣。上面即討論了兩種常見的複雜度:O(n)與O(1)。本着節約空間的原則,我推薦O(1)複雜度的方法。


算法穩定性

堆排序存在大量的篩選和移動過程,屬於不穩定的排序算法。


算法適用場景

堆排序在創建堆和調整堆的過程當中會產生比較大的開銷,在元素少的時候並不適用。可是,在元素比較多的狀況下,仍是不錯的一個選擇。尤爲是在解決諸如「前n大的數」一類問題時,幾乎是首選算法。


參考資料

1.維基百科 http://zh.wikipedia.org/wiki/%E5%A0%86%E6%8E%92%E5%BA%8F#java.E8.AF.AD.E8.A8.80

2.堆排序-chenlb的博客 http://blog.chenlb.com/2008/12/heap-sort-for-java.html

相關文章
相關標籤/搜索