排序3:堆排序

堆是一種特殊的樹結構,其最典型的的應用就是堆排序。堆排序是一種原地的、時間複雜度爲O(nlogn)的排序算法。

什麼是堆
  1. 必須是一棵完全二叉樹。
  2. 每個節點都必須大於等於(或小於等於)它的所有子節點,等價於每個節點都必須大於等於(或小於等於)它的左右節點。
如何實現一個最大堆

我們可以選取基於數組的順序存儲法來保存堆。根節點存儲在數組下標爲1的位置,左子節點存儲在下標2的位置,右子節點存儲在下標3的位置……以此類推,下標爲n元素的左子節點的下標爲2n,右子節點的下標爲2n+1。

  1. 堆的基本結構
    其有三個成員變量,T[] arr用於存儲堆中元素,capacity表示這個堆的最大容量,count表示當前堆中元素個數。‘’
    public class MaxHeap <T extends Comparable> {
        private T[] arr;
        private int capacity;
        private int count;
        public MaxHeap(int capacity){
            arr = (T[]) new Comparable[capacity+1];
            this.capacity = capacity;
        }
        public MaxHeap(T[] arr) {
            this(arr.length);
            for (T t : arr) {
                insert(t);
            }
        }
        public int size() {
            return count;
        }
        public boolean isEmpty() {
            return count == 0;
        }
        public T getMax() {
        	return arr[1];
    	}
    }
    
  2. 向堆中插入一個元素
    我們可以將這個新元素直接添加到數組的最後,但這樣一來就無法保持堆的性質,所以我們需要重新調整堆的結構,以保證其維持堆的特性。我們可以將這個元素不斷向上移動到合適的位置,這個過程是一種自下而上(shift up)的堆化。
    例如,我們在一個最大堆中插入一個值爲40的元素,
    首先,將這個元素放在數組最後(索引n),然後與其父節點(索引n/2)比較,若大於父節點元素,則與父節點元素交換位置。
    在這裏插入圖片描述
    元素到達新位置後,繼續與其父節點元素比較,若仍大於父節點元素,則繼續交換。
    在這裏插入圖片描述
    直到該元素小於等於其父節點元素或者該元素交換到了根節點,此時整個堆就又恢復了其性質,至此堆化過程結束。
    在這裏插入圖片描述
    上面的思路翻譯成代碼:
    public void insert(T t) {
        arr[++count] = t;
        shiftUp(count);
    }
    
    private void shiftUp(int index) {
        if (index<=1) {
            return;
        }
        int prtindex = index/2;
        if (arr[index].compareTo(arr[prtindex])>0) {
            SortTestHelper.swap(arr, index, prtindex);
            shiftUp(prtindex);
        }
    }
    
  3. 取出堆中最大元素
    當我們從堆中取出最大元素時,也就是從堆中移除了這元素,那麼數組中下標1的位置就空缺了,也就是二叉樹的根空缺了。我們可以將數組中最後一個元素提到根節點上,然後在不斷交換下移到正確的位置,這個過程稱爲自上而下(shift down)的堆化。
    首先,將第一個元素移除,此時根結點空缺。
    在這裏插入圖片描述
    第二步,我們將最後一個元素移到根結點,以維持樹的基本結構。但這時34比它的左右子節點40和36都小,不滿足堆的性質。我們要想辦法將34這個節點放到正確的位置。
    在這裏插入圖片描述
    第三步,對比34和它的左右子節點,選擇其中較大的一個交換位置。在這裏插入圖片描述
    此時,34大於左子節點26而小於右子節點39,所以交換34與39的位置。
    在這裏插入圖片描述
    一直重複這個過程,不斷比較、下移,直到34這個節點大於等於其子節點,
    在這裏插入圖片描述
    上面的思路翻譯成代碼:
    public T extractMax() {
        T t = arr[1];
        arr[1] = arr[count];
        count--;
        shiftDown(1);
        return t;
    }
    private void shiftDown(int index) {
        if (index>count/2) {
            return;
        }
        int cldIndex = index*2;
        if (cldIndex <count && arr[cldIndex ].compareTo(arr[cldIndex +1])<0) {
            cldIndex ++;
        }
        if (arr[index].compareTo(arr[cldIndex ])<0) {
            SortTestHelper.swap(arr, index, cldIndex );
            shiftDown(cldIndex );
        }
    }
    
如何基於一個堆實現排序

根據上面的思路我們就可以很輕鬆的基於一個堆實現排序。
將數組元素依次插入最大堆,然後再依次取出最大值,賦值回數組。此時數組中的元素就完成了排序,無論是創建堆的過程, 還是從堆中依次取出元素的過程, 時間複雜度均爲O(nlogn),整個堆排序的整體時間複雜度爲O(nlogn)。但這個實現不是原地的。
那有沒有原地排序的實現方案呢?
首先我們可以將數組原地堆化,這個過程稱爲heapify。也就是從第一個非葉子節點(索引:(n-1)/2,注意現在根節點的索引爲0)開始,將每棵子樹heapify,得到最大堆。
此時索引0位置就是當前數組中的最大值,交換索引0和索引n-1處的元素,最大值就被放到了排序後正確的位置。而arr[0,n-2]又失去了最大堆的性質,需要重新heapify,再將新的最大值與n-2位置的元素(也就是新堆中的最後一個元素)交換。如此重複,直到堆中只剩下一個元素。此時數組就排序完成了。
下圖就是原地堆排序的過程,其中淺粉色表示不滿足堆性質的數組,淺綠色表示heapify後得到的最大堆。
在這裏插入圖片描述
上面的思路翻譯成代碼:

public class HeapSort {
    private HeapSort() {}
    public static void sort(Comparable[] arr) {
        int r = arr.length-1;
        // 原地建堆
        for (int i=(r-1)/2; i>=0; i--){
            shiftDown(arr, i, r);
        }
        // 將最大值,與新堆中的最後一個元素交換,再次原地建堆
        for (int i=arr.length-1; i>0; i--) {
            SortTestHelper.swap(arr,0, i);
            shiftDown(arr, 0, i-1);
        }
    }

    private static void shiftDown(Comparable[] arr, int l, int r) {
        while(2*l+1<=r) {
            int newIndex = 2*l+1;
            if (newIndex+1<=r && arr[newIndex].compareTo(arr[newIndex+1])<0) {
                newIndex++;
            }
            if (arr[l].compareTo(arr[newIndex])>=0) {
                return;
            }
            SortTestHelper.swap(arr, l, newIndex);
            l=newIndex;
        }
    }
}
總結

堆分爲大頂堆和小頂堆,上面都是以大頂堆爲例,小頂堆同理。其特點是二叉樹中的每個節點都大於等於(或小於等於)其子節點。
堆的常見的操作是插入元素和取出堆頂元素。這兩個操作都會破壞堆性質,需要重新堆化。
堆的經典應用就是堆排序。堆排序分兩個過程,堆化和排序。堆化的時間複雜度爲O(logn),排序需要遍歷n次,將n個元素依次放入正確位置。所以堆排序的時間複雜度爲O(nlogn)。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章