PriorityQueue 優先隊列
基於MaxHeap最大堆
文章目錄
1、什麼是優先隊列
優先隊列也是一種隊列,它的接口函數和隊列相同。
public interface Queue<E> {
int getSize();
boolean isEmpty();
E dequeue();
void enqueue(E e);
E getFront();
}
雖然代碼相同,需要注意的是,出隊操作:拿出最大值(優先級最高)。但相對於普通的隊列有着宏觀上的不同。
- 普通隊列:先進先出,後進後出
- 優先隊列:出隊和入隊得順序無關,和優先級有關
形象地理解就是超市和醫院的排隊。超市排隊這種特性就符合普通隊列的形式。先排隊先結賬。醫院就不一樣啦,醫院要優先處理急診的病人,這就跟優先級有關,優先級越高的元素放在最前面。優先進行處理。
不同的底層實現方法:
作爲一種抽象的數據結構,底層實現的方法包含很多。數組或者鏈表這種線性結構,當然還有我們的樹結構,當然我們也可以引入順序的線性結構。具體的複雜度如下:
入隊 | 出隊 | |
---|---|---|
普通線性結構 | O(1) | O(N):拿出最大元素 |
順序線性結構 | O(N):順序放入元素 | O(1) |
二叉堆 | O(log(N)) | O(log(N)) |
2、二叉堆的實現
2.1、什麼是二叉堆
二叉堆是一個完全二叉樹。那什麼是完全二叉樹呢?
滿二叉樹就是除了最下面一層,其他的節點都是具有左右孩子節點,就類似於這樣。
完全二叉樹就類似這種:
完全二叉樹不是一顆滿的二叉樹,但是它的不滿的那一部分一定在他的右下角部分。存放的過程也就是從左向右的過程。
堆的特性和二分搜索樹不同,堆的某個節點總是不大於其父親節點的值。也就是它並不具有順序性。我們可以看一下下面這張圖。
可以看出任意子樹的最大值永遠是自己的父親節點。
2.2、二叉堆的結構
這裏我們可以看出來二叉堆是一層一層的從左到右這麼依次排列的,所以這裏我們使用數組進行存儲二叉樹。通過數組索引找到節點。
這樣我們就非常巧妙的將樹結構存儲到了數組當中。我們還可以發現下面的規則:
- 左孩子的索引等於該父親節點索引值的 2 倍 + 1
- 有孩子的索引等於該父親節點的索引值的 2 倍 + 2
- 父親節點的索引值等於左右孩子節點的(索引值 - 1) / 2
用代碼展示就是:
parent(i) = (i - 1)/ 2;//獲得i索引值的父親節點索引值
leftChild(i) = i * 2 + 1
rightChild(i) = i * 2 + 2
2.3、初始化操作
在最大堆這個數據結構當中我們使用的是數組的底層實現,當然我們也就需要動態數組來實現這個動態大小的最大堆。關於Array動態數組這一章可以參考Array 動態數組。當然也可以直接使用Java自帶的動態數組。
初始化程序實現:
public MaxHeap() {
data = new Array<>();
}
public MaxHeap(int capacity) {
data = new Array<>(capacity);
}
節點索引查詢實現:
我們需要對查詢父親節點進行判斷,因爲index-1操作會導致負值出現。
private int parent(int index) {
if (index == 0)
throw new IllegalArgumentException("index isn't zero");
return (index - 1) / 2;
}
private int leftChild(int index) {
return index * 2 + 1;
}
private int rightChild(int index) {
return index * 2 + 2;
}
2.4、添加元素
這裏的操作底層實現其實是上浮(SiftUp)操作。下面我們就來看看是如何上浮的。
- 向數組末尾添加一個元素,也就是向樹的最下角添加一個元素;
- 根據堆的性質,父親節點大於它的左右孩子節點,來進行替換操作
- 不斷進行第二步操作直到待添加節點小於它的父親節點
程序實現:
public void add(E e) {
data.addLast(e);
siftUp(data.getSize() - 1);
}
private void siftUp(int index) {
while (index > 0 && data.get(parent(index)).compareTo(data.get(index)) < 0) {
data.swap(parent(index), index);
index = (index - 1) / 2; //父親節點也就是待插入元素現在的位置
}
}
2.5、提取最大值
對於我們上面實現的最大堆,看得出來,最大值的地方存在於根節點的位置。也就是數組索引位0的位置。而且我們需要維護二叉堆的性質。
步驟:
- 用樹最後一個節點移動到根節點
- 判斷待下沉操作的節點必須大於孩子節點的最大值,如果大於那麼循環結束,否則替換孩子節點最大值和待下沉節點的位置。
提取最大值程序實現:
public E extractMax() {
E ret = findMax(); //查找最大值
data.swap(0, size() - 1); //移動最後一個節點到根節點
data.removeLast();
siftDown(0);
return ret;
}
// 下沉操作
private void siftDown(int index) {
while (leftChild(index) < size()) {
int j = leftChild(index);
if (j + 1 < data.getSize() && data.get(j + 1).compareTo(data.get(j)) > 0)
j++; //右孩子節點值大
if (data.get(index).compareTo(data.get(j)) >= 0)
break;
else
data.swap(index, j);
index = j;
}
}
2.6、查詢操作
查詢操作就是查找元素最大的值,這裏就是根節點位置,也就是索引爲 0 的位置。
程序實現:
public E findMax() {
if (isEmpty())
throw new IllegalArgumentException("Empty");
return data.get(0);
}
2.7、replace操作
replace替換操作主要包括:去除最大元素,放入一個新的元素。這其實是一個組合操作。但這裏我們準備封裝一下,並對其進行優化。
優化的方式就是在刪除元素這裏,如果我們分extraMax和add操作就需要兩次 O(log(N)) 級別的時間複雜度。在replace操作中,我們可以直接將待添加的元素元素替換到根節點的位置,然後在執行下沉操作就可以,這樣就是一次 O(log(N)) 級別的時間複雜度。
程序實現:
public E replace(E e) {
E ret = findMax();
data.set(0, e);
siftDown(0);
return ret;
}
2.8、Heapify數組堆化
操作就是將任意數組整理成堆的形狀。
具體的過程就是:
- 找到樹結構的倒數第一個非葉子節點;
- 不斷向上進行下沉SiftDown操作
初始位置的查詢就是最後一個節點的父親節點。
程序實現:
public MaxHeap(E[] arr) {
data = new Array<>(arr);
for (int i = parent(arr.length - 1); i >= 0; i--)
siftDown(i);
}
3、優先隊列的實現——基於二叉堆
具體的函數方法其實在最大堆已經映射過了。
優先隊列 | 最大堆 | |
---|---|---|
入隊操作 | enqueue | add |
出隊操作 | dequeue | extraMax |
查詢棧頂元素 | getFront | findMax |
@Override
public E dequeue() {
return maxHeap.extractMax();
}
@Override
public void enqueue(E e) {
maxHeap.add(e);
}
@Override
public E getFront() {
return maxHeap.findMax();
}
最後
更多精彩內容,大家可以轉到我的主頁:曲怪曲怪的主頁
或者關注我的微信公衆號:TeaUrn
源碼地址:可在公衆號內回覆 數據結構與算法源碼 即可獲得。