小橙書閱讀指南(七)——優先隊列和索引優先隊列
算法描述:許多應用程序都需要按照順序處理任務,但是不一定要求他們全部有序,或是不一定要一次就將他們排序。很多情況下我們只需要處理當前最緊急或擁有最高優先級的任務就可以了。面對這樣的需求,優先隊列算法是一個不錯的選擇。
算法圖示:
算法解釋:上圖所展示的是最大優先隊列(大頂堆)的算法邏輯,在這個標準的二叉樹中,任意節點的元素都大於其葉子節點的元素。利用數組表示該二叉樹即Array[2]和Array[3]是Array[1]的葉子節點,Array[4]和Array[5]是Array[2]的葉子節點,Array[6]和Array[7]是Array[3]的葉子節點,以此類推。通過計算可知,有任意節點K,K/2是它的根節點,2*K和2*K+1是它的葉子節點。(註:Array[0]通常不使用)。於是對於任意節點的調整可以通過上浮(swim)或下稱(sink)來達到目的。
當有新的元素插入的時候,我們會首先把它分配在數組的尾部(Array[size+1]),然後自下而上的根據子節點到根節點的路徑不斷上浮到合適的位置。
當最大的元素被取走以後,我們會首先把數組尾部Array[size])的元素放到數組的頭部(Array[1]),然後自上而下的從根節點下稱到子節點的合適位置。
數組和二叉樹互換的算法圖例:
Java代碼示例:
package algorithms.sorting.pq; import algorithms.common.ArraysGenerator; /** * 最大優先隊列(大頂堆) * @param <T> */ publicclass MaxPriorityQueue<T extends Comparable<T>> { private T[] heap; private int size = 0; public MaxPriorityQueue(int maxSize) { heap = (T[]) new Comparable[maxSize + 1]; } /** * 判單是否為空 * @return {@code true}當前隊列未空 * {@code false}否則不為空 */ publicboolean isEmpty() { return size == 0; } /** * 插入新元素至末尾,並上浮至合適的位置 * @param value */ public void insert(T value) { heap[++size] = value; swim(size); } /** * 移除堆頂元素並調整堆 * @return T 返回最大元素 */ public T remove() { T maxValue = heap[1]; // 堆頂的元素和堆底元素交換位置,並減少數組長度 exch(1, size--); heap[size + 1] = null; sink(1); return maxValue; } // 元素上浮 private void swim(int k) { // 下層元素如果大於上層元素且該元素非頂層元素時,循環上浮 while (k > 1 && heap[k / 2].compareTo(heap[k]) < 0) { exch(k / 2, k); k = k / 2; } } private void sink(int k) { while (2 * k <= size) { int leafIndex = 2 * k; // 選擇兩個子節點中更大的那個元素作為交換目標 if (leafIndex < size && heap[leafIndex].compareTo(heap[leafIndex + 1]) < 0) { leafIndex++; } if (heap[k].compareTo(heap[leafIndex]) < 0) { exch(k, leafIndex); } else { // 如果本輪比較未發生元素交換則不用繼續下沈 break; } k = leafIndex; } } private void exch(int i, int j) { T tmp = heap[i]; heap[i] = heap[j]; heap[j] = tmp; } @Override public String toString() { StringBuffer buffer = new StringBuffer(); buffer.append("["); for (int i = 1; i <= size; ++i) { buffer.append(heap[i]); buffer.append(","); } return buffer.deleteCharAt(buffer.length() - 1).append("]").toString(); } public static void main(String[] args) { MaxPriorityQueue maxPriorityQueue = new MaxPriorityQueue(100); Integer[] array = ArraysGenerator.generate(10, 1, 100); for (int i = 0; i < 10; ++i) { maxPriorityQueue.insert(array[i]); } System.out.println(maxPriorityQueue); while(!maxPriorityQueue.isEmpty()) { System.out.println(maxPriorityQueue.remove()); } } }
Qt/C++代碼示例:
// MaxPriorityQueue.h class QString; class MaxPriorityQueue { public: MaxPriorityQueue(); ~MaxPriorityQueue(); bool isEmpty(); void insert(int val); int remove(); QString toString(); private: void increase(); void decrease(); void swim(int k); void sink(int k); void exch(int i, int j); int size; int maxSize; int *heap = 0; static int initialCapacity; }; // MaxPriorityQueue.cpp #include "maxpriorityqueue.h" #include <QDebug> #include <QString> int MaxPriorityQueue::initialCapacity = 16; MaxPriorityQueue::MaxPriorityQueue() :maxSize(initialCapacity), size(0) { heap = new int[maxSize]; } MaxPriorityQueue::~MaxPriorityQueue() { if (heap) { delete heap; } } bool MaxPriorityQueue::isEmpty() { return size == 0; } void MaxPriorityQueue::insert(int val) { if (size >= maxSize) { increase(); } heap[++size] = val; swim(size); } int MaxPriorityQueue::remove() { int maxValue = heap[1]; exch(1, size--); sink(1); if (size < maxSize / 2 && maxSize > initialCapacity) { decrease(); } return maxValue; } QString MaxPriorityQueue::toString() { QString buf; buf.append("["); for (int i = 1; i < size; ++i) { buf.append(QString::number(heap[i])); buf.append(","); } return buf.left(buf.length() - 1).append("]"); } void MaxPriorityQueue::increase() { maxSize *= 2; int *newheap = new int[maxSize]; for (int i = 1; i <= size; ++i) { newheap[i] = heap[i]; } heap = newheap; } void MaxPriorityQueue::decrease() { maxSize /= 2; int *newheap = new int[maxSize]; for (int i = 1; i <= size; ++i) { newheap[i] = heap[i]; } heap = newheap; } void MaxPriorityQueue::swim(int k) { while (k > 1 && heap[k / 2] < heap[k]) { exch(k / 2, k); k /= 2; } } void MaxPriorityQueue::sink(int k) { while (2 * k <= size) { int j = 2 * k; if (j < size && heap[j] < heap[j + 1]) { j++; } if (heap[k] < heap[j]) { exch(k, j); } else { break; } k = j; } } void MaxPriorityQueue::exch(int i, int j) { int temp = heap[i]; heap[i] = heap[j]; heap[j] = temp; }
C++的代碼增加了動態數組擴容的實現。
算法總結:上面提供的是最大優先隊列算法,適合獲取最大優先值的應用。如果需要獲取最小值則需要構造最小優先隊列,即在完全二叉樹的任意節點都小於其子節點。但是,優先隊列存在一個缺點,即我們無法自由訪問隊列中的元素並且也無法提供修改的操作。試想在一個多任務的應用系統中,我們對已經加入處理隊列的任務需要調整優先級。這就是索引優先隊列的由來。
算法圖示:
算法分析:索引優先隊列對於剛剛接觸算法的同學是非常難的,主要是在這個數據結構中我們引入了三個平行數組。觀察上圖,indexHeap是索引和元素的對應數組,由於我們需要隨時根據索引(indexHeap數組的下標)找到對應的元素,所以這個數組中的元素實際是不會移動的。因此我們就需要引入新的數pq。註意,pq是三個數組中唯一的緊密數組(其余的兩個都是稀松數組)。pq負責保存元素排序後的索引順序,因此pq數組可以和完全二叉樹相互轉換。
現在假設我們需要維護3=A這對映射關系,需要修改成3=T:indexHeap[3]=T。可是接下來就有點麻煩了,我們不知道A在樹中的具體位置。因此我們還需要再引入一個數組用來保存每一個索引在二叉樹中的位置(否則就只能通過遍歷的方法),qp[pq[key]]=key。
Java算法示例:
package algorithms.sorting.pq; /** * 最小索引優先數組 * * @param <T> */ public class IndexMinPriorityQueue<T extends Comparable<T>> { private T[] indexHeap; private int[] pq; private int[] qp; private int size; public IndexMinPriorityQueue(int maxSize) { size = 0; indexHeap = (T[]) new Comparable[maxSize + 1]; pq = new int[maxSize + 1]; qp = new int[maxSize + 1]; for (int i = 1; i <= maxSize; ++i) { qp[i] = -1; } } /** * 插入新的索引和元素 * * @param key * @param value */ public void insert(int key, T value) { size++; indexHeap[key] = value; pq[size] = key; qp[key] = size; swim(size); } public void change(int key, T value) { if (contains(key)) { indexHeap[key] = value; swim(qp[key]); sink(qp[key]); } } /** * 移除堆頂的最小元素並返回該元素 * * @return */ public T remove() { int minKey = pq[1]; exch(1, size--); sink(1); qp[minKey] = -1; return indexHeap[minKey]; } /** * 移除指定索引的元素,並返回該元素 * * @param key * @return */ public T remove(int key) { int pos = qp[key]; exch(pos, size--); swim(pos); sink(pos); qp[key] = -1; return indexHeap[key]; } public int delete() { int minKey = pq[1]; exch(1, size--); sink(1); qp[minKey] = -1; return minKey; } public T get(int key) { return indexHeap[key]; } public boolean contains(int key) { return qp[key] != -1; } public boolean isEmpty() { return size == 0; } private void swim(int k) { while (k > 1 && indexHeap[pq[k / 2]].compareTo(indexHeap[pq[k]]) > 0) { exch(k / 2, k); k /= 2; } } private void sink(int k) { while (2 * k <= size) { int leafIndex = 2 * k; // 當前節點存在兩個葉子節點 且 右葉子節點 小於 左葉子節點 以右葉子節點作為比較目標 if (leafIndex < size && indexHeap[pq[leafIndex + 1]].compareTo(indexHeap[pq[leafIndex]]) < 0) { leafIndex++; } if (indexHeap[pq[k]].compareTo(indexHeap[pq[leafIndex]]) > 0) { exch(k, leafIndex); } else { break; } k = leafIndex; } } private void exch(int i, int j) { int temp = pq[i]; pq[i] = pq[j]; pq[j] = temp; qp[pq[i]] = i; qp[pq[j]] = j; } public static void main(String[] args) { IndexMinPriorityQueue<String> indexMinPriorityQueue = new IndexMinPriorityQueue<>(50); indexMinPriorityQueue.insert(5, "C"); indexMinPriorityQueue.insert(7, "A"); indexMinPriorityQueue.insert(2, "Z"); indexMinPriorityQueue.insert(6, "F"); indexMinPriorityQueue.change(6, "X"); while (!indexMinPriorityQueue.isEmpty()) { System.out.println(indexMinPriorityQueue.remove()); } } }
算法總結:qp可能是索引優先隊列最難理解的部分。理解索引優先隊列對於深入理解數據庫的本地數據保存非常重要。希望對大家能有所幫助。
相關鏈接:
Algorithms for Java
Algorithms for Qt
小橙書閱讀指南(七)——優先隊列和索引優先隊列