數據結構_3:隊列(數組隊列 + 循環隊列)

隊列 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)水平,較數組隊列有更好的性能。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章