4.2 手寫Java PriorityQueue 核心原始碼
阿新 • • 發佈:2018-12-08
上一節介紹了PriorityQueue的原理,先來簡單的回顧一下 PriorityQueue 的原理
以最大堆為例來介紹
- PriorityQueue是用一棵完全二叉樹實現的。
- 不但是棵完全二叉樹,而且樹中的每個根節點都比它的左右兩個孩子節點元素大
- PriorityQueue底層是用陣列來儲存這棵完全二叉樹的。
如下圖,是一棵最大堆。
最大堆的刪除操作
刪除指的是刪除根元素,也就是圖中的100元素
刪除元素也就是 shiftDown
操作,向下翻
刪除一個根元素有以下步驟:
- 將100元素刪除,將最後一個元素12放到100的位置上,12成為根節點
- 找出 12 這個節點的左右兩個孩子節點中的最大的,也就是圖中的28節點
- 12 出 28節點進行比較,如果12比28小,則交換位置
- 12節點繼續重複2,3步驟,直到12比它的左右孩子節點都大則停止
最大堆插入一個節點
插入一個節點,也叫shiftUp
操作,向上翻
以插入一個節點23為例,步驟如下:
- 將23放到二叉樹的最後位置,也就是成為了9這個節點的左孩子
- 23與它的父節點進行比較,如果比它的父節點大,就交換位置
- 23這個節點繼續重要第2步驟,直到比它的父節點小方停止比較
程式碼實現
首先我們先上兩張圖
我們從左往右,按層序遍歷,分別存放到陣列的相應索引對應的位置上。
陣列的第0個索引位置我們不用,從索引為1的位置開始存放。
最終這個最大堆存放到陣列中,如下圖
首先實現一個最簡單的只存 int 型別的優先順序佇列 QPriorityQueueInt
完整程式碼如下:
//最大堆,只存放int,並且沒有擴容機制 public class QPriorityQueueInt { //預設底層資料大小為10 private static int DEFAULT_INIT_CAPACITY = 10; //底層陣列 private int[] queue; //節點的個數 private int size; public QPriorityQueueInt() { //因為陣列是從索引 1 的位置開始存放,索引為 0 的位置不用 //所以開闢空間的時候需要加 1 queue = new int[DEFAULT_INIT_CAPACITY + 1]; //當前陣列中節點的個數為0 size = 0; } //返回節點的個數 public int size() { return size; } //最大堆是否為空 public boolean isEmpty() { return size == 0; } //新增一個節點 public void add(int e) { //將元素存放到陣列當前最後一個位置上 queue[size + 1] = e; //個數需要加1 size++; //需要向上翻 shiftUp(size); } //向上翻,最大堆中的最後一個節點,不停的與父節點比較 //最大堆中父節點的索引是 k / 2 private void shiftUp(int k) { // k > 1 ,說明從第2個節點開始,因為如果只有一個節點的話,不需要比較了 // queue[k] > queue[k / 2] ,當前節點大於父節點 while (k > 1 && queue[k] > queue[k / 2]) { //交換位置 swap(k, k / 2); //把父節點的索引賦值給 k,然後繼續重複上面步驟 k = k / 2; } } //刪除最大堆中的節點 public int poll() { //把第1個位置的節點儲存起來 int result = queue[1]; //把最後一個節點放到第1個節點上面,成為整棵樹的根節點 queue[1] = queue[size]; //別忘了size 要減1 size--; //最後一個節點成為根節點後,就需要向下翻了 //向下翻的目的就是把大的節點翻上來 shiftDown(1); //返回第1個節點,也就是隊頭節點 return result; } //向下翻 private void shiftDown(int k) { //2 * k <= size ,2*k 是左孩子 //2 * k <= size ,是當前節點有左孩子 //至少有個左孩子才可以交換,因為是完全二叉樹,左孩子沒有,右孩子肯定沒有 while (2 * k <= size) { //比較左右兩個孩子節點,將大的節點的索引賦值給 j //左孩子索引 int j = 2 * k; //如果有右孩子,且 右孩子大於左孩子,將右孩子索引賦值給j if (j + 1 <= size && queue[j + 1] > queue[j]) { j = j + 1; } //現在 j 儲存的是左右孩子中較大的節點的索引 //比較當前節點和左右孩子中較大的節點 //如果比左右孩子中較大的節點還大,則不用向下翻了 if (queue[k] > queue[j]) { break; } //否則交換當前節點和左右孩子中較大的節點 swap(k, j); //把左右孩子中較大的節點的索引賦值給k,繼續向下翻 k = j; } } //交換兩個位置 private void swap(int i, int j) { int t = queue[i]; queue[i] = queue[j]; queue[j] = t; } }
下面是測試程式碼:
public static void main(String[] args) {
QPriorityQueueInt queue = new QPriorityQueueInt();
//隨便弄5個數入隊,數越大優先順序越大
//由於我們的QPriorityQueueInt預設只支援10個元素
//所以插入的節點個數不要多於10個
queue.add(3);
queue.add(5);
queue.add(1);
queue.add(8);
queue.add(7);
//列印
System.out.println(queue.poll());
System.out.println(queue.poll());
System.out.println(queue.poll());
System.out.println(queue.poll());
System.out.println(queue.poll());
}
輸出如下:
8
7
5
3
1
從輸出可以看出來,雖然7是最後入隊的,但是優先順序比較高,第二次就打印出來了。
優先順序佇列,同樣是用陣列實現。但是入隊的效率比單純的用陣列排序要高多了。
至於擴容機制,讀者可以自己查閱相關資源,自己實現。