美文网首页
数据结构之队列

数据结构之队列

作者: 陈盼同学 | 来源:发表于2021-06-07 09:13 被阅读0次

    (注释:整篇数据结构与算法文集,部分总结于王争的《数据结构与算法之美》和李明杰的《恋上数据结构与算法》,加上自己的理解,所以出了这个文集,仅做个人笔记记录所用。如你需要,请购买他们的正版资源,支持他们的原创)

    队列是一种特殊的线性表,只能在头尾两端进行操作

    • 队尾(rear):只能从队尾添加元素,一般叫做 enQueue,入队
    • 队头(front):只能从队头移除元素,一般叫做 deQueue,出队
    • 先进先出的原则,First In First Out,FIFO

    队列接口的设计

    • int size(); // 元素的数量
    • boolean isEmpty(); // 是否为空
    • void clear(); // 清空
    • void enQueue(E element); // 入队
    • E deQueue(); // 出队
    • E front(); // 获取队列的头元素

    队列的内部实现是否可以直接利用以前学过的数据结构?

    动态数组、链表
    优先使用双向链表,因为队列主要是往头尾操作元素

    用栈来实现队列

    https://leetcode-cn.com/problems/implement-queue-using-stacks/

    WechatIMG145.png

    双端队列

    双端队列是能在头尾两端添加、删除的队列
    -英文 deque 是 double ended queue 的简称

    双端队列接口的设计 (可用链表来便捷的 实现双端队列)

    -int size(); // 元素的数量
    -boolean isE mpty(); // 是否为空
    -void clear(); // 清空
    -void enQueueRear(E element);// 从队尾入队
    -E deQueueRear(); // 从队尾出队
    -void enQueueFront(E element); // 从队头入队
    -E deQueueFront(); // 从队头出队
    -E front(); // 获取队列的头元素
    -E rear(); // 获取队列的尾元素

    用数组来实现队列

    // 用数组实现的队列
    public class ArrayQueue {
      // 数组:items,数组大小:n
      private String[] items;
      private int n = 0;
      // head表示队头下标,tail表示队尾下标
      private int head = 0;
      private int tail = 0;
    
      // 申请一个大小为capacity的数组
      public ArrayQueue(int capacity) {
        items = new String[capacity];
        n = capacity;
      }
    
      // 入队
      public boolean enqueue(String item) {
        // 如果tail == n 表示队列已经满了
        if (tail == n) return false;
        items[tail] = item;
        ++tail;
        return true;
      }
    
      // 出队
      public String dequeue() {
        // 如果head == tail 表示队列为空
        if (head == tail) return null;
        // 为了让其他语言的同学看的更加明确,把--操作放到单独一行来写了
        String ret = items[head];
        ++head;
        return ret;
      }
    }
    

    当 a、b、c、d 依次入队之后,队列中的 head 指针指向下标为 0 的位置,tail 指针指向下标为 4 的位置。


    image.png

    当我们调用两次出队操作之后,队列中 head 指针指向下标为 2 的位置,tail 指针仍然指向下标为 4 的位置。


    image.png
    你肯定已经发现了,随着不停地进行入队、出队操作,head 和 tail 都会持续往后移动。当 tail 移动到最右边,即使数组中还有空闲空间,也无法继续往队列中添加数据了。这个问题该如何解决呢?
       // 入队操作,将item放入队尾
      public boolean enqueue(String item) {
        // tail == n表示队列末尾没有空间了
        if (tail == n) {
          // tail ==n && head==0,表示整个队列都占满了
          if (head == 0) return false;
          // 数据搬移
          for (int i = head; i < tail; ++i) {
            items[i-head] = items[i];
          }
          // 搬移完之后重新更新head和tail
          tail -= head;
          head = 0;
        }
        
        items[tail] = item;
        ++tail;
        return true;
      }
    

    从代码中我们看到,当队列的 tail 指针移动到数组的最右边后,如果有新的数据入队,我们可以将 head 到 tail 之间的数据,整体搬移到数组中 0 到 tail-head 的位置。


    image.png

    这只是数组实现队列的其中一种的解法。之后空间还不足的话就需要动态扩容或者使用循环队列等等技术了。

    循环队列 (可以理解为循环利用的队列)

    以下内容为王争的授课内容,采用数组,辅以双指针的方式,先来看看双指针方式实现循环队列。

    我们刚才用数组来实现队列的时候,在 tail==n 时,会有数据搬移操作,这样入队操作性能就会受到影响。那有没有办法能够避免数据搬移呢?

    我们来看看循环队列的解决思路。循环队列,顾名思义,它长得像一个环。原本数组是有头有尾的,是一条直线。现在我们把首尾相连,扳成了一个环。我画了一张图,你可以直观地感受一下。

    image.png

    我们可以发现,图中这个队列的大小为 8,当前 head=4,tail=7。当有一个新的元素 a 入队时,我们放入下标为 7 的位置。但这个时候,我们并不把 tail 更新为 8,而是将其在环中后移一位,到下标为 0 的位置。当再有一个元素 b 入队时,我们将 b 放入下标为 0 的位置,然后 tail 加 1 更新为 1。所以,在 a,b 依次入队之后,循环队列中的元素就变成了下面的样子:


    image.png

    通过这样的方法,我们成功避免了数据搬移操作。看起来不难理解,但是循环队列的代码实现难度要比前面讲的非循环队列难多了。要想写出没有 bug 的循环队列的实现代码,我个人觉得,最关键的是,确定好队空和队满的判定条件。

    在用数组实现的非循环队列中,队满的判断条件是 tail == n,队空的判断条件是 head == tail。那针对循环队列,如何判断队空和队满呢?

    队列为空的判断条件仍然是 head == tail。但队列满的判断条件就稍微有点复杂了。我画了一张队列满的图,你可以看一下,试着总结一下规律。

    image.png

    就像我图中画的队满的情况,tail=3,head=4,n=8,所以总结一下规律就是:(3+1)%8=4。多画几张队满的图,你就会发现,当队满时,(tail+1)%n=head。

    你有没有发现,当队列满时,图中的 tail 指向的位置实际上是没有存储数据的。所以,循环队列会浪费一个数组的存储空间。(为什么要浪费一个数组的存储空间,我的理解是这样的,其实主要问题就是为了能够很好的判断队满的条件是什么?如果不浪费一个数组空间,可能需要引入其他变量,去记录队列的实际容量,也是需要额外的内存空间,那如果浪费了一个数组空间,可以在不需要引入额外的变量的情况下,给出队满的判断条件。)

    public class CircularQueue {
      // 数组:items,数组大小:n
      private String[] items;
      private int n = 0;
      // head表示队头下标,tail表示队尾下标
      private int head = 0;
      private int tail = 0;
    
      // 申请一个大小为capacity的数组
      public CircularQueue(int capacity) {
        items = new String[capacity];
        n = capacity;
      }
    
      // 入队
      public boolean enqueue(String item) {
        // 队列满了
        if ((tail + 1) % n == head) return false;
        items[tail] = item;
        tail = (tail + 1) % n;
        return true;
      }
    
      // 出队
      public String dequeue() {
        // 如果head == tail 表示队列为空
        if (head == tail) return null;
        String ret = items[head];
        head = (head + 1) % n;
        return ret;
      }
    }
    
    下面是MJ使用数组辅以单指针的方式,来实现循环队列。 (单指针是省略了尾指针,使用 (front+size)%length的方式求出最后一个要添加的位置,其实也就等同于上面的尾指针了。其实头指针也容易越界,所以也要取模操作front = (front + 1)%length )。下面代码实现循环队列的同时做了动态扩容。
    package com.mj.circle;
    
    @SuppressWarnings("unchecked")
    public class CircleQueue<E> {
        private int front;
        private int size;
        private E[] elements;
        private static final int DEFAULT_CAPACITY = 10;
        
        public CircleQueue() {
            elements = (E[]) new Object[DEFAULT_CAPACITY];
        }
        
        public int size() {
            return size;
        }
    
        public boolean isEmpty() {
            return size == 0;
        }
        
        public void clear() {
            for (int i = 0; i < size; i++) {
                elements[index(i)] = null;
            }
            front = 0;
            size = 0;
        }
    
        public void enQueue(E element) {
           //入队前先判断容量够不够存,size是当前容量,size + 1就是需要看看有没有额外一个容量,有的话才能add
            ensureCapacity(size + 1);
            // element[(front+size)%elements.length] = element;
            elements[index(size)] = element;
            size++;
        }
    
        public E deQueue() {
            E frontElement = elements[front];
            elements[front] = null;
            //front = (front + 1)%length ;
            front = index(1);
            size--;
            return frontElement;
        }
    
        public E front() {
            return elements[front];
        }
        
        @Override
        public String toString() {
            StringBuilder string = new StringBuilder();
            string.append("capcacity=").append(elements.length)
            .append(" size=").append(size)
            .append(" front=").append(front)
            .append(", [");
            for (int i = 0; i < elements.length; i++) {
                if (i != 0) {
                    string.append(", ");
                }
                
                string.append(elements[i]);
            }
            string.append("]");
            return string.toString();
        }
        
    //因为几处地方都用到了(*+front)%elements.length,所以把这个提出来,需要的地方直接把*传进来。
        private int index(int index) {
    
                    return  (front + index)%elements.length;
            
                    //index += front;
            //return index - (index >= elements.length ? elements.length : 0);
        }
        
        /**
         * 保证要有capacity的容量
         * @param capacity
         */
        private void ensureCapacity(int capacity) {
            int oldCapacity = elements.length;
            if (oldCapacity >= capacity) return;
            
            // 新容量为旧容量的1.5倍
            int newCapacity = oldCapacity + (oldCapacity >> 1);
            E[] newElements = (E[]) new Object[newCapacity];
            for (int i = 0; i < size; i++) {
                //扩容时数据搬迁不是单纯的的0元素放0元素,而是front元素放在新的数组0的位置,依次放入。
                //newElements[i] = elements[(i+front)%elements.length];
                newElements[i] = elements[index(i)];
            }
            elements = newElements;
            
            // 重置front
            front = 0;
        }
    }
    

    循环双端队列(此处使用了数组来实现循环双端队列。因为如果用链表太过于 简单)

    循环双端队列相比上面的循环队列,特别需要注意的是在头部入队的问题。其实头部坐标也很好解决。如果0的位置有值的话,依旧往头部入队,其实是要入到整个数组的最后一个元素处,如果front-1的话就变成-1了,此时如果-1+length的话就是需要入队的头部位置了

          package com.mj.circle;
    
    @SuppressWarnings("unchecked")
    public class CircleDeque<E> {
        private int front;
        private int size;
        private E[] elements;
        private static final int DEFAULT_CAPACITY = 10;
        
        public CircleDeque() {
            elements = (E[]) new Object[DEFAULT_CAPACITY];
        }
        
        public int size() {
            return size;
        }
    
        public boolean isEmpty() {
            return size == 0;
        }
    
        public void clear() {
            for (int i = 0; i < size; i++) {
                elements[index(i)] = null;
            }
            front = 0;
            size = 0;
        }
    
        /**
         * 从尾部入队
         * @param element
         */
        public void enQueueRear(E element) {
            ensureCapacity(size + 1);
            
            elements[index(size)] = element;
            size++;
        }
    
        /**
         * 从头部出队
         * @param element
         */
        public E deQueueFront() {
            E frontElement = elements[front];
            elements[front] = null;
            front = index(1);
            size--;
            return frontElement;
        }
    
        /**
         * 从头部入队
         * @param element
         */
        public void enQueueFront(E element) {
            ensureCapacity(size + 1);
            
            front = index(-1);
            elements[front] = element;
            size++;
        }
    
        /**
         * 从尾部出队
         * @param element
         */
        public E deQueueRear() {
            int rearIndex = index(size - 1);
            E rear = elements[rearIndex];
            elements[rearIndex] = null;
            size--;
            return rear;
        }
    
        public E front() {
            return elements[front];
        }
    
        public E rear() {
            return elements[index(size - 1)];
        }
    
        @Override
        public String toString() {
            StringBuilder string = new StringBuilder();
            string.append("capcacity=").append(elements.length)
            .append(" size=").append(size)
            .append(" front=").append(front)
            .append(", [");
            for (int i = 0; i < elements.length; i++) {
                if (i != 0) {
                    string.append(", ");
                }
                
                string.append(elements[i]);
            }
            string.append("]");
            return string.toString();
        }
        
        private int index(int index) {
           //先让front和index相加,看看是不是负数。如果是负数就说明是从头部入队,此时加上整个数组长度就可得到入队坐标。如果是正数,就按着之前的取模。
            index += front;
            if (index < 0) {
                return index + elements.length;
            }
            return index %  elements.length;
           //上面的那句取模运算可以优化
          /**
          n = 11 (坐标)
          m = 10 (数组长度)
          在此处n限定于小于2m的情况下
          if(n>=m){
          return n-m;
          }else {
          return n;
          }
          所以可以总结为下面这句
          return index - (index >= elements.length ? elements.length : 0);
          */
            
        }
        
        /**
         * 保证要有capacity的容量
         * @param capacity
         */
        private void ensureCapacity(int capacity) {
            int oldCapacity = elements.length;
            if (oldCapacity >= capacity) return;
            
            // 新容量为旧容量的1.5倍
            int newCapacity = oldCapacity + (oldCapacity >> 1);
            E[] newElements = (E[]) new Object[newCapacity];
            for (int i = 0; i < size; i++) {
                newElements[i] = elements[index(i)];
            }
            elements = newElements;
            
            // 重置front
            front = 0;
        }
    }
    

    相关文章

      网友评论

          本文标题:数据结构之队列

          本文链接:https://www.haomeiwen.com/subject/ngqzsltx.html