教你如何使用Java手寫一個基於陣列實現的佇列
一、概述
佇列,又稱為佇列(queue),是先進先出(FIFO, First-In-First-Out)的線性表。在具體應用中通常用連結串列或者陣列來實現。佇列只允許在後端(稱為rear)進行插入操作,在前端(稱為front)進行刪除操作。佇列的操作方式和堆疊類似,唯一的區別在於佇列只允許新資料在後端進行新增。
在Java中佇列又可以分為兩個大類,一種是阻塞佇列和非阻塞佇列。
1、沒有實現阻塞介面:
1)實現java.util.Queue的LinkList,
2)實現java.util.AbstractQueue介面內建的不阻塞佇列: PriorityQueue 和 ConcurrentLinkedQueue
2、實現阻塞介面的
java.util.concurrent 中加入了 BlockingQueue 介面和五個阻塞佇列類。它實質上就是一種帶有一點扭曲的 FIFO 資料結構。不是立即從佇列中新增或者刪除元素,執行緒執行操作阻塞,直到有空間或者元素可用。
五個佇列所提供的各有不同:
* ArrayBlockingQueue :一個由陣列支援的有界佇列。
* LinkedBlockingQueue :一個由連結節點支援的可選有界佇列。
* PriorityBlockingQueue :一個由優先順序堆支援的無界優先順序佇列。
* DelayQueue :一個由優先順序堆支援的、基於時間的排程佇列。
* SynchronousQueue :一個利用 BlockingQueue 介面的簡單聚集(rendezvous)機制。
佇列是Java中常用的資料結構,比如線上程池中就是用到了佇列,比如訊息佇列等。
由佇列先入先出的特性,我們知道佇列資料的儲存結構可以兩種,一種是基於陣列實現的,另一種則是基於單鏈實現。前者在建立的時候就已經確定了陣列的長度,所以佇列的長度是固定的,但是可以迴圈使用陣列,所以這種佇列也可以稱之為迴圈佇列。後者實現的佇列內部通過指標指向形成一個佇列,這種佇列是單向且長度不固定,所以也稱之為非迴圈佇列。下面我將使用兩種方式分別實現佇列。
二、基於陣列實現迴圈佇列
由於在往佇列中放資料或拉取資料的時候需要移動陣列對應的下標,所以需要記錄一下隊尾和隊頭的位置。說一下幾個核心的屬性吧:
1、queue:佇列,object型別的陣列,用於儲存資料,長度固定,當儲存的資料數量大於陣列當度則丟擲異常;
2、head:隊頭指標,int型別,用於記錄佇列頭部的位置資訊。
3、tail:隊尾指標,int型別,用於記錄佇列尾部的位置資訊。
4、size:佇列長度,佇列長度大於等於0或者小於等於陣列長度。
/** * 佇列管道,當管道中存放的資料大於佇列的長度時將不會再offer資料,直至從佇列中poll資料後 */ private Object[] queue; //佇列的頭部,獲取資料時總是從頭部獲取 private int head; //佇列尾部,push資料時總是從尾部新增 private int tail; //佇列長度 private int size; //陣列中能存放資料的最大容量 private final static int MAX_CAPACITY = 1<<30; //陣列長度 private int capacity; //最大下標 private int maxIndex;
三、資料結構
圖中,紅色部分即為佇列的長度,陣列的長度為16。因為這個佇列是迴圈佇列,所以佇列的頭部不一定要在佇列尾部前面,只要佇列的長度不大於陣列的長度就可以了。
四、構造方法
1、MyQueue(int initialCapacity):建立一個最大長度為 initialCapacity的佇列。
2、MyQueue():建立一個預設最大長度的佇列,預設長度為16;
public MyQueue(int initialCapacity){ if (initialCapacity > MAX_CAPACITY) throw new OutOfMemoryError("initialCapacity too large"); if (initialCapacity <= 0) throw new IndexOutOfBoundsException("initialCapacity must be more than zero"); queue = new Object[initialCapacity]; capacity = initialCapacity; maxIndex = initialCapacity - 1; head = tail = -1; size = 0; } public MyQueue(){ queue = new Object[16]; capacity = 16; head = tail = -1; size = 0; maxIndex = 15; }
五、往佇列新增資料
新增資料時,首先判斷佇列的長度是否超出了陣列的長度,如果超出了就新增失敗(也可以設定成等待,等到有人消費了佇列裡的資料,然後再新增進去)。都是從佇列的尾部新增資料的,新增完資料後tail指標也會相應自增1。具體實現如一下程式碼:
/** * 往佇列尾部新增資料 * @param object 資料 */ public void offer(Object object){ if (size >= capacity){ System.out.println("queue's size more than or equal to array's capacity"); return; } if (++tail > maxIndex){ tail = 0; } queue[tail] = object; size++; }
六、向佇列中拉取資料
拉取資料是從佇列頭部拉取的,拉取完之後將該元素刪除,佇列的長度減一,head自增1。程式碼如下:
/** * 從佇列頭部拉出資料 * @return 返回佇列的第一個資料 */ public Object poll(){ if (size <= 0){ System.out.println("the queue is null"); return null; } if (++head > maxIndex){ head = 0; } size--; Object old = queue[head]; queue[head] = null; return old; }
七、其他方法
1、檢視資料:這個方法跟拉取資料一樣都是獲取到佇列頭部的資料,區別是該方法不會將對頭資料刪除:
/** * 檢視第一個資料 * @return */ public Object peek(){ return queue[head]; }
2、清空佇列:
/** * 清空佇列 */ public void clear(){ for (int i = 0; i < queue.length; i++) { queue[i] = null; } tail = head = -1; size = 0; }
完整的程式碼如下:
/** * 佇列 */ public class MyQueue { /** * 佇列管道,當管道中存放的資料大於佇列的長度時將不會再offer資料,直至從佇列中poll資料後 */ private Object[] queue; //佇列的頭部,獲取資料時總是從頭部獲取 private int head; //佇列尾部,push資料時總是從尾部新增 private int tail; //佇列長度 private int size; //陣列中能存放資料的最大容量 private final static int MAX_CAPACITY = 1<<30; //陣列長度 private int capacity; //最大下標 private int maxIndex; public MyQueue(int initialCapacity){ if (initialCapacity > MAX_CAPACITY) throw new OutOfMemoryError("initialCapacity too large"); if (initialCapacity <= 0) throw new IndexOutOfBoundsException("initialCapacity must be more than zero"); queue = new Object[initialCapacity]; capacity = initialCapacity; maxIndex = initialCapacity - 1; head = tail = -1; size = 0; } public MyQueue(){ queue = new Object[16]; capacity = 16; head = tail = -1; size = 0; maxIndex = 15; } /** * 往佇列尾部新增資料 * @param object 資料 */ public void offer(Object object){ if (size >= capacity){ System.out.println("queue's size more than or equal to array's capacity"); return; } if (++tail > maxIndex){ tail = 0; } queue[tail] = object; size++; } /** * 從佇列頭部拉出資料 * @return 返回佇列的第一個資料 */ public Object poll(){ if (size <= 0){ System.out.println("the queue is null"); return null; } if (++head > maxIndex){ head = 0; } size--; Object old = queue[head]; queue[head] = null; return old; } /** * 檢視第一個資料 * @return */ public Object peek(){ return queue[head]; } /** * 佇列中儲存的資料量 * @return */ public int size(){ return size; } /** * 佇列是否為空 * @return */ public boolean isEmpty(){ return size == 0; } /** * 清空佇列 */ public void clear(){ for (int i = 0; i < queue.length; i++) { queue[i] = null; } tail = head = -1; size = 0; } @Override public String toString() { if (size <= 0) return "{}"; StringBuilder builder = new StringBuilder(size + 8); builder.append("{"); int h = head; int count = 0; while (count < size){ if (++h > maxIndex) h = 0; builder.append(queue[h]); builder.append(", "); count++; } return builder.substring(0,builder.length()-2) + "}"; } }
八、總結:
1、該佇列為非執行緒安全的,在多執行緒環境中可能會發生資料丟失等問題。
2、佇列通過移動指標來確定陣列下標的位置,因為是基於陣列實現的,所以佇列的長度不能夠超過陣列的長度。
3、該佇列是迴圈佇列,這就意味著陣列可以重複被使用,避免了重複建立物件帶來的效能的開銷。
4、新增資料時總是從佇列尾部新增,拉取資料時總是從佇列頭部拉取,拉取完將物件元素刪除。