圖解數據結構:堆

前言

此處所說的,是數據結構中的,而不是JVM運行時數據區域的那個。本篇主要講解數據結構中的實現、原理、使用等。

定義

堆(英語:Heap)是計算機科學中的一種特別的樹狀數據結構。若是滿足以下特性,即可稱爲堆:“給定堆中任意節點P和C,若P是C的母節點,那麼P的值會小於等於(或大於等於)C的值”。若母節點的值恆小於等於子節點的值,此堆稱爲最小堆(min heap);反之,若母節點的值恆大於等於子節點的值,此堆稱爲最大堆(max heap)。在堆中最頂端的那一個節點,稱作根節點(root node),根節點本身沒有母節點(parent node)。

在隊列中,調度程序反覆提取隊列中第一個作業並運行,因爲實際情況中某些時間較短的任務將等待很長時間才能結束,或者某些不短小,但具有重要性的作業,同樣應當具有優先權。堆即爲解決此類問題設計的一種數據結構。

實際上是說了堆的以下特點:

  • 堆是一種樹狀結構(邏輯結構)
  • 滿足“每個節點都大於等於其父節點(根節點除外)”的堆稱爲最小堆
  • 滿足“每個節點都小於等於其父節點(根節點除外)”的堆稱爲最大堆
  • 往往用於優先級隊列

一圖勝千言,看下典型的堆結構便知。
最小堆
可以看出除了根節點外,每個節點都滿足節點值大於等於其父親節點的值,所以這是一個最小堆

堆的邏輯結構往往是一棵完全二叉樹,即除了最底層,其他層的節點都被元素填滿,且最底層儘可能地從左到右填入。堆的存儲結構(物理結構)往往是一個數組,也就是二叉樹從根節點開始進行層次遍歷(廣度優先遍歷)得到的結果,所以上述最小堆的存儲結構如下:

[16, 18, 20, 20, 24, 34, 36, 28, 30, 26, 29, 42, 41]

從數組看,存儲的元素毫無規律。仔細分析節點的父子關係和數組的下標可以得出如下性質:

  • 數組中下標爲k(k > 0)的元素,其父節點的下標爲(k - 1) / 2
  • 數組中下標爲k的元素,其左孩子的下標爲k * 2 + 1(如果有左孩子的話)
  • 數組中下標爲k的元素,其右孩子的下標爲k * 2 + 2(如果有右孩子的話)

根據這些性質,可以構建出一個最小堆的實現

最小堆

定義

/**
 * 最小堆
 */
public class MinHeap<E extends Comparable<E>> {

    /**
     * 此處是之前博客中實現的ArrayList,並非JDK提供的ArrayList
     * 實現過程:https://blog.csdn.net/Baisitao_/article/details/102575180
     * 源代碼:https://github.com/Sicimike/note/blob/master/source/ArrayList.java
     */
    private ArrayList<E> data;

    public MinHeap(int initCapacity) {
        data = new ArrayList(initCapacity);
    }

    public MinHeap() {
        data = new ArrayList();
    }
}    

因爲之前的ArrayList代碼中維護的size,所以這裏也不再需要。

核心方法

根據上文提到了三個性質(父子關係和數組下標),可以先實現相關的方法

/**
 * 根據子節點找到父節點
 *
 * @param child
 * @return
 */
private int parent(int child) {
    if (child <= 0 || child > size()) {
        throw new IllegalArgumentException("illegal argument:" + child);
    }
    return (child - 1) / 2;
}

/**
 * 根據父節點找到左子節點
 *
 * @param parent
 * @return
 */
private int left(int parent) {
    return parent * 2 + 1;
}

/**
 * 根據父節點找到右子節點
 *
 * @param parent
 * @return
 */
private int right(int parent) {
    return parent * 2 + 2;
}

/**
 * 查看堆中的最小元素
 * 因爲是最小堆,所以就是data.get(0)
 *
 * @return
 */
public E min() {
    return data.get(0);
}

add(E e)方法

add(E e)方法用於往堆中添加元素,先用圖解的方式,來看下往堆中添加元素的過程:
最小堆元素上浮
整個過程可以分成兩步:
1、新增節點插入二叉樹中,也就是數組的尾部
2、循環比較新增節點和其父親節點的大小,適當的時候交換其與父親節點的位置

其實現如下:

/**
 * 添加元素
 *
 * @param e
 */
public void add(E e) {
	// 容量不夠會自動擴容
    data.add(e);
    siftUp(size() - 1);
}

/**
 * 把節點上浮(siftUp)到合適的地方
 *
 * @param index
 */
private void siftUp(int index) {
    while (index > 0 && data.get(index).compareTo(data.get(parent(index))) < 0) {
        // 當前節點的元素小於父節點的元素,需要上移(交換)
        data.swap(index, parent(index));
        // 更新當前元素的index
        index = parent(index);
    }
}

extractMin()方法

extractMin()方法用於刪除最小堆的根節點,繼續用圖解的方式,來看下刪除最小堆中根節點的過程:

刪除最小元素
可以看到整個過程分爲三步:
1、把根節點和最後一個節點交換位置
2、刪除最後一個節點(也就是之前的根節點)
3、循環下沉根節點

其實現如下:

/**
 * 移除並返回最小的元素
 *
 * @return
 */
public E extractMin() {
    // 獲取最小的元素用於返回
    E result = min();
    // 把最小的元素和最後一個元素交換位置
    data.swap(0, size() - 1);
    // 刪除最後一個元素(最小值)
    data.remove();
    // 堆頂元素下沉到合適的位置
    siftDown(0);
    return result;
}

/**
 * 把節點下沉(siftDown)到合適的地方
 *
 * @param index
 */
private void siftDown(int index) {
    while (left(index) < size()) {
    	// 要下沉的節點還有左孩子
        int k = left(index);
        if (right(index) < size() && data.get(right(index)).compareTo(data.get(left(index))) < 0) {
            // 當前節點有右孩子,且右孩子的值小於左孩子的值,則把右孩子記錄爲待交換的節點k,否則記錄左節點爲k
            // 因爲是最小堆,所以需要找出兩個孩子節點(如果有右孩子)中比較小的那個進行交換
            k = right(index);
        }

        if (data.get(index).compareTo(data.get(k)) <= 0) {
        	// 如果下沉的節點已經小於或者等於兩個子節點中比較小的那個,結束循環
            break;
        }
        // 當前節點的值大於k節點的值,進行交換
        data.swap(index, k);
        index = k;
    }
}

heapify()方法

heapify()方法是將任意一個數組調整成堆的形式。要想實現這個功能,可以從數組第二個元素開始,對每個元素執行上浮(siftUp)操作。
除了這種操作之外,還有一種更高效的實現方式,那就是heapify:從最後一個非葉子節點開始,從下往上,對每個節點執行下沉(siftDown)操作,這樣一開始就排除了一半的節點。具體實現如下(構造方法的形式實現):

/**
 * 利用構造函數實現heapify功能
 * heapify:根據數組生成最小堆
 *
 * @param arr
 */
public MinHeap(E[] arr) {
    data = new ArrayList(arr);
    // parent(data.size() - 1) 就是最後一個非葉子節點的下標
    for (int i = parent(data.size() - 1); i >= 0; i--) {
        siftDown(i);
    }
}

總結

源代碼

Github:MinHeap.java

本篇實現的是最小堆,同理可以實現最大堆。文章開始就說過,堆往往用於優先級隊列,在JDK源碼的優先隊列中也能看到堆的實現。

發佈了55 篇原創文章 · 獲贊 107 · 訪問量 1萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章