[從今天開始修煉資料結構]佇列、迴圈佇列、PriorityQueue的原理及實現
[從今天開始修煉資料結構]基本概念
[從今天開始修煉資料結構]線性表及其實現以及實現有Itertor的ArrayList和LinkedList
[從今天開始修煉資料結構]棧、斐波那契數列、逆波蘭四則運算的實現
[從今天開始修煉資料結構]佇列、迴圈佇列、PriorityQueue的原理及實現
一、什麼是佇列
佇列queue是隻允許在一端進行插入操作,而在另一端進行刪除操作的線性表。
佇列是先進先出的線性表,簡稱FIFO。允許插入的一端稱為隊尾,允許刪除的一端稱為隊頭。如下圖所示
例如聊天室訊息傳送就是佇列形式,先發出去的訊息會先顯示,後發的會後顯示。
二、佇列的抽象資料型別
佇列的抽象資料型別與線性表的基本相同,只不過插入刪除的位置有限制。
ADT Queue Data 同線性表,相鄰的元素具有前驅後繼關係 Operation InitQueue(*Q):初始化操作 DestroyQueue(*Q):若佇列Q存在,銷燬他 ClearQueue(*Q):將佇列Q清空 QueueEmpty(Q):若佇列Q為空,返回true,否則返回false GetHead(Q,*e):若Q存在且非空,用e返回隊首元素 EnQueue(*Q,e):若Q存在,插入新元素e到隊尾 DeQueue(*Q,*e):刪除Q中的隊首元素,並用e返回其值 QueueLength(Q):返回佇列Q的元素個數 endADT
三、迴圈佇列
1,在看迴圈佇列之前,我們首先看一下普通的順序儲存佇列。
順序儲存佇列由陣列實現,下標0一端作為初始隊首,另一端作為隊尾,初始化陣列大小應該比要儲存的資料要大,以方便插入。我們宣告一個隊首指標front指向第一個元素,一個隊尾指標rear指向隊尾元素的下一個位置。
每刪除一個元素,隊首指標就向後移動一個格子,每插入一個元素,隊尾指標也向後移動一個格子。
這樣實現的佇列有什麼弊端? 由於陣列的大小是固定的,儘管設計者可能考慮到宣告一個足夠大的陣列,在經年累月的插入刪除後,陣列空間總有用完的一天,這時候只能想辦法來擴容。但是此時陣列的前端還剩下許多由於刪除而沒有使用的空間,這種現象叫做“假溢位”。為了解決這種現象,我們考慮使用迴圈佇列。
2,迴圈佇列
解決假溢位的辦法就是後面滿了,就再從頭開始,也就是頭尾相接的迴圈。這就是迴圈佇列。當陣列後端位置用到最後一個時,讓front指向0位置,這樣不會造成指標指向不明的問題。
現在解決了空間浪費和假溢位的問題,但是現在如何判斷佇列是滿呢?當front=rear時,佇列是空還是滿呢?
我們設佇列的最大長度為QueueSize,且讓佇列滿時,保留一個元素空間。也就是說array.size - 1 = QueueSize。這時佇列滿的條件為(rear + 1) % QueueSize == front。
另外,通用的隊列當前長度計算方法為(rear - front + QueueSize) % QueueSize。
3,迴圈佇列的實現
package Queue;
public class ArrayQueueDemo<T> {
/*
仿照JDK1.8 中的 ArrayQueue 實現一個簡單的迴圈佇列,有些許不同
JDK 13中為什麼沒找到ArrayQueue?
*/
private Object[] queue;
private int capacity; //實際要裝的資料個數
private int front;
private int rear;
public ArrayQueueDemo(int capacity){
this.capacity = capacity + 1;
this.queue = new Object[capacity + 1]; //+1之後是陣列長度,因為要預留一個空位置給rear。 文中提到的QueueSize是沒有 + 1 的capacity
this.front = 0;
this.rear = 0;
}
public void add(T data){
queue[rear] = data;
int newRear = (rear + 1) % capacity;
if (newRear == front){
throw new IndexOutOfBoundsException();
}
rear = newRear;
}
public T remove(){
if (isFull()){
throw new IndexOutOfBoundsException();
}
Object removed = queue[front];
queue[front] = null;
front = (front + 1) % capacity;
return (T)removed;
}
/**
* 返回最大佇列長度
* @return 最大
*/
public int capacity(){
return capacity;
}
/**
* 返回當前佇列大小
* @return
*/
public int size(){
return (rear - front + capacity - 1) % (capacity - 1);
}
public boolean isFull(){
return (rear + 1) % (capacity - 1) == front;
}
}
四、佇列的鏈式儲存結構及實現。
佇列的臉書儲存結構,就是線性表的單鏈表,只不過限制了只能尾進頭出。我們把它簡稱為鏈佇列。為了操作方便,我們把隊頭指標指向連結串列的表頭,隊尾指標指向連結串列的表尾。空佇列時,front和rear都指向頭結點。
由於JDK中沒有單純的鏈式佇列,且鏈式佇列的實現很簡單,所以我選擇在這裡實現一下PriorityQueue
五、PriorityQueue 這部分參考和轉載自https://blog.csdn.net/u010623927/article/details/87179364
優先佇列是在入隊時自動按照自然順序或者給定的比較器排序的佇列。
優先佇列是通過陣列表示的小頂堆實現的。小頂堆可以理解為,父結點的權值總是不大於子節點的完全二叉樹。
上圖中我們給每個元素按照層序遍歷的方式進行了編號,如果你足夠細心,會發現父節點和子節點的編號是有聯絡的,更確切的說父子節點的編號之間有如下關係:
leftNo = parentNo*2+1
rightNo = parentNo*2+2
parentNo = (nodeNo-1)/2
通過上述三個公式,可以輕易計算出某個節點的父節點以及子節點的下標。這也就是為什麼可以直接用陣列來儲存堆的原因。
PriorityQueue的peek()和element操作是常數時間,add(), offer(), 無引數的remove()以及poll()方法的時間複雜度都是log(N)。
方法剖析
add()和offer()
add(E e)和offer(E e)的語義相同,都是向優先佇列中插入元素,只是Queue介面規定二者對插入失敗時的處理不同,前者在插入失敗時丟擲異常,後則則會返回false。對於PriorityQueue這兩個方法其實沒什麼差別。
新加入的元素可能會破壞小頂堆的性質,因此需要進行必要的調整。
Poll()方法的實現原理如下
首先記錄0下標處的元素,並用最後一個元素替換0下標位置的元素,之後呼叫siftDown()方法對堆進行調整,最後返回原來0下標處的那個元素(也就是最小的那個元素)。重點是siftDown(int k, E x)方法,該方法的作用是從k指定的位置開始,將x逐層向下與當前點的左右孩子中較小的那個交換,直到x小於或等於左右孩子中的任何一個為止。
PriorityQueue的簡易程式碼實現:
package Queue; import java.util.Arrays; import java.util.Comparator; /* 優先佇列的作用是能保證每次取出的元素都是佇列中權值最小的 (Java的優先佇列每次取最小元素,C++的優先佇列每次取最大元素)。 這裡牽涉到了大小關係,元素大小的評判可以通過元素本身的自然順序(natural ordering), 也可以通過構造時傳入的比較器。 */ public class PriorityQueueDemo<T> { private Object[] queue; private int size; private Comparator<? super T> comparator; public PriorityQueueDemo(int capacity){ queue = new Object[capacity]; size = 0; } public PriorityQueueDemo(int capacity,Comparator<? super T> comparator){ this.comparator = comparator; queue = new Object[capacity]; size = 0; } /** * 這裡給出一個堆排序初始化一個小頂堆佇列的方法 * @param data 要被排序的陣列 * @param comparator 定義比較方式的比較器 */ public PriorityQueueDemo(T[] data, Comparator<T> comparator) throws Exception { int capacity = data.length; queue = new Object[capacity]; size = 0; this.comparator = comparator; for (int i = 0; i < data.length; i++) { add(data[i]); } } public void add(T data) throws Exception { int i = size; if (data == null){ throw new Exception(); }else if (i >= queue.length){ grow(i); } if (size == 0){ queue[0] = data; size++; } else { queue[i] = data; shiftUp(i, data); size++; } } public T poll(){ if (size == 0){ throw new NullPointerException(); } T polled = (T)queue[0]; int s = --size; T x = (T)queue[s]; queue[0] = queue[s]; queue[s] = null; if (s != 0){ shiftDown(0,x); } return polled; } private void shiftDown(int i, T x) { //父節點是否大於子節點的標誌 //若為true,則需要進入迴圈。 boolean flag = true; while(flag) { int child = (i << 1) + 1; T smallerChild = (T) queue[child]; int right = child + 1; if (right >= size || child >= size){ flag = false; break; } if (comparator.compare(smallerChild, (T) queue[right]) > 0) { child = right; smallerChild = (T) queue[child]; } if (comparator.compare(x, smallerChild) <= 0) { flag = false; break; } queue[i] = smallerChild; i = child; } queue[i] = x; } /** * 新結點上浮 * @param i i的初始值是size的引用,即尾指標的位置。 * @param data 新結點的資料 */ //理解。 private void shiftUp(int i,T data) { while(i > 0) { int parent = (i - 1) >>> 1; Object e = queue[parent]; if (comparator.compare(data, (T)e) >= 0) { break; } queue[i] = e; i = parent; } queue[i] = data; } private void grow(int i) { Object[] newQueue = Arrays.copyOf(queue, i + 1); this.size++; } }
&n