データ構造_3:キュー(配列キュー+循環キュー)

キュー

最初に書く

  • 先入れ先出し線形データ構造(FIFO)
  • テーブルの前面(前面)での削除操作と、テーブルの背面(末尾)での挿入操作のみを許可します。挿入操作を実行する端はキューの終わりと呼ばれ、削除操作を実行する端はキューの先頭と呼ばれます。
  • キューに要素がない場合、それは空のキューと呼ばれます。

動的配列と組み合わせた配列キューの実現は、インターフェースを介してArrayQueue <E>を構築します

  • インターフェース:キュー

      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)レベルです
    • デキュー操作はチームの最初の要素に対するものであり、基になる配列は、トラバーサル操作を含むインデックス0の要素を削除した後、残りの要素を前方に移動すると言われるのはなぜですか。そのため、時間計算量はO(n)に上昇します。 。
    • 循環キュー、要素がデキューされた後に他の要素の前方操作を放棄し、ヘッドポインターフロント、テールポインターテール(基本的に動的配列のサイズ)を構築し、デキューされた要素のヘッドポインター移動操作への前方移動を単純化します(フロント++)。
  • 注意すべき2つのポイント:
    • 循環キューは空です:front == tail [初期状態]
    • 循環キューはいっぱいであると判断されます:(tail + 1)%C == front [Cはキューの長さであり、テールポインタが指す配列スペースを浪費します]
  • 基礎となる動的配列の拡張について?
    • 動的配列の記事では、拡張の本質は新しいメモリスペースを開き、元の配列の内容を新しい配列にコピーすることであると述べられています。この場所に問題があります。循環配列は、配列のスペースであるため、循環キューとして使用されます。いっぱいになった場合、配列インデックスが0の位置は、必ずしも循環キューの最初の要素であるとは限りません。
    • 次の図に示すように、配列インデックスが0の位置は、循環キューに追加された最後の要素です。この時点で、配列拡張操作がトリガーされます。配列をコピーするときは、次の点に注意する必要があります。も線形構造であるため、要素を順番に配置する必要があるため、動的配列のサイズ変更方法いくつかの変更を加える必要があります。
      ここに画像の説明を挿入
  • ArrayQueue <E>を変換し、キューインターフェイスと組み合わせてメソッドを書き直します
    • 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)enqueue

        @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()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");
      }
    
  • チームに参加するための時間のかかるテスト:
    ここに画像の説明を挿入

  • エントリ+デキューの時間のかかるテスト:2つのキューの主な違いはデキューです。デキュー操作により、配列キューの複雑さが増します。
    ここに画像の説明を挿入
    要約:結果は明らかです。循環キュー方式では、配列スペースを適切に使用します。時間計算量はO(1)レベルに戻り、アレイキューよりもパフォーマンスが向上します。

おすすめ

転載: blog.csdn.net/Nerver_77/article/details/89787019