隊列 Queue
寫在開頭
- 先進先出的線性數據結構(FIFO)
- 只允許在表的前端(front)進行刪除操作,而在表的後端(tail)進行插入操作。進行插入操作的端稱爲隊尾,進行刪除操作的端稱爲隊頭。
- 隊列中沒有元素時,稱爲空隊列。
數組隊列的實現,結合動態數組,以接口的方式構建ArrayQueue<E>
-
接口:Queue
public interface Queue<E> { /** * 獲取隊列容量大小 * @return */ int getSize(); /** * 隊列空判斷 * @return */ boolean isEmpty(); /** * 入隊 * @param e */ void enqueue(E e); /** * 出隊 * @return */ E dequeue(); /** * 獲取隊首元素 * @return */ E getFront(); }
-
接口實現類:ArrayQueue<E>
public class ArrayQueue<E> implements Queue<E>{ private Array<E> array; public ArrayQueue() { array = new Array<>(); } public ArrayQueue(int capacity) { array = new Array<>(capacity); } @Override public int getSize() { return array.getSize(); } @Override public boolean isEmpty() { return array.isEmpty(); } @Override public void enqueue(E e) { array.addLast(e); } @Override public E dequeue() { return array.removeFirst(); } @Override public E getFront() { return array.getFirst(); } @Override public String toString() { return "ArrayQueue{" + "array=" + array + '}'; } }
循環隊列的實現
- 數組隊列的侷限性:聚焦於出隊操作,時間複雜度爲O(n)級別
- 爲什麼這樣講,出隊操作針對隊首元素,底層數組在remove索引爲0的元素後,會對其餘元素進行前移操作,從而涉及到遍歷操作,因此時間複雜度上升至O(n)。
- 循環隊列,摒棄元素出隊後的其他元素前移操作,構建頭指針front,尾指針tail(本質爲動態數組的size),出隊元素前移操作簡化爲頭指針的移動操作(front++)。
- 需要注意的兩點:
- 循環隊列判空:front == tail [ 初始狀態 ]
- 循環隊列判滿:(tail + 1) % C == front [ C爲隊列長度,浪費一個數組空間用於尾指針指向 ]
- 關於底層動態數組擴容的問題?
- 在動態數組文章中提到了擴容的實質是開闢新的內存空間,並將原數組內容複製到新數組中,這個地方就出現了一個問題,循環數組由於充分利用了數組的空間,所以當循環隊列滿時,數組索引處爲0的位置,並不一定是循環隊列的第一個元素。
- 如下圖,數組索引爲0的位置是循環隊列中最後添加的元素,此刻觸發數組擴容操作,數組複製的時候需要注意:由於隊列也是線性結構,元素應該有序放入,所以動態數組的resize方法需要做一些變動。
- 改造 ArrayQueue<E>,結合 Queue 接口進行方法重寫
-
創建 LoopQueue<E>,完成基本成員屬性構造
public class LoopQueue<E> implements Queue<E> { private E[] data; private int front, tail; private int size; // 隊列實際容量標識 public LoopQueue(int capacity) { // capacity + 1 適應循環隊列滿載機制 // (tail + 1) % c == front data = (E[]) new Object[capacity + 1]; front = 0; tail = 0; size = 0; } public LoopQueue() { this(10); } // 獲取隊列最大容量 public int getCapacity() { return data.length - 1; } }
-
getSize() 獲取隊列實際容量
@Override public int getSize() { return size; }
-
isEmpty() 隊列空判斷
@Override public boolean isEmpty() { // 隊列判空條件 return front == tail; }
-
getFront() 獲取隊首元素
@Override public E getFront() { if (isEmpty()) { throw new IllegalArgumentException("Queue is empty"); } return data[front]; }
-
重寫 resize(),規整循環隊列
/** * 容量重置 * @param capacity */ private void resize(int capacity) { E[] newData = (E[]) new Object[capacity + 1]; for (int i = 0; i < size; i++) { // 新數組中的元素索引相較原數組中索引存在front的偏移量 newData[i] = data[(front + i) % data.length]; } // 數組地址指向、頭指針變更爲默認值、尾指針指向變更 data = newData; front = 0; tail = size; }
-
enqueue(E e) 入隊
@Override public void enqueue(E e) { if ((tail + 1) % data.length == front) { resize(getCapacity() * 2); } data[tail] = e; tail = (tail + 1) % data.length; size ++; }
-
dequeue() 出隊
@Override public E dequeue() { if (isEmpty()) { throw new IllegalArgumentException("Queue is empty"); } E res = data[front]; data[front] = null; front = (front + 1) % data.length; size --; // 四等分點進行數組縮容,避免複雜度震盪 if (size == getCapacity() / 4 && getCapacity() / 2 != 0) { resize(getCapacity() / 2); } return res; }
-
比較 - 數組隊列和循環隊列 (分別從入隊角度和出隊角度考慮)
-
測試方法
private static double testQueue(Queue<Integer> q, int opCount) { long startTime = System.nanoTime(); Random random = new Random(); for (int i = 0; i < opCount; i++) { q.enqueue(random.nextInt(Integer.MAX_VALUE)); } // 出隊測試時使用 for (int i = 0; i < opCount; i++) { q.dequeue(); } long endTime = System.nanoTime(); return (endTime - startTime) / 1000000000.0; }
-
測試Main方法,定義操作次數、分別創建數組隊列和循環隊列對象
public static void main(String[] args) { int opCount = 100000; Queue<Integer> arrayQueue = new ArrayQueue<>(); Queue<Integer> loopQueue = new LoopQueue<>(); System.out.println("arrayQueue:" + testQueue(arrayQueue, opCount) + " s"); System.out.println("loopQueue:" + testQueue(loopQueue, opCount) + " s"); }
-
入隊耗時測試:
-
入對 + 出隊耗時測試:兩種隊列的區別主要在出隊,數組隊列複雜度上升也因爲出隊操作
總結:結果顯而易見,循環隊列的方式合理利用了數組空間,並且將出隊操作的時間複雜度拉回O(1)水平,較數組隊列有更好的性能。