堆排序 Heap Sort

斷風雨發表於2018-11-13

堆排序 - Heap Sort

堆排序是指利用堆這種資料結構所設計的一種排序演算法。堆是一個近似完全二叉樹的結構,並同時滿足堆積的性質:即子結點的鍵值或索引總是小於(或者大於)它的父節點。

堆的特徵

  • 堆的資料結構近似完全二叉樹,即每個節點存在兩個子節點
  • 當節點的值小於或等於父節點值,大於或等於子節點值稱為大頂堆(也即根節點的值最大)
  • 當節點的值大於或等於父節點值,小於或等於子節點值稱為小頂堆(也即根節點的值最小)
  • 若當前節點的索引為 k , 那麼左子節點索引為 2k + 1, 右子節點索引為 2k + 2, 父節點索引為 (k - 1) / 2

在本文中我們以大頂堆為例

堆的動作 - 上浮

上浮 : 是指在構建堆時,若節點的值大於父節點則將當前節點與父節點互相交換;直至該節點小於父節點時終止上浮(可以理解為一個新入職的員工能力出眾晉升更高的職位); 效果如下圖

堆排序 Heap Sort

程式碼如下:

	private void siftUp(int k) {
		// k == 0 時表明上浮到根節點,結束上浮操作
        while (k > 0) {
        	// 獲取父節點索引
            int parent = (k - 1) / 2;

            // 小於父節點時退出,結束上浮操作
            if (less(parent, k)) {
                break;
            }
            // 與父節點交換資料
            swap(parent, k);
            // 改變 k 的指向 繼續上浮
            k = parent;
        }
    }
複製程式碼

堆的動作 - 下沉

下沉 : 是指構建堆的過程中,若當前節點值小於子節點則將當前節點與子節點互相交換,直至該節點大於子節點時終止下沉(可以理解為一個leader能力平庸的時候被降職的過程,是不是有點很慘); 效果如下圖

堆排序 Heap Sort

程式碼如下:

	private void siftDown (int k, int length) {
        // 獲取左子節點索引
        int childIndex = 2 * k + 1;
		// 判斷是否存在子節點
        while (childIndex < length) {
            // 判斷左右子節點 查詢最大的子節點 
            if (childIndex + 1 < length && !less(childIndex, childIndex + 1)) {
                childIndex++;
            }

            // 若當前節點大於子節點 退出迴圈
            if (less(k, childIndex)) {
                break;
            }

            // 判斷當前節點是否小於子節點, 若小於執行交換
            swap(k, childIndex);
            // 改變 k 指向
            k = childIndex;

            childIndex = 2 * k + 1;
        }
    }
複製程式碼

堆排序

那麼如何採用堆的資料結構,對一個無序的陣列進行排序呢 ?

  • 首先將無序陣列構造成一個最大堆,此時根節點為最大值
  • 將最後一個節點與根節點值交換,剔除最大值節點;
  • 將剩下節點重新執行構造堆
  • 迴圈執行第 2,3 兩步操作
無序陣列構造堆

將無序陣列構造堆,可以採用上浮, 也可以採用下沉的方式處理

堆排序 Heap Sort

如上圖所示,為採用上浮的方式構建堆,其流程是依次從下標為 0 開始對陣列的每個元素進行上浮操作,直至最後得到一個有序的大頂堆。

堆排序 Heap Sort

如上圖所示,為採用下沉的方式構建堆,其流程是依次從非葉子節點 開始對陣列的每個元素進行下沉操作,直至最後得到一個有序的大頂堆。

程式碼如下:

	for (int i = 0; i < array.length; i++) {
		// 上浮方式構建堆
        siftUp(i);
    }
複製程式碼
// 因為堆是完全二叉樹的特性, 所以下標小於等於 array.length / 2 的節點為非葉子節點
// 採用下沉的方式 從下往上構建子堆
    for (int i = array.length / 2; i >= 0; i--) {
        siftDown(i, array.length);
    }
複製程式碼

完成初始堆構造之後,剩下的工作就是重複進行以下邏輯(這個地方就不畫圖了):

  • 尾節點和根節點交換元素
  • 剔除尾節點,對餘下的元素進行子堆構造(構造堆的方式與初始堆一樣)

完整程式碼如下 :

public class HeapSort {

    private int[] array;

    public HeapSort(int[] array) {
        this.array = array;
    }

    private boolean less (int i, int j) {
        return array[i] > array[j];
    }

    private void swap (int k, int j) {
        int temp = array[k];

        array[k] = array[j];
        array[j] = temp;
    }

    /**
     * 下沉操作
     *
     * @param k
     */
    private void siftDown(int k, int length) {
        // loop
        // 判斷是否存在子節點
        int childIndex = 2 * k + 1;

        while (childIndex < length) {
            // 查詢最大的子節點
            if (childIndex + 1 < length && !less(childIndex, childIndex + 1)) {
                childIndex++;
            }

            // 若當前節點大於子節點 退出迴圈
            if (less(k, childIndex)) {
                break;
            }

            // 判斷當前節點是否小於子節點, 若小於執行交換
            swap(k, childIndex);
            // 改變 k 指向
            k = childIndex;

            childIndex = 2 * k + 1;
        }
    }

    /**
     * 上浮操作
     *
     * @param k
     */
    private void siftUp(int k) {
        while (k > 0) {
            int parent = (k - 1) / 2;

            // 小於父節點時退出
            if (less(parent, k)) {
                break;
            }
            // 與父節點交換資料
            swap(parent, k);
            // 改變 k 的指向
            k = parent;
        }
    }

    public void sort () {
        // 構造堆
        for (int i = 0; i < array.length; i++) {
            siftUp(i);
        }

        print();

        int n = array.length - 1;

        while (n > 0) {
            // 因為每次完成堆的構造後, 根節點為最大(小)值節點
            // 將根節點與最後一個節點交換
            swap(0, n);

            for (int i = 0; i <= n - 1; i++) {
                // 排除有序的節點
                // 重新構造堆
                siftUp(i);
            }

            print();

            n--;
        }
    }

    private void sort1 () {
        // 構建堆
        // 因為堆是完全二叉樹的特性, 所以下標小於等於 array.length / 2 的節點為非葉子節點
        // 採用下沉的方式 從下往上構建子堆
        for (int i = array.length / 2; i >= 0; i--) {
            siftDown(i, array.length);
        }

        print();

        int n = array.length - 1;

        while (n > 0) {
            // 因為每次完成堆的構造後, 根節點為最大(小)值節點
            // 將根節點與最後一個節點交換
            swap(0, n);

            for (int i = n / 2; i >= 0; i--) {
                // 排除有序的節點
                // 重新構造堆
                siftDown(i, n);
            }

            print();

            n--;
        }

    }
    private void print () {
        for (Integer num : array) {
            System.out.print(num);
            System.out.print(",");
        }
        System.out.println("");
    }

    public static void main(String[] args) {
        int[] array = {10, 40, 38, 20, 9, 15, 25, 30, 32};

        new HeapSort(array).sort();

        System.out.println("");

        new HeapSort(array).sort1();
    }
}

複製程式碼

小結 : 堆排序在哪裡應用到了呢 ? 可參考優先佇列 PriorityQueue 的實現

相關文章