1.優先級隊列介紹
1.1 優先級隊列
有時在調度任務時,我們會想要先處理優先級更高的任務。例如,對於同一個櫃台,在決定隊列中下一個服務的用戶時,總是傾向於優先服務VIP用戶,而讓普通用戶等待,即使普通的用戶是先加入隊列的。
優先級隊列和普通的先進先出FIFO的隊列類似,最大的不同在於,優先級隊列中優先級最高的元素總是最先出隊的,而不是遵循先進先出的順序。
1.2 堆
優先級隊列的接口要求很簡單。從邏輯上來說,向量、鏈表或者平衡二叉搜索樹等數據結構都可用於實現優先級隊列。但考慮到時間和空間的效率,就必須仔細斟酌和考量了。而一種被稱為堆的數據結構非常適合實現優先級隊列。’
堆和二叉搜索樹類似,存儲的元素在邏輯上是按照層次排放的,在全局任意地方其上層元素優先級大於下層元素,這一順序性也被稱為堆序性,而其中優先級最大的元素被放在最高的層級(大頂堆)。和二叉搜索樹的排序方式不同的是,堆中元素的順序並不是完全的排序,而只是維護了一種偏序關系,被稱為堆序性。在這種偏序關系下,元素之間的順序性比較疏散,維護堆序性的代價比較低,因而在實現優先級隊列時,堆的效率要高於平衡二叉搜索樹。
1.3 完全二叉堆
完全二叉堆是堆的一種,其元素在邏輯上是以完全二叉樹的形式存放的,但實際卻是存儲在向量(數組)中的。在這里,我們使用完全二叉堆來實現優先級隊列。
2.優先級隊列ADT接口
/** * 優先級隊列 ADT接口 */ public interface PriorityQueue <E>{ /** * 插入新數據 * @param newData 新數據 * */ void insert(E newData); /** * 獲得優先級最大值(窺視) 不刪數據 * @return 當前優先級最大的數據 * */ E peekMax(); /** * 獲得並且刪除當前優先級最大值 * @return 被刪除的 當前優先級最大的數據 */ E popMax(); /** * 獲得當前優先級隊列 元素個數 * @return 當前優先級隊列 元素個數 * */ int size(); /** * 是否為空 * @return true 隊列為空 * false 隊列不為空 * */ boolean isEmpty(); }
3.完全二叉堆實現細節
3.1 基礎屬性
完全二叉堆內部使用之前封裝好的向量作為基礎。和二叉搜索樹類似,用戶同樣可以通過傳入Comparator比較器來指定堆中優先級大小比較的邏輯。
public class CompleteBinaryHeap<E> implements PriorityQueue<E>{ /** * 內部向量 * */ private ArrayList<E> innerArrayList; /** * 比較邏輯 * */ private final Comparator<E> comparator; /** * 當前堆的邏輯大小 * */ private int size; }
構造方法:
/** * 無參構造函數 * */ public CompleteBinaryHeap() { this.innerArrayList = new ArrayList<>(); this.comparator = null; } /** * 指定初始容量的構造函數 * @param defaultCapacity 指定的初始容量 * */ public CompleteBinaryHeap(int defaultCapacity){ this.innerArrayList = new ArrayList<>(defaultCapacity); this.comparator = null; } /** * 指定初始容量的構造函數 * @param comparator 指定的比較器邏輯 * */ public CompleteBinaryHeap(Comparator<E> comparator){ this.innerArrayList = new ArrayList<>(); this.comparator = comparator; } /** * 指定初始容量和比較器的構造函數 * @param defaultCapacity 指定的初始容量 * @param comparator 指定的比較器邏輯 * */ public CompleteBinaryHeap(int defaultCapacity, Comparator<E> comparator) { this.innerArrayList = new ArrayList<>(defaultCapacity); this.comparator = comparator; } /** * 將指定數組 轉換為一個完全二叉堆 * @param array 指定的數組 * */ public CompleteBinaryHeap(E[] array){ this.innerArrayList = new ArrayList<>(array); this.comparator = null; this.size = array.length; // 批量建堆 heapify(); } /** * 將指定數組 轉換為一個完全二叉堆 * @param array 指定的數組 * @param comparator 指定的比較器邏輯 * */ public CompleteBinaryHeap(E[] array, Comparator<E> comparator){ this.innerArrayList = new ArrayList<>(array); this.comparator = comparator; this.size = array.length; // 批量建堆 heapify(); }
3.2 輔助方法
由於完全二叉堆在邏輯上等價於一顆完全二叉樹,但實際上卻采用了一維的向量數據結構來存儲元素。因而我們需要實現諸如getParentIndex、getLeftChildIndex、getRightChildIndex等方法來進行完全二叉樹和向量表示方法的轉換。
這里,定義了一些私有方法來封裝常用的邏輯,用以簡化代碼。
/** * 獲得邏輯上 雙親節點下標 * @param currentIndex 當前下標 * */ private int getParentIndex(int currentIndex){ return (currentIndex - 1)/2; } /** * 獲得邏輯上 左孩子節點下標 * @param currentIndex 當前下標 * */ private int getLeftChildIndex(int currentIndex){ return (currentIndex * 2) + 1; } /** * 獲得邏輯上 右孩子節點下標 * @param currentIndex 當前下標 * */ private int getRightChildIndex(int currentIndex){ return (currentIndex + 1) * 2; } /** * 獲得末尾下標 * */ private int getLastIndex(){ return this.size - 1; } /** * 獲得最后一個非葉子節點下標 * */ private int getLastInternal(){ return (this.size()/2) - 1; } /** * 交換向量中兩個元素位置 * @param a 某一個元素的下標 * @param b 另一個元素的下標 * */ private void swap(int a, int b){ // 現暫存a、b下標元素的值 E aData = this.innerArrayList.get(a); E bData = this.innerArrayList.get(b); // 交換位置 this.innerArrayList.set(a,bData); this.innerArrayList.set(b,aData); } /** * 進行比較 * */ @SuppressWarnings("unchecked") private int compare(E t1, E t2){ // 迭代器不存在 if(this.comparator == null){ // 依賴對象本身的 Comparable,可能會轉型失敗 return ((Comparable) t1).compareTo(t2); }else{ // 通過迭代器邏輯進行比較 return this.comparator.compare(t1,t2); } }
3.3 插入和上濾
當新元素插入完全二叉堆時,我們直接將其插入向量末尾(堆底最右側),此時新元素的優先級可能會大於其雙親元素甚至祖先元素,破壞了堆序性,因此我們需要對插入的新元素進行一次上濾操作,使完全二叉堆恢復堆序性。由於堆序性只和雙親和孩子節點相關,因此堆中新插入元素的非祖先元素的堆序性不會受到影響,上濾只是一個局部性的行為。
上濾操作
上濾的元素不斷的和自己的雙親節點進行優先級的比較:
1. 如果上濾元素的優先級較大,則與雙親節點交換位置,繼續向上比較。
2. 如果上濾元素的優先級較小(等於),堆序性恢復,終止比較,結束上濾操作。
3. 特別的,當上濾的元素被交換到樹根節點時(向量下標第0位),此時由於上濾的元素是堆中的最大元素,終止上濾操作。
上濾操作的時間復雜度:
上濾操作時,上濾元素進行比較的次數正比於上濾元素的深度。因此,上濾操作的時間復雜度為O(logN)。
@Override public void insert(E newData) { // 先插入新數據到 向量末尾 this.innerArrayList.add(newData); // 獲得向量末尾元素下標 int lastIndex = getLastIndex(); // 對向量末尾元素進行上濾,以恢復堆序性 siftUp(lastIndex); } /** * 上濾操作 * @param index 需要上濾的元素下標 * */ private void siftUp(int index){ while(index >= 0){ // 獲得當前節點 int parentIndex = getParentIndex(index); E currentData = this.innerArrayList.get(index); E parentData = this.innerArrayList.get(parentIndex); // 如果當前元素 大於 雙親元素 if(compare(currentData,parentData) > 0){ // 交換當前元素和雙親元素的位置 swap(index,parentIndex); // 繼續向上迭代 index = parentIndex; }else{ // 當前元素沒有違反堆序性,直接返回 return; } } }
3.4 刪除和下濾
當優先級隊列中極值元素出隊時,需要在滿足堆序性的前提下,選出新的極值元素。
我們簡單的將當前向量末尾的元素放在堆頂,堆序性很有可能被破壞了。此時,我們需要對當前的堆頂元素進行一次下濾操作,使得整個完全二叉堆恢復堆序性。
下濾操作:
下濾的元素不斷的和自己的左、右孩子節點進行優先級的比較:
1. 雙親節點最大,堆序性恢復,終止下濾。
2. 左孩子節點最大,當前下濾節點和自己的左孩子節點交換,繼續下濾。
3. 右孩子節點最大,當前下濾節點和自己的右孩子節點交換,繼續下濾。
4. 特別的,當下濾的元素抵達堆底時(成為葉子節點),堆序性已經恢復,終止下濾。
下濾操作時間復雜度:
下濾操作時,下濾元素進行比較的次數正比於下濾元素的高度。因此,下濾操作的時間復雜度為O(logN)。
@Override public E popMax() { if(this.innerArrayList.isEmpty()){ throw new CollectionEmptyException("當前完全二叉堆為空"); } // 將當前向量末尾的元素和堆頂元素交換位置 int lastIndex = getLastIndex(); swap(0,lastIndex); // 暫存被刪除的最大元素(之前的堆頂最大元素被放到了向量末尾) E max = this.innerArrayList.get(lastIndex); this.size--; // 對當前堆頂元素進行下濾,以恢復堆序性 siftDown(0); return max; } /** * 下濾操作 * @param index 需要下濾的元素下標 * */ private void siftDown(int index){ int size = this.size(); // 葉子節點不需要下濾 int half = size >>> 1; while(index < half){ int leftIndex = getLeftChildIndex(index); int rightIndex = getRightChildIndex(index); if(rightIndex < size){ // 右孩子存在 (下標沒有越界) E leftData = this.innerArrayList.get(leftIndex); E rightData = this.innerArrayList.get(rightIndex); E currentData = this.innerArrayList.get(index); // 比較左右孩子大小 if(compare(leftData,rightData) >= 0){ // 左孩子更大,比較雙親和左孩子 if(compare(currentData,leftData) >= 0){ // 雙親最大,終止下濾 return; }else{ // 三者中,左孩子更大,交換雙親和左孩子的位置 swap(index,leftIndex); // 繼續下濾操作 index = leftIndex; } }else{ // 右孩子更大,比較雙親和右孩子 if(compare(currentData,rightData) >= 0){ // 雙親最大,終止下濾 return; }else{ // 三者中,右孩子更大,交換雙親和右孩子的位置 swap(index,rightIndex); // 繼續下濾操作 index = rightIndex; } } }else{ // 右孩子不存在 (下標越界) E leftData = this.innerArrayList.get(leftIndex); E currentData = this.innerArrayList.get(index); // 當前節點 大於 左孩子 if(compare(currentData,leftData) >= 0){ // 終止下濾 return; }else{ // 交換 左孩子和雙親的位置 swap(index,leftIndex); // 繼續下濾操作 index = leftIndex; } } } }
3.5 批量元素建堆
有時,我們需要將一個無序的元素集合數組轉換成一個完全二叉堆,這一操作被稱為批量建堆。
一個朴素的想法是:將無序集合中的元素依次插入一個空的完全二叉堆,對每一個新插入的元素進行上濾操作。使用上濾操作實現的對N個元素進行批量建堆的算法,其時間復雜度為O(n.logn),比較直觀。
但還存在一種效率更加高效的批量建堆算法,是以下濾操作為基礎實現的,被稱為Floyd建堆算法。下濾操作可以看做是將兩個較小的堆合並為一個更大堆的過程(單個元素可以被視為一個最小的堆),通過從底到高不斷的下濾操作,原本無序的元素集合將通過不斷的合並建立較小的堆,最終完成整個集合的建堆過程。
Floyd建堆算法的時間復雜度的證明較為復雜,其時間復雜度比起以上濾為基礎的朴素算法效率高一個數量級,為O(n)。
簡單的一種解釋是:在完全二叉樹中,低層元素的數量要遠遠少於高層的數量。高層元素的高度較高而深度較低;底層元素的高度較低而深度較高。由於上濾操作的時間復雜度正比於高度,對於存在大量底層元素的完全二叉堆很不友好,使得基於上濾的批量建堆算法效率較低。
/** * 批量建堆(將內部數組轉換為完全二叉堆) * */ private void heapify(){ // 獲取下標最大的 內部非葉子節點 int lastInternalIndex = getLastInternal(); // Floyd建堆算法 時間復雜度"O(n)" // 從lastInternalIndex開始向前遍歷,對每一個元素進行下濾操作,從小到大依次合並 for(int i=lastInternalIndex; i>=0; i--){ siftDown(i); } }
4.堆排序
堆排序主要分為兩步進行:
1. 堆排序首先將傳入的數組轉化為一個堆(floyd建堆算法,時間復雜度O(n))。
2. 和選擇排序類似,堆排序每次都從未排序的區間中選擇出一個極值元素置入已排序區域,在堆中極值元素就是堆頂元素,可以通過popMax方法(時間復雜度O(logN))獲得。從數組末尾向前遍歷,循環往復直至排序完成,總的時間復雜度為O(N logN)。
綜上所述,堆排序的漸進時間復雜度為O(N logN)。同時由於堆排序能夠在待排序數組中就地的進行排序,因此空間效率很高,空間復雜度為(O(1))。
public static <T> void heapSort(T[] array){ CompleteBinaryHeap<T> completeBinaryHeap = new CompleteBinaryHeap<>(array); for(int i=array.length-1; i>=0; i--){ array[i] = completeBinaryHeap.popMax(); } }
5.完整代碼
優先級隊列ADT接口:

1 /** 2 * 優先級隊列 ADT接口 3 */ 4 public interface PriorityQueue <E>{ 5 6 /** 7 * 插入新數據 8 * @param newData 新數據 9 * */ 10 void insert(E newData); 11 12 /** 13 * 獲得優先級最大值(窺視) 14 * @return 當前優先級最大的數據 15 * */ 16 E peekMax(); 17 18 /** 19 * 獲得並且刪除當前優先級最大值 20 * @return 被刪除的 當前優先級最大的數據 21 */ 22 E popMax(); 23 24 /** 25 * 獲得當前優先級隊列 元素個數 26 * @return 當前優先級隊列 元素個數 27 * */ 28 int size(); 29 30 /** 31 * 是否為空 32 * @return true 隊列為空 33 * false 隊列不為空 34 * */ 35 boolean isEmpty(); 36 }
完全二叉堆實現:

/** * 完全二叉堆 實現優先級隊列 */ public class CompleteBinaryHeap<E> implements PriorityQueue<E>{ // =========================================成員屬性=========================================== /** * 內部向量 * */ private ArrayList<E> innerArrayList; /** * 比較邏輯 * */ private final Comparator<E> comparator; /** * 當前堆的邏輯大小 * */ private int size; // ===========================================構造函數======================================== /** * 無參構造函數 * */ public CompleteBinaryHeap() { this.innerArrayList = new ArrayList<>(); this.comparator = null; } /** * 指定初始容量的構造函數 * @param defaultCapacity 指定的初始容量 * */ public CompleteBinaryHeap(int defaultCapacity){ this.innerArrayList = new ArrayList<>(defaultCapacity); this.comparator = null; } /** * 指定初始容量的構造函數 * @param comparator 指定的比較器邏輯 * */ public CompleteBinaryHeap(Comparator<E> comparator){ this.innerArrayList = new ArrayList<>(); this.comparator = comparator; } /** * 指定初始容量和比較器的構造函數 * @param defaultCapacity 指定的初始容量 * @param comparator 指定的比較器邏輯 * */ public CompleteBinaryHeap(int defaultCapacity, Comparator<E> comparator) { this.innerArrayList = new ArrayList<>(defaultCapacity); this.comparator = comparator; } /** * 將指定數組 轉換為一個完全二叉堆 * @param array 指定的數組 * */ public CompleteBinaryHeap(E[] array){ this.innerArrayList = new ArrayList<>(array); this.comparator = null; this.size = array.length; // 批量建堆 heapify(); } /** * 將指定數組 轉換為一個完全二叉堆 * @param array 指定的數組 * @param comparator 指定的比較器邏輯 * */ public CompleteBinaryHeap(E[] array, Comparator<E> comparator){ this.innerArrayList = new ArrayList<>(array); this.comparator = comparator; this.size = array.length; // 批量建堆 heapify(); } // ==========================================外部方法=========================================== @Override public void insert(E newData) { // 先插入新數據到 向量末尾 this.innerArrayList.add(newData); // 獲得向量末尾元素下標 int lastIndex = getLastIndex(); // 對向量末尾元素進行上濾,以恢復堆序性 siftUp(lastIndex); } @Override public E peekMax() { // 內部數組第0位 即為堆頂max return this.innerArrayList.get(0); } @Override public E popMax() { if(this.innerArrayList.isEmpty()){ throw new CollectionEmptyException("當前完全二叉堆為空"); } // 將當前向量末尾的元素和堆頂元素交換位置 int lastIndex = getLastIndex(); swap(0,lastIndex); // 暫存被刪除的最大元素(之前的堆頂最大元素被放到了向量末尾) E max = this.innerArrayList.get(lastIndex); this.size--; // 對當前堆頂元素進行下濾,以恢復堆序性 siftDown(0); return max; } @Override public int size() { return this.size; } @Override public boolean isEmpty() { return this.size() == 0; } @Override public String toString() { //:::空列表 if(this.isEmpty()){ return "[]"; } //:::列表起始使用"[" StringBuilder s = new StringBuilder("["); //:::從第一個到倒數第二個元素之間 for(int i=0; i<size-1; i++){ //:::使用", "進行分割 s.append(this.innerArrayList.get(i)).append(",").append(" "); } //:::最后一個元素使用"]"結尾 s.append(this.innerArrayList.get(size-1)).append("]"); return s.toString(); } public static <T> void heapSort(T[] array){ CompleteBinaryHeap<T> completeBinaryHeap = new CompleteBinaryHeap<>(array); for(int i=array.length-1; i>=0; i--){ array[i] = completeBinaryHeap.popMax(); } } // =========================================內部輔助函數=========================================== /** * 上濾操作 * @param index 需要上濾的元素下標 * */ private void siftUp(int index){ while(index >= 0){ // 獲得當前節點 int parentIndex = getParentIndex(index); E currentData = this.innerArrayList.get(index); E parentData = this.innerArrayList.get(parentIndex); // 如果當前元素 大於 雙親元素 if(compare(currentData,parentData) > 0){ // 交換當前元素和雙親元素的位置 swap(index,parentIndex); // 繼續向上迭代 index = parentIndex; }else{ // 當前元素沒有違反堆序性,直接返回 return; } } } /** * 下濾操作 * @param index 需要下濾的元素下標 * */ private void siftDown(int index){ int size = this.size(); // 葉子節點不需要下濾 int half = size >>> 1; while(index < half){ int leftIndex = getLeftChildIndex(index); int rightIndex = getRightChildIndex(index); if(rightIndex < size){ // 右孩子存在 (下標沒有越界) E leftData = this.innerArrayList.get(leftIndex); E rightData = this.innerArrayList.get(rightIndex); E currentData = this.innerArrayList.get(index); // 比較左右孩子大小 if(compare(leftData,rightData) >= 0){ // 左孩子更大,比較雙親和左孩子 if(compare(currentData,leftData) >= 0){ // 雙親最大,終止下濾 return; }else{ // 三者中,左孩子更大,交換雙親和左孩子的位置 swap(index,leftIndex); // 繼續下濾操作 index = leftIndex; } }else{ // 右孩子更大,比較雙親和右孩子 if(compare(currentData,rightData) >= 0){ // 雙親最大,終止下濾 return; }else{ // 三者中,右孩子更大,交換雙親和右孩子的位置 swap(index,rightIndex); // 繼續下濾操作 index = rightIndex; } } }else{ // 右孩子不存在 (下標越界) E leftData = this.innerArrayList.get(leftIndex); E currentData = this.innerArrayList.get(index); // 當前節點 大於 左孩子 if(compare(currentData,leftData) >= 0){ // 終止下濾 return; }else{ // 交換 左孩子和雙親的位置 swap(index,leftIndex); // 繼續下濾操作 index = leftIndex; } } } } /** * 批量建堆(將內部數組轉換為完全二叉堆) * */ private void heapify(){ // 獲取下標最大的 內部非葉子節點 int lastInternalIndex = getLastInternal(); // Floyd建堆算法 時間復雜度"O(n)" // 從lastInternalIndex開始向前遍歷,對每一個元素進行下濾操作,從小到大依次合並 for(int i=lastInternalIndex; i>=0; i--){ siftDown(i); } } /** * 獲得邏輯上 雙親節點下標 * @param currentIndex 當前下標 * */ private int getParentIndex(int currentIndex){ return (currentIndex - 1)/2; } /** * 獲得邏輯上 左孩子節點下標 * @param currentIndex 當前下標 * */ private int getLeftChildIndex(int currentIndex){ return (currentIndex * 2) + 1; } /** * 獲得邏輯上 右孩子節點下標 * @param currentIndex 當前下標 * */ private int getRightChildIndex(int currentIndex){ return (currentIndex + 1) * 2; } /** * 獲得當前向量末尾下標 * */ private int getLastIndex(){ return this.size - 1; } /** * 獲得最后一個非葉子節點下標 * */ private int getLastInternal(){ return (this.size()/2) - 1; } /** * 交換向量中兩個元素位置 * @param a 某一個元素的下標 * @param b 另一個元素的下標 * */ private void swap(int a, int b){ // 現暫存a、b下標元素的值 E aData = this.innerArrayList.get(a); E bData = this.innerArrayList.get(b); // 交換位置 this.innerArrayList.set(a,bData); this.innerArrayList.set(b,aData); } /** * 進行比較 * */ @SuppressWarnings("unchecked") private int compare(E t1, E t2){ // 迭代器不存在 if(this.comparator == null){ // 依賴對象本身的 Comparable,可能會轉型失敗 return ((Comparable) t1).compareTo(t2); }else{ // 通過迭代器邏輯進行比較 return this.comparator.compare(t1,t2); } } }
本系列博客的代碼在我的 github上:https://github.com/1399852153/DataStructures ,存在許多不足之處,請多多指教。