1. 程式人生 > >【圖解演算法】排序演算法——堆排序

【圖解演算法】排序演算法——堆排序

簡介

關於堆排序(HeapSort),堆這種資料結構比這種排序演算法更為有價值。

堆排序(Heapsort)是指利用堆積樹(堆)這種資料結構所設計的一種排序演算法,它是選擇排序的一種。可以利用陣列的特點快速定位指定索引的元素。堆分為大根堆和小根堆,是完全二叉樹。大根堆的要求是每個節點的值都不大於其父節點的值,即A[PARENT[i]] >= A[i]。在陣列的非降序排序中,需要使用的就是大根堆,因為根據大根堆的要求可知,最大的值一定在堆頂。(from 百度百科)

這裡需要補充介紹一下堆這種資料結構:

堆通常是一個可以被看做一棵樹的陣列物件。堆總是滿足下列性質:

  1. 堆中某個節點的值總是不大於或不小於其父節點的值;
  2. 堆總是一棵完全二叉樹。

最大二叉推

我們可以根據性質一的描述將堆分為最大堆(子節點永遠小於父節點)、最小堆(子節點永遠大於父節點),上圖所示是一個最大堆。我們可以在計算機中使用一個數組來儲存一個堆。

[ -, 90, 36, 17, 25, 26, 7, 1, 2, 3, 10 ]

注意我們將陣列下標為 0 的位置棄用了,根據圖示我們不難得出一個節點的父節點和子節點的下標位置。

  1. 父節點的位置: i/2
  2. 左子節點的位置: i*2
  3. 右子節點的位置: i*2 + 1

維護堆

維護一個堆需要做兩件事情:進隊和出隊

進隊

HeapIn

進隊需要做的事情很簡單,先將元素丟進佇列尾,然後和父節點進行比較,比父節點大時則於其交換位置,比父節點小時則表示進隊完成。

出隊

HeapOut

出隊像上圖所示,也非常簡單,只需要將佇列頭位置的元素取出(下標為 1 的元素),然後將佇列尾的元素填充到佇列頭,接下來和子節點較大者對換位置,直至比子節點的元素都要大時結束交換位置。

排序

在弄清楚了堆的維護之後我們只需要將陣列中的元素入隊和出隊,即可完成整個排序過程。

Java 實現

MaxHeap.java

/**
 * 最大二叉堆
 * 概念: 父結點的鍵值總是大於或等於任何一個子節點的鍵值
 */
public class MaxHeap {
    // 模擬最大堆的陣列,記錄從 1 開始

    private int[] heapArr;

    // 當前位置的計數,由此可得出:
// 父節點位置 count/2 ; // 左邊的子節點 count * 2 ; // 右邊的子節點 count * 2 + 1 。 private int count; // 堆的大小 private int length; /** * @param length 申明 heapArr 的長度 */ public MaxHeap(int length) { this.heapArr = new int[length + 1]; this.count = 0; this.length = length; } public int size() { return count; } public boolean isEmpty() { return count == 0; } /** * 插入一個新的元素 * @param item */ public void insert(int item) { if(count + 1 > length) throw new ArrayIndexOutOfBoundsException(); heapArr[count + 1] = item; count ++; shiftUp(count); } /** * 取出最大(優先)的元素 * @return 存在即取出改元素,若不存在則返回 Integer 最小值 */ public int extractMax() { if(count < 1) throw new ArrayIndexOutOfBoundsException(); int ret = heapArr[1]; swap(heapArr, 1, count ); count --; shiftDown(1); // 由於上面的 count-- 了,即 count >= 0 表明下標為 1 的位置存在值 return count >= 0?ret:Integer.MIN_VALUE; } /** * 將堆中最大的元素出隊,並調整最大二叉堆的位置,保持最大二叉堆的定義 */ private void shiftDown(int n) { while (n*2 <= count) { // 當 n 位置的元素存在子節點時 // 較出左節點和右節點的較大的元素,然後將較大元素與 n 位置元素比較,若 n 位置元素較小則與其交換位置 int j = n*2; // 較大子節點下標,初始化為左子節點 if(j + 1 <= count) { // 存在右節點 j = heapArr[j + 1] > heapArr[j]?j+1:j; } if(heapArr[n] < heapArr[j]) { swap(heapArr, n, j); n = j; }else { break; } } } /** * 調整位置為 n 的元素的位置,即保持最大二叉堆的定義 -> 父結點的鍵值總是大於或等於任何一個子節點的鍵值 * @param n 需要調整的元素的位置 */ private void shiftUp(int n) { while (n > 1 && heapArr[n/2] < heapArr[n]) { swap(heapArr, n/2, n); n = n/2; } } /** * 交換陣列中的兩個元素 * @param arr * @param a * @param b */ private void swap(int[] arr,int a,int b){ int temp = arr[a]; arr[a] = arr[b]; arr[b] = temp; } }

測試方法


    public static void main(String[] args) {
        MaxHeap maxHeap = new MaxHeap(100);
        int [] testArr = {2,7,26,25,19,17,1,90,3,36};
        for (int i = 0; i < testArr.length; i++) {
            maxHeap.insert(testArr[i]);
        }

        for (int i = 0; i < testArr.length; i++){
            System.out.print( maxHeap.extractMax() );
            if(i != testArr.length - 1) System.out.print(", ");
        }

    }

HeapSort
堆排序的過程

最後

過兩天就新年了,祝願各位朋友新年快樂!

還有需要一提的是: 該演算法存在優化的途徑,比如說在入隊時可以採取直接整個陣列入隊的方式;以及在交換方式上優化,使用賦值的方式取代(直接算出需要交換的值,避免多次交換值),諸君加油。

圖解演算法目錄