你看遠處的山它好像一個小頂堆

原創文章,轉載請私信.關注公衆號 tastejava 學習加思考,品味java之美

什麼是小頂堆

小頂堆是一種經過排序的完全二叉樹, 其滿足如下性質:

  1. 小頂堆中的任意父節點都比其兩個孩子結點小

由上方性質又可以推導出如下性質:

  1. 小頂堆的根節點爲整個堆元素中最小的元素

將小頂堆裝入數組

我們當然可以用面向對象的方式描述一顆二叉樹, 但是有沒有不浪費一丁點空間. 即除了元素本身開銷外, 儘量不額外佔用內存空間的描述方式呢?

有的, 我們可以把小頂堆裝入數組中. 爲了把小頂堆裝入數組中, 我們需要給出一個數組中的元素, 就能計算出其對應的父結點, 以及其兩個子節點對應的位置 (這樣我們就能定位到小頂堆中所有元素的位置, 可以操作任意一個元素, 這樣就達到用數組描述小頂堆的目的啦).

裝入數組的小頂堆元素位置滿足如下性質:

  1. 假設有結點m在數組中下標爲n, 那麼其左孩子結點下標爲2n+1, 右孩子結點下標爲2n+2

三條性質, 條條有用

通過上面的介紹, 我們現在知道一個裝在數組中的小頂堆要滿足如下三個性質:

  1. 小頂堆中的任意父節點都比其兩個孩子結點小
  2. 小頂堆的根節點爲整個堆元素中最小的元素
  3. 假設有結點m在數組中下標爲n, 那麼其左孩子結點下標爲2n+1, 右孩子結點下標爲2n+2

這三個性質第一點是元素構成小頂堆的基本性質, 衍生出的第二點性質是利用小頂堆對無序元素進行堆排序的關鍵, 第三點性質是爲了將小頂堆裝入到數組中.

來一根數組, 以及一打元素下酒

假設我們有一個長度爲10的無序數組:

[5, 2, 1, 9, 6, 7, 3, 4, 0, 8]

按照性質3我們可以將無序數組等價還原成一顆完全二叉樹:

      5
     / \
    2   1
  / \   / \
  9  6 7 3
 / \   /
4  0 8

我們的目標是將其元素排序, 構造成符合性質1和2的完全二叉樹, 即小頂堆(同一個完全二叉樹排序後小頂堆有多種情況, 下方展示其中一種).

排序後構造成小頂堆的數組:

[0, 2, 1, 4, 6, 7, 3, 5, 9, 8]

也就是下方所示的小頂堆:

      0
     / \
    2   1
  / \   / \
  4  6 7 3
 / \   /
5  9  3

排序! 每個元素都有屬於自己的位置

以下方的完全二叉樹爲例, 我們分析一下, 如何調整元素順序, 來將其構造成小頂堆呢.

      5
     / \
    2   1
  / \   / \
  9  6 7 3
 / \   /
4  0 8

根據性質1, 我們要讓所有的父節點都比其直接子節點小, 也就是說我們要把二叉樹中每一個子樹的父節點與其子節點比較, 如果父節點比子節點大, 那麼將父節點與其交換, 使得子樹滿足最小堆的性質.

那麼父節點要與孩子結點比較幾次呢, 是存在幾個孩子就比較幾次嗎, 其實比較一次就可以, 我們先讓兩個孩子結點比較, 找到較大的一個, 然後讓父節點與較大的比較, 如果父節點比較大的子節點大, 那麼它肯定也比較小的子節點大, 將父節點與較大的子節點交換, 那麼此時子樹就滿足最小堆性質.

將整顆二叉樹, 最後一個擁有子節點的父節點進行上述調整, 當從後往前處理完所有節點後, 整顆二叉樹都滿足最小堆性質, 那麼就完成了最小堆的構建. 整個過程看起來像是元素在下沉, 比如根節點5, 最終下沉到了4的位置.

上代碼

接下來的代碼展示瞭如何構建小頂堆, 依賴了lombok與junit, 放在IDE裏調試運行可以理解的更清晰.

/**
 * 最小堆的定義是父節點一定比其兩個直接子節點要大
 * 根節點一定是所有元素中最小的, 依據這個性質可以從前往後不斷構建小頂堆, 最終整個數組元素升序排列, 這就是堆排序
 *
 * @author Gaozl
 * @date 2020/6/10 17:00
 */
@Slf4j
public class MinHeap {

    /**
     * 元素數組, 需要先了解下如何用數組表示二叉樹
     * 數組表示的二叉樹有如下特性
     * 假設有結點m下標爲n, 那麼2n+1是結點m的左孩子, 2n+2是結點m的右孩子
     */
    private Integer[] elements = {5, 2, 1, 9, 6, 7, 3, 4, 0, 8};

    /**
     * 嘗試將給定元素放到指定下標, 如果不符合小頂堆特性將調整結點位置直到符合
     * @param index 元素嘗試放入的下標
     * @param element 給定元素
     * @param length 用數組前幾個元素構建小頂堆
     */
    public void siftDown(int index, Integer element, int length) {
        int half = length >>> 1;
        while (index < half) {
            int leftChild = 2 * index + 1;
            int rightChild = 2 * index + 2;
            // 比左右兩個孩子中較小的孩子大才需要下沉, 因爲比較小孩子小時肯定也比較大的孩子小, 此時比兩個孩子都小無需下沉
            int compareChild = (rightChild < length) // 存在rightChild
                    && elements[rightChild] < elements[leftChild] ? rightChild : leftChild;
            // 當前元素比孩子大, 需要下沉
            if (element > elements[compareChild]) {
                elements[index] = elements[compareChild];
                index = compareChild;
            } else { // 否則無需下沉, 停止處理
                break;
            }
        }
        elements[index] = element;
    }

    /**
     * 將數組構建成小頂堆
     */
    public void heapify(int length) {
        for (int i = (length >>> 1) - 1; i >= 0; i--) { // 遍歷所有非葉子結點
            siftDown(i, elements[i], length); // 將每個非葉子結點調整到符合小頂堆性質後, 整個數組自然構建成了小頂堆
        }
    }

    /**
     * 用數組構建小頂堆
     */
    @Test
    public void testHeapify() {
        log.info("{}", toString());
        heapify(elements.length);
        log.info("{}", toString());
    }

    @Override
    public String toString() {
        return Arrays.toString(elements);
    }
}

堆排序

既然我們都瞭解瞭如何構建小頂堆, 那麼可以進一步瞭解一下很有意思的排序方法, 堆排序. 堆排序依賴堆的第2條性質. 直接上代碼吧:)

/**
* 堆排序, 降序是用小頂堆, 小頂堆每次找到最小的元素並下沉.
* 升序是用大頂堆, 大頂堆每次找到最大元素並下沉
*/
@Test
public void testDescendSort() {
    log.info("{}", toString());
    // 先將數組所有元素構建小頂堆, 然後將堆頂最小元素與小頂堆最後一個元素交換
    // 排除此時最後一個最小的元素, 前面元素繼續構建小頂堆, 找到最小元素繼續交換到此時小頂堆最後一個元素
    // 經過構建length次小頂堆, 此時數組裏所有元素已經是降序排列
    for (int i = 0; i < elements.length; i++) {
        int length = elements.length - i;
        heapify(length);
        int temp = elements[length - 1];
        elements[length - 1] = elements[0];
        elements[0] = temp;
    }
    log.info("{}", toString());
}

拓展

本篇文章的構建小頂堆代碼並不是我憑空想象出來的, 是JDK1.8中優先隊列PriorityQueue類中的代碼片段改造註釋而來. 既然聰明的你已經讀到了這裏, 那麼可以順便去看看PriorityQueue源代碼實現啦, 如果還是看不懂也沒關係, 請參考如下開源項目, 超級方便哦, 一個致力於幫大家節約閱讀JDK源碼時間的開源項目

https://github.com/gaozhilai/open-jdk1.8-analysis

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