1. 程式人生 > >4.2 手寫Java PriorityQueue 核心原始碼

4.2 手寫Java PriorityQueue 核心原始碼

上一節介紹了PriorityQueue的原理,先來簡單的回顧一下 PriorityQueue 的原理

以最大堆為例來介紹

  1. PriorityQueue是用一棵完全二叉樹實現的。
  2. 不但是棵完全二叉樹,而且樹中的每個根節點都比它的左右兩個孩子節點元素大
  3. PriorityQueue底層是用陣列來儲存這棵完全二叉樹的。

如下圖,是一棵最大堆。

最大堆的刪除操作
刪除指的是刪除根元素,也就是圖中的100元素
刪除元素也就是 shiftDown 操作,向下翻
刪除一個根元素有以下步驟:

  1. 將100元素刪除,將最後一個元素12放到100的位置上,12成為根節點
  2. 找出 12 這個節點的左右兩個孩子節點中的最大的,也就是圖中的28節點
  3. 12 出 28節點進行比較,如果12比28小,則交換位置
  4. 12節點繼續重複2,3步驟,直到12比它的左右孩子節點都大則停止

最大堆插入一個節點
插入一個節點,也叫shiftUp操作,向上翻
以插入一個節點23為例,步驟如下:

  1. 將23放到二叉樹的最後位置,也就是成為了9這個節點的左孩子
  2. 23與它的父節點進行比較,如果比它的父節點大,就交換位置
  3. 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是最後入隊的,但是優先順序比較高,第二次就打印出來了。
優先順序佇列,同樣是用陣列實現。但是入隊的效率比單純的用陣列排序要高多了。

至於擴容機制,讀者可以自己查閱相關資源,自己實現。