上節介紹了堆的基本概念和算法,本節我們來探討堆在Java中的具體實現類 - PriorityQueue。
我們先從基本概念談起,然後介紹其用法,接著分析實現代碼,最後總結分析其特點。
基本概念
顧名思義,PriorityQueue是優先級隊列,它首先實現了隊列接口(Queue),與LinkedList類似,它的隊列長度也沒有限制,與一般隊列的區別是,它有優先級的概念,每個元素都有優先級,隊頭的元素永遠都是優先級最高的。
PriorityQueue內部是用堆實現的,內部元素不是完全有序的,不過,逐個出隊會得到有序的輸出。
雖然名字叫優先級隊列,但也可以將PriorityQueue看做是一種比較通用的實現了堆的性質的數據結構,可以用PriorityQueue來解決適合用堆解決的問題,下一節我們會來看一些具體的例子。
基本用法
Queue接口
PriorityQueue實現了Queue接口,我們在LinkedList一節介紹過Queue,為便於閱讀,這裡重復下其定義:
public interface Queue<E> extends Collection<E> { boolean add(E e); boolean offer(E e); E remove(); E poll(); E element(); E peek(); }
Queue擴展了Collection,主要操作有三個:
構造方法
PriorityQueue有多個構造方法,如下所示:
public PriorityQueue() public PriorityQueue(int initialCapacity) public PriorityQueue(int initialCapacity, Comparator<? super E> comparator) public PriorityQueue(Collection<? extends E> c) public PriorityQueue(PriorityQueue<? extends E> c) public PriorityQueue(SortedSet<? extends E> c)
PriorityQueue是用堆實現的,堆物理上就是數組,與ArrayList類似,PriorityQueue同樣使用動態數組,根據元素個數動態擴展,initialCapacity表示初始的數組大小,可以通過參數傳入。對於默認構造方法,initialCapacity使用默認值11。對於最後三個構造方法,它們接受一個已有的Collection,數組大小等於參數容器中的元素個數。
與TreeMap/TreeSet類似,為了保持一定順序,PriorityQueue要求,要麼元素實現Comparable接口,要麼傳遞一個比較器Comparator:
基本例子
我們來看個基本的例子:
Queue<Integer> pq = new PriorityQueue<>(); pq.offer(10); pq.add(22); pq.addAll(Arrays.asList(new Integer[]{ 11, 12, 34, 2, 7, 4, 15, 12, 8, 6, 19, 13 })); while(pq.peek()!=null){ System.out.print(pq.poll() + " "); }
代碼很簡單,添加元素,然後逐個從頭部刪除,與普通隊列不同,輸出是從小到大有序的:
2 4 6 7 8 10 11 12 12 13 15 19 22 34
如果希望是從大到小呢?傳遞一個逆序的Comparator,將第一行代碼替換為:
Queue<Integer> pq = new PriorityQueue<>(11, Collections.reverseOrder());
輸出就會變為:
34 22 19 15 13 12 12 11 10 8 7 6 4 2
任務隊列
我們再來看個例子,模擬一個任務隊列,定義一個內部類Task表示任務,如下所示:
static class Task { int priority; String name; public Task(int priority, String name) { this.priority = priority; this.name = name; } public int getPriority() { return priority; } public String getName() { return name; } }
Task有兩個實例變量,priority表示優先級,值越大優先級越高,name表示任務名稱。
Task沒有實現Comparable,我們定義一個單獨的靜態成員taskComparator表示比較器,如下所示:
private static Comparator<Task> taskComparator = new Comparator<Task>() { @Override public int compare(Task o1, Task o2) { if(o1.getPriority()>o2.getPriority()){ return -1; }else if(o1.getPriority()<o2.getPriority()){ return 1; } return 0; } };
下面來看任務隊列的示例代碼:
Queue<Task> tasks = new PriorityQueue<Task>(11, taskComparator); tasks.offer(new Task(20, "寫日記")); tasks.offer(new Task(10, "看電視")); tasks.offer(new Task(100, "寫代碼")); Task task = tasks.poll(); while(task!=null){ System.out.print("處理任務: "+task.getName() +",優先級:"+task.getPriority()+"\n"); task = tasks.poll(); }
代碼很簡單,就不解釋了,輸出任務按優先級排列:
處理任務: 寫代碼,優先級:100 處理任務: 寫日記,優先級:20 處理任務: 看電視,優先級:10
實現原理
理解了PriorityQueue的用法和特點,我們來看其具體實現代碼,從內部組成開始。
內部組成
內部有如下成員:
private transient Object[] queue; private int size = 0; private final Comparator<? super E> comparator; private transient int modCount = 0;
queue就是實際存儲元素的數組。size表示當前元素個數。comparator為比較器,可以為null。modCount記錄修改次數,在介紹第一個容器類ArrayList時已介紹過。
如何實現各種操作,且保持堆的性質呢?我們來看代碼,從基本構造方法開始。
基本構造方法
幾個基本構造方法的代碼是:
public PriorityQueue() { this(DEFAULT_INITIAL_CAPACITY, null); } public PriorityQueue(int initialCapacity) { this(initialCapacity, null); } public PriorityQueue(int initialCapacity, Comparator<? super E> comparator) { if (initialCapacity < 1) throw new IllegalArgumentException(); this.queue = new Object[initialCapacity]; this.comparator = comparator; }
代碼很簡單,就是初始化了queue和comparator。
下面介紹一些操作的代碼,大部分的算法和圖示,我們在上節已經介紹過了。
添加元素 (入隊)
代碼為:
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; }
offer方法的基本步驟為:
有兩步復雜一些,一步是grow,另一步是siftUp,我們來細看下。
grow方法的代碼為:
private void grow(int minCapacity) { int oldCapacity = queue.length; // Double size if small; else grow by 50% int newCapacity = oldCapacity + ((oldCapacity < 64) ? (oldCapacity + 2) : (oldCapacity >> 1)); // overflow-conscious code if (newCapacity - MAX_ARRAY_SIZE > 0) newCapacity = hugeCapacity(minCapacity); queue = Arrays.copyOf(queue, newCapacity); }
如果原長度比較小,大概就是擴展為兩倍,否則就是增加50%,使用Arrays.copyOf方法拷貝數組。
siftUp的基本思路我們在上節介紹過了,其實際代碼為:
private void siftUp(int k, E x) { if (comparator != null) siftUpUsingComparator(k, x); else siftUpComparable(k, x); }
根據是否有comparator分為了兩種情況,代碼類似,我們只看一種:
private void siftUpUsingComparator(int k, E x) { while (k > 0) { int parent = (k - 1) >>> 1; Object e = queue[parent]; if (comparator.compare(x, (E) e) >= 0) break; queue[k] = e; k = parent; } queue[k] = x; }
參數k表示插入位置,x表示新元素。k初始等於數組大小,即在最後一個位置插入。代碼的主要部分是:往上尋找x真正應該插入的位置,這個位置用k表示。
怎麼找呢?新元素(x)不斷與父節點(e)比較,如果新元素(x)大於等於父節點(e),則已滿足堆的性質,退出循環,k就是新元素最終的位置,否則,將父節點往下移(queue[k]=e),繼續向上尋找。這與上節介紹的算法和圖示是對應的。
查看頭部元素
代碼為:
public E peek() { if (size == 0) return null; return (E) queue[0]; }
就是返回第一個元素。
刪除頭部元素 (出隊)
代碼為:
public E poll() { if (size == 0) return null; int s = --size; modCount++; E result = (E) queue[0]; E x = (E) queue[s]; queue[s] = null; if (s != 0) siftDown(0, x); return result; }
返回結果result為第一個元素,x指向最後一個元素,將最後位置設置為null (queue[s] = null),最後調用siftDown將原來的最後元素x插入頭部並調整堆,siftDown的代碼為:
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) { int child = (k << 1) + 1; // assume left child is least Object c = queue[child]; int right = child + 1; 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; } queue[k] = key; }
k表示最終的插入位置,初始為0,x表示原來的最後元素。代碼的主要部分是:向下尋找x真正應該插入的位置,這個位置用k表示。
怎麼找呢?新元素key不斷與較小的孩子比較,如果小於等於較小的孩子,則已滿足堆的性質,退出循環,k就是最終位置,否則將較小的孩子往上移,繼續向下尋找。這與上節介紹的算法和圖示也是對應的。
解釋下其中的一些代碼:
查找元素
代碼為:
public boolean contains(Object o) { return indexOf(o) != -1; }
indexOf的代碼為:
private int indexOf(Object o) { if (o != null) { for (int i = 0; i < size; i++) if (o.equals(queue[i])) return i; } return -1; }
代碼很簡單,就是數組的查找。
根據值刪除元素
也可以根據值刪除元素,代碼為:
public boolean remove(Object o) { int i = indexOf(o); if (i == -1) return false; else { removeAt(i); return true; } }
先查找元素的位置i,然後調用removeAt進行刪除,removeAt的代碼為:
private E removeAt(int i) { assert i >= 0 && i < size; modCount++; int s = --size; if (s == i) // removed last element queue[i] = null; else { E moved = (E) queue[s]; queue[s] = null; siftDown(i, moved); if (queue[i] == moved) { siftUp(i, moved); if (queue[i] != moved) return moved; } } return null; }
如果是刪除最後一個位置,直接刪即可,否則移動最後一個元素到位置i並進行堆調整,調整有兩種情況,如果大於孩子節點,則向下調整,否則如果小於父節點則向上調整。
代碼先向下調整(siftDown(i, moved)),如果沒有調整過(queue[i] == moved),可能需向上調整,調用siftUp(i, moved)。
如果向上調整過,返回值為moved,其他情況返回null,這個主要用於正確實現PriorityQueue迭代器的刪除方法,迭代器的細節我們就不介紹了。
構建初始堆
如果從一個既不是PriorityQueue也不是SortedSet的容器構造堆,代碼為:
private void initFromCollection(Collection<? extends E> c) { initElementsFromCollection(c); heapify(); }
initElementsFromCollection的主要代碼為:
private void initElementsFromCollection(Collection<? extends E> c) { Object[] a = c.toArray(); if (a.getClass() != Object[].class) a = Arrays.copyOf(a, a.length, Object[].class); this.queue = a; this.size = a.length; }
主要是初始化queue和size。
heapify的代碼為:
private void heapify() { for (int i = (size >>> 1) - 1; i >= 0; i--) siftDown(i, (E) queue[i]); }
與之前算法一樣,heapify也在上節介紹過了,就是從最後一個非葉節點開始,自底向上合並構建堆。
如果構造方法中的參數是PriorityQueue或SortedSet,則它們的toArray方法返回的數組就是有序的,就滿足堆的性質,就不需要執行heapify了。
PriorityQueue特點分析
PriorityQueue實現了Queue接口,有優先級,內部是用堆實現的,這決定了它有如下特點:
小結
本節介紹了Java中堆的實現類PriorityQueue,它實現了隊列接口Queue,但按優先級出隊,我們介紹了其用法和實現代碼。
除了用作基本的優先級隊列,PriorityQueue還可以作為一種比較通用的數據結構,用於解決一些其他問題,讓我們在下一節繼續探討。
---------------
未完待續,查看最新文章,敬請關注微信公眾號“老馬說編程”(掃描下方二維碼),從入門到高級,深入淺出,老馬和你一起探索Java編程及計算機技術的本質。用心原創,保留所有版權。