前言
此處所說的堆,是數據結構中的堆,而不是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源碼的優先隊列中也能看到堆的實現。