数据结构_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)水平,较数组队列有更好的性能。

猜你喜欢

转载自blog.csdn.net/Nerver_77/article/details/89787019