【数据结构与算法】 一文搞懂队列|JAVA实现

167 阅读5分钟

持续创作,加速成长!这是我参与「掘金日新计划 · 6 月更文挑战」的第6天,点击查看活动详情

队列(Queue)是什么

  • 队列是一种有序列表,它可以用数组链表所实现
  • 队列是只允许在一端进行插入操作,而在另一端进行删除操作的线性表。允许插入的一端成为队尾,允许删除的一端成为队头
  • 队列遵循着先进先出的原则,即:先存入队列的数据,要先取出。而后入队列的数据,要后取出
  • 就跟我们排队一样,排在第一个的要优先出队,后来的当然排在队伍的最后

队列的常用操作

  • InitQueue(Queue):初始化操作,建立一个空队列
  • ClearQueue(Queue):将队列清空
  • QueueEmpty(Queue):判断队列是否为空
  • GetHead(Queue):若队列存在且非空,返回队列的头部元素
  • EnQueue(Queue,e):若队列存在,将新元素e插入到队列的队尾
  • DeQueue():删除队列中的头部元素,并返回
  • QueueLength(Queue):返回队列中的元素个数

队列的顺序存储结构(数组模拟队列)

思路分析

  1. 队列本身是有序列表,若使用数组的结构来存储队列的数据,则队列数组的声明如下图, 其中 maxSize 是该队列的最大容量。

  2. 因为队列的输出、输入是分别从前后端来处理,因此需要两个变量 front及 rear分别记录队列前后端的下标,front 会随着数据输出而改变,而 rear则是随着数据输入而改变,如图所示: img-20220530231526.png

  3. 为了避免当只有一个元素时,队头和队尾重合使处理变得麻烦,所以我们将front指向队头元素的前一个位置,rear指向队尾元素,这样当front == rear时,此队列不是还有一个元素,而是空队列

  4. rear小于队列的长度maxszie-1,元素可以放进队列里。否则无法存入数据,即队列已满的判断条件为 rear = maxsize - 1;

具体操作代码

顺序队列的结构

class ArrayQueue {

    private int maxsize;//队列的长度
    private int[] queue;//数组模拟队列,数据就放在数组里
    private int front = -1;//指向队列的队头前一个位置
    private int rear = -1;//指向队列的队尾

    /**
     * 构造器
     * @param maxsize
     */
    public ArrayQueue(int maxsize) {   
        this.maxsize = maxsize;
        queue = new int[this.maxsize];
    }
}

清空队列

    public void Clear() {
        front = -1;
        rear = -1;
    }

判断队列满

    public boolean isFull() {
        return rear == maxsize-1;
    }

判断队列空

    public boolean isKong() {
        return front == rear;
    }

入队操作

    public void add(int value) {
        if (isFull()) {
            System.out.println("队列已满,无法往里添加元素");
            return;
        }
        rear++;
        queue[rear] = value;

    }

出队操作

    public int get() {
        if (isKong()) {
            throw new RuntimeException("队列是空的");
        }

        front++;
        return queue[front];
    }

若队列存在且非空,返回队列头部元素


    public int GetHead() {
        if (isKong()) {
            throw new RuntimeException("队列是空的,无法获取元素");
        }

        return queue[front + 1];
    }

遍历队列元素


    public void show() {
        if (isKong()) {
            throw new RuntimeException("队列是空的,无法获取元素");
        }

        for (int i = front+1; i <= rear;i++) {
            System.out.println("queue["+i+"] = "+ queue[i]);
        }
    }

获取队列元素个数

    public int Length() {
        if (isKong()) {
            throw new RuntimeException("队列是空的,无元素");
        }
        int count = 0;
        while (front < rear) {
            count++;
            front++;
        }
        return count;
    }

优化

我们在使用时,会发现顺序结构的队列会存在一种问题,即数组只能用一次,达不到复用的效果。 举例来说:

  1. 假设有一个长度为3的数组,初始时,front指针和rear指针分别指向数组下标为-1的位置
  2. 入队a1,a2,a3front指向下标为-1的位置,rear指向下标为1的位置
  3. 出队a1元素,则front指向下标为0的位置,rear不变
  4. 再添加元素a3到队列中,因为此时数组末尾元素已满,如果再添加元素就会导致数组越界的错误,可实际上,我们的队列在下标0处还是空闲的,这种现象就叫做假溢出
  5. 再比如说,你上了公交车,发现后排没有空座了,但前排还有两个空座,正常来说,你肯定不会直接下车等下一个后排有空座的车,而是会坐到前排有空座的位置上
  6. 所以我们如果需要解决这种假溢出的问题,就需要使用一种方法,即当后面满了,我们直接从头开始,也就是头尾相接的循环。我们就将这种头尾相接的顺序存储结构称为循环队列

头尾相接的顺序存储结构 (环形队列)

思路分析

  1. 尾部索引的下一个为头索引时表示队列已满,即将队列空出一个作为约定,也就是说,当数组中还有一个空闲单元时,我们就认为此队列已经满了。即(rear + 1) % maxsize == front;
  2. 队列为空:rear == front 3. front变量含义做一个调整:front指向队列的第一个元素,front初始值为0
  3. rear变量含义也做一个调整:rear指向队列的队尾的后一个元素,因为要空出一个空间单元,rear初始值也为0
  4. 队列中有效的元素个数:
  • rear > front时,此时队列元素个数为rear - front;
  • front > rear时,此时队列分成两段,一段是maxsize - front,一段是0 + rear;即rear - front + maxsize
  • 因此通用的计算队列长度的公式为:(rear + front + maxsize) % maxsize; img-20220530225805.png

有了这些思路,具体实现代码就不难了

具体操作代码

环形队列的结构和初始化

class ArrayQueue {

    private int maxsize;//队列的长度
    private int[] queue;//数组模拟队列,数据就放在数组里
    private int front = 0;//队列队头的前一个位置
    private int rear = 0;//队列的队尾

    /**
     * 构造器
     * @param maxsize
     */
    public ArrayQueue(int maxsize) {
        this.maxsize = maxsize;
        queue = new int[this.maxsize];
    }
}

入队列

public void push(int value) {
        if (isFull()) {
            System.out.println("队列已满,无法往里添加元素");
            return;
        }

        queue[rear] = value;
        rear = (rear + 1) % maxsize;
    }

出队列

public int pop() {
        if (isKong()) {
            throw new RuntimeException("队列是空的");
        }

        int val = queue[front];
        front = (front + 1) % maxsize;
        return val;
    }

单是顺序结构,如果不是循环队列,时间复杂度还不是很高,但顺序队列又有可能发生数组溢出的情况,所以我们还是要说一下不需要担心队列长度的链式存储结构

队列的链式顺序结构(链表模拟队列)

思路分析

  • 队列的链式存储结构,就是线性表的单链表,只不过它只能尾进头出而已,我们将它简称为链队列
  • 为了操作上的方便,我们将队头指针指向链队列的头节点,而队尾指针指向链队列的终端结点
  • 为了更方便理解链表,我们又引入一个虚拟头结点指针

具体操作代码

链队列的结构

 public static class LinkListQueue {
            private int data;
            private LinkListQueue next;

            public int getData() {
                return data;
            }

            public void setData(int data) {
                this.data = data;
            }

            public LinkListQueue getNext() {
                return next;
            }

            public void setNext(LinkListQueue next) {
                this.next = next;
            }


        }

        public LinkListQueue front;//队列队头
        public LinkListQueue rear;//队列队尾
        public LinkListQueue header = null;

    /**
     * 构造器
      */
    public LinkListQueueDemo() {
        header = new LinkListQueue();
        front = new LinkListQueue();
        rear = new LinkListQueue();
        front = header;
        rear = header;
    }

判断队列是否为空

    public boolean isKong () {
       return rear == header;
    }

入队操作

public void push (int val) {
            LinkListQueue s = new LinkListQueue();
            s.setData(val);
            s.setNext(null);

            if (isKong()) {
                rear = s;
                header.next = s;
                front.next = s;
            }else {

                //把拥有元素val的新结点s赋值到原链表尾结点的后继
                rear.next = s;

                //把当前的s设置为队尾结点,rear指向s
                rear = s;
            }

        }

出队操作

public int pop () {
            int o = 0;
            if (isKong()) {
                throw new RuntimeException("队列是空的,里面没有元素呢~");
            }else {
                o = front.next.getData();
                //队列只有一个元素
                if (front.next == rear) {
                    front.next = header;
                    rear = header;
                }else {//队中大于一个元素
                    front.next = front.next.next;
                }
            }
            return o;
        }

获取队列元素个数

        public int getQueueSize () {
            LinkListQueue temp = header;
            if (isKong()) {
                throw new RuntimeException("队列是空的,里面没有元素呢~");
            }

            int count = 0;
            while (temp.next != null) {
                count++;

                temp = temp.next;
            }
            return count;
        }

总结

  • 时间上来说,循环队列是事先申请好空间,使用期间不释放,而对于链队列来说,每次申请和释放结点也会存在一些时间开销
  • 空间上来说,循环队列必须有一个固定的长度,所以就会存在存储元素个数和空间浪费的问题,而链队列就不存在这个问题,所以从空间上来说,链队列更加灵活
  • 总的来说,在确定了队列长度最大值的情况下,建议使用循环队列,如果你无法估计队列的长度,则用链队列