深入Java集合系列之五 PriorityQueue
分享一下我老師大神的人工智慧教程!零基礎,通俗易懂!http://blog.csdn.net/jiangjunshow
也歡迎大家轉載本篇文章。分享知識,造福人民,實現我們中華民族偉大復興!
前言
今天繼續來分析一下PriorityQueue的原始碼實現,實際上在Java集合框架中,還有ArrayDeque(一種雙端佇列),這裡就來分析一下PriorityQueue的原始碼。PriorityQueue也叫優先佇列,所謂優先佇列指的就是每次從優先佇列中取出來的元素要麼是最大值(最大堆),要麼是最小值(最小堆)。我們知道,佇列是一種先進先出的資料結構,每次從隊頭出隊(移走一個元素),從隊尾插入一個元素(入隊),可以類比生活中排隊的例子就好理解了。
PriorityQueue說明
PriorityQueue底層實現的資料結構是“堆”,堆具有以下兩個性質:
任意一個節點的值總是不大於(最大堆)或者不小於(最小堆)其父節點的值;堆是一棵完全二叉樹
而優先佇列在Java中的使用的最小堆,意味著每次從佇列取出的都是最小的元素,為了更好理解原始碼,有必要了解堆的一些數字規律。我們知道無論堆還是其他資料結構,最終都要採用程式語言加以實現,在Java中實現堆這種資料結構歸根結底採用的還是陣列,但這個陣列有點特殊,每個陣列中的元素的左右孩子節點也存在該陣列中,對於任意一個數組下標i,滿足:
左孩子節點的下標left(i)=2*i,右孩子節點right(i) = 2*i+1
這樣的話就可以把資料結構中複雜的樹形元素放在簡單的陣列中了,只要按照上面的規律就可以很方便找到任意節點的左右孩子節點。解決完元素的儲存問題還要把陣列中的元素還原為堆,這就是建堆的過程,後面的原始碼也是基於同樣的思想。以每次向堆中新增一個元素為例,由於使用陣列儲存,新新增的元素的下標是陣列的最後一個下標值,對應到堆中就是堆中最後一個葉子節點,由於新新增元素破壞了堆的性質,所以需要對新的新增的元素做調整,使其移動到正確的位置,使得堆重新符合堆的性質。
那麼問題來了,從哪個位置開始建堆呢?我們注意到最後一個節點的父節點是擁有孩子節點的下標最大的節點,因為葉子節點沒有孩子節點,基於這點考慮我們選擇最後一個節點的父節點作為建堆的起點,對與每個節點來說,接著要做的就是調整節點的位置了,這是實現最大堆或者最小堆的關鍵,為了能形象說明建堆的過程,請參看下面的示意圖:
下面以元素{6,5,3,1,4,8,7}為例,說明建堆的具體過程:
如果你覺得這個過程太單調,你可以參考下面的動態圖,不過下面這個動態圖還包括堆排序的內容,只需要關注前面建堆哪個動態圖就好了。
好了,現在你應該瞭解了建堆的具體過程,下面的關鍵就是新增元素以及移除元素了,為了結合Priority的原始碼說明,我把這部分的內容留到原始碼分析了。
原始碼分析
入隊
在分析入隊之前,我們來看看Java原始碼是怎麼建堆的?
//從插入最後一個元素的父節點位置開始建堆private void heapify() { for (int i = (size >>> 1) - 1; i >= 0; i--) siftDown(i, (E) queue[i]);}//在位置k插入元素x,為了保持最小堆的性質會不斷調整節點位置private void siftDown(int k, E x) { if (comparator != null) //使用插入元素的實現的比較器調整節點位置 siftDownUsingComparator(k, x); else //使用預設的比較器(按照自然排序規則)調整節點的位置 siftDownComparable(k, x);}//具體實現調整節點位置的函式private void siftDownComparable(int k, E x) { Comparable<? super E> key = (Comparable<? super E>)x; // 計算非葉子節點元素的最大位置 int half = size >>> 1; // loop while a non-leaf //如果不是葉子節點則一直迴圈 while (k < half) { //得到k位置節點左孩子節點,假設左孩子比右孩子更小 int child = (k << 1) + 1; // assume left child is least //儲存左孩子節點值 Object c = queue[child]; //右孩子節點的位置 int right = child + 1; //把左右孩子中的較小值儲存在變數c中 if (right < size && ((Comparable<? super E>) c).compareTo((E) queue[right]) > 0) c = queue[child = right]; //如果要插入的節點值比其父節點更小,則交換兩個節點的值 if (key.compareTo((E) c) <= 0) break; queue[k] = c; k = child; } //迴圈結束,k是葉子節點 queue[k] = key;}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
- 31
- 32
- 33
- 34
- 35
- 36
- 37
- 38
- 39
- 40
ok,下面看看如何在一個最小堆中新增一個元素:
public boolean add(E e) { //呼叫offer函式 return offer(e);}//siftUp之前的程式碼主要確認佇列的容量不發生溢位,並儲存佇列中的元素個數以及發生結構//性修改的次數public boolean offer(E e) { if (e == null) throw new NullPointerException(); modCount++; int i = size; if (i >= queue.length) grow(i + 1); size = i + 1; if (i == 0) queue[0] = e; else //具體執行新增元素的函式 siftUp(i, e); return true;}//呼叫不同的比較器調整元素的位置private void siftUp(int k, E x) { if (comparator != null) siftUpUsingComparator(k, x); else siftUpComparable(k, x);}//使用預設的比較器調整元素的位置private void siftUpComparable(int k, E x) { Comparable<? super E> key = (Comparable<? super E>) x; while (k > 0) { int parent = (k - 1) >>> 1; //儲存父節點的值 Object e = queue[parent]; //使用compareTo方法,如果要插入的元素小於父節點的位置則交換兩個節點的位置 if (key.compareTo((E) e) >= 0) break; queue[k] = e; k = parent; } queue[k] = key;}//呼叫實現的比較器進行元素位置的調整,總的過程和上面一致,就是比較的方法不同private void siftUpUsingComparator(int k, E x) { while (k > 0) { int parent = (k - 1) >>> 1; Object e = queue[parent]; //這裡是compare方法 if (comparator.compare(x, (E) e) >= 0) break; queue[k] = e; k = parent; } queue[k] = x;}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
- 31
- 32
- 33
- 34
- 35
- 36
- 37
- 38
- 39
- 40
- 41
- 42
- 43
- 44
- 45
- 46
- 47
- 48
- 49
- 50
- 51
- 52
- 53
- 54
- 55
為了更好理解上面程式碼的執行過程,請參看下面的示意圖:
出隊
出隊就是從佇列中移除一個元素,我們看看在原始碼中實現:
private E removeAt(int i) { assert i >= 0 && i < size; modCount++; //s是佇列的隊頭,對應到陣列中就是最後一個元素 int s = --size; //如果要移除的位置是最後一個位置,則把最後一個元素設為null if (s == i) // removed last element queue[i] = null; else { //儲存待刪除的節點元素 E moved = (E) queue[s]; queue[s] = null; //先把最後一個元素和i位置的元素交換,之後執行下調方法 siftDown(i, moved); //如果執行下調方法後位置沒變,說明該元素是該子樹的最小元素,需要執行上調方//法,保持最小堆的性質 if (queue[i] == moved) {//位置沒變 siftUp(i, moved); //執行上調方法 if (queue[i] != moved)//如果上調後i位置發生改變則返回該元素 return moved; } } return null;}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
在上面的程式碼上調方法與下調方法只會執行其中的一個,參看下面需要執行下調方法的示意圖:
這是需要執行上調方法的示意圖:
PriorityQueue小結
經過上面的原始碼的分析,對PriorityQueue的總結如下:
- 時間複雜度:remove()方法和add()方法時間複雜度為O(logn),remove(Object obj)和contains()方法需要O(n)時間複雜度,取隊頭則需要O(1)時間
- 在初始化階段會執行建堆函式,最終建立的是最小堆,每次出隊和入隊操作不能保證佇列元素的有序性,只能保證隊頭元素和新插入元素的有序性,如果需要有序輸出佇列中的元素,則只要呼叫Arrays.sort()方法即可
- 可以使用Iterator的迭代器方法輸出佇列中元素
- PriorityQueue是非同步的,要實現同步需要呼叫java.util.concurrent包下的PriorityBlockingQueue類來實現同步
- 在佇列中不允許使用null元素