为什么循环队列要浪费一个存储空间

  • 什么是队列
    • 队空和队满
  • 队列的实现
    • 数组实现队列
    • 链表实现队列
    • 假溢出问题
  • 循环队列
    • 队空和队满
    • 实现循环队列
  • 队列实战
    • 两个栈实现队列
  • 总结

什么是队列

队列和数组,链表,栈一样都是属于线性数据结构,而队列又和栈比较相似,都属于操作受限的数据结构,其中栈是“后入先出”,而队列是“先进先出”。

和栈一样,队列也仅有两个基本操作:入队和出队(栈则是入栈和出栈)。往队列中放元素称之为入队,往队列中取元素称之为出队,然而相对于栈来说,队列又会复杂一点。

队列中通常需要定义两个指针:headtail(当然,也有称之为:frontrear)分别用来表示头指针和尾指针。初始化队列时,headtail 相等,当有元素入队时,tail 指针往后移动一位,当有元素出队时,head 指针往后移动一位。

队空和队满

根据上面的队列初始化和入队出队的过程,我们可以得到以下三个关系:

  • 初始化队列时:head=tail=-1(这里也可以设置为 0,看具体实现)。
  • 队列满时:tail=size-1(其中 size 为初始化队列时队列的大小)。
  • 队列空时:head=tail(比如上图中的第一和第三个队列)。

队列的实现

和栈一样,队列也可以通过数组或者链表来实现,通过数组来实现的队列我们称之为“顺序队列”,通过链表实现的队列称之为“链式队列”。

数组实现队列

package com.lonely.wolf.note.queue;/*** 基于数组实现自定义单向队列。FIFO(first in first out)先进先出* @author lonely_wolf* @version 1.0* @date 2021/12/26* @since jdk1.8*/
public class MyQueueByArray<E> {public static void main(String[] args) {MyQueueByArray myQueue = new MyQueueByArray(3);System.out.println("队列是否为空:" + myQueue.isEmpty());//输出:truemyQueue.enqueue(1);myQueue.enqueue(2);myQueue.enqueue(3);System.out.println("队列是否已满:" + myQueue.isFull());//输出:trueSystem.out.println("第1次出队:" + myQueue.dequeue());//输出:1System.out.println("第2次出队:" + myQueue.dequeue());//输出:2System.out.println("第3次出队:" + myQueue.dequeue());//输出:3System.out.println("队列是否为空:" + myQueue.isEmpty());//输出:trueSystem.out.println("队列是否已满:" + myQueue.isFull());//输出:true}private Object[] data;private int size;//队列长度private int head;//队列头部private int tail;//队列尾部public MyQueueByArray(int size) {//初始化this.size = size;data = new Object[size];head = -1;tail = -1;}/*** tail=size-1 表示队列已满* @return*/public boolean isFull(){return tail == size - 1;}/*** head=tail表示队列为空* @return*/public boolean isEmpty(){return head == tail;}/*** 元素入队,tail指针后移一位* @param e* @return*/public boolean enqueue (E e){if (isFull()){return false;}data[++tail] = e;//tail 指针后移return true;}/*** 元素出丢* @return*/public E dequeue (){if (isEmpty()){return null;}E e = (E)data[++head];//出队return e;}
}

链表实现队列

有了上面基于数组实现的简单队列例子,基于链表实现队列相信大家也能写出来,如果用链表实现队列,我们们同样需要head 指针和 tail 指针。其中 head 指向链表的第一个结点,tail 指向最后一个结点。

入队时:

tail.next = newNode;
tail = tail.next;

出队时:

head = head.next;

假溢出问题

我们回到上面数组实现的例子中,我们发现最后的两个输出语句两个都是 true,也就是队列又是空的又是满的,这看起来是一个自相矛盾的事情却在队列里面出现了,我们来看看这时候队列的示意图:

通过上图中我们发现,因为 tail=size-1,所以队列是满的;而此时又同时满足:head=tail,所以根据上面队空的条件,我们又可以得到当前队列是空的,也就是当前队列是没有元素的。这时候我们已经无法继续入队了,但是这时候队列中的其实是有空间的,有空间却不能被利用,这就是单向队列的“假溢出”问题。

那么如何解决这个问题呢?解决这个问题有两个思路。

  • 思路一

前面我们学习数组的时候提到了,当数组中删除一个元素,那么这个位置之后的所有元素都需要往前移动一位,所以队列中也是同理,假如我们用数组来实现的话,当元素出队时,我们把所有元素都往前移,不过这样因为每次都需要搬移数据,导致入队的时间复杂度是 O(n)

  • 思路二

因为搬移数据会影响到入队效率,那么如何不搬移数据又能将队列的空闲空间利用起来呢?答案也很简单,那就是当再次有新元素入队时,我们把 tail 指针又重新移动到队列的最开头位置,这样也能避免出现“假溢出”问题,同时时间复杂度仍然保持为 O(1)

循环队列

上面提到的第二种思路来解决单向队列的“假溢出”问题,实际上就构成了一个循环队列。而上面单向队列判断当前队列是否队满时仅需要满足 tail=size-1 即可,但是循环队列肯定不行,因为当 tail=size-1 时继续添加元素,tail 可能可以移动到 size=0 的位置,所以如果要实现循环队列最关键还是需要确定好队空和队满的条件。

队空和队满

在循环队列中,当初始化队列时,一般会将其设置为 0,而队空条件仍然是 head=tail,循环队列最关键的是如何确定队满条件。

下图是初始化一个长度为 6 的队列以及连续入队 5 个元素后的循环队列中 headtail 指针示意图:

上图中第二个队列表示的是入队 5 个元素之后 tail 指针的位置,这时候如果继续入队,那么 tail 只能指向队列开头也就是元素 1 所在的位置,此时会得到下面这个示意图的场景:

这时候发现 head=tail,和队空时的条件冲突了,那么如何解决这个问题呢?主要有三种解决办法:

  1. (tail+1)%size=head 时就表示队列已满,这时候 tail 所在位置为空,所以会浪费一个空间(也就是第一幅图中第二个队列的场景就算队列已满)。
  2. 新增一个容量 capacity 字段,并进行维护,当 capacity=size 表示队列已满。
  3. 和第二个办法差不多,我们可以再维护一个标记,或者当 head=tail 时同步判断当前位置是否有元素来判断当前是队空还是队满。

这三种思路我们发现都各有缺点:方法 1 会浪费一个空间;方法 2 每次入队和出队时候都需要维护 capacity 字段;方法 3 就是不论队列空或者队列满时都要多一次判断方式。

相互比较之下,其实方法一的思路就是空间换时间,而另外两种办法当数据量并发量很大时,多一次判断其实也是会对性能有所影响,常规的循环链表会采用方法 1 进行处理,也就是选择浪费一个空间的方式。当然大家在实际开发过程中也可以自行斟酌。

实现循环队列

下面我们就以方法 1 的思路基于数组来实现一个循环队列:

package com.lonely.wolf.note.queue;/**** 实现循环队列* @author lonely_wolf* @version 1.0* @date 2021/12/26* @since jdk1.8*/
public class MyCycleQueueByArray<E> {public static void main(String[] args) {MyCycleQueueByArray cycleQueue = new MyCycleQueueByArray(3);System.out.println("循环队列是否为空:" + cycleQueue.isEmpty());//输出:trueSystem.out.println("1是否入队成功:" + cycleQueue.enqueue(1));//输出:trueSystem.out.println("2是否入队成功:" + cycleQueue.enqueue(2));//输出:trueSystem.out.println("3是否入队成功:" + cycleQueue.enqueue(3));//输出:falseSystem.out.println("循环队列是否已满:" + cycleQueue.isFull());//输出:trueSystem.out.println("第1次出队:" + cycleQueue.dequeue());//输出:1System.out.println("第2次出队:" + cycleQueue.dequeue());//输出:2System.out.println("第3次出队:" + cycleQueue.dequeue());//输出:null,因为 3 入队失败System.out.println("循环队列是否为空:" + cycleQueue.isEmpty());//输出:trueSystem.out.println("循环队列是否已满:" + cycleQueue.isFull());//输出:false}private Object[] data;private int size;private int head;//队列头部private int tail;//队列尾部public MyCycleQueueByArray(int size) {this.size = size;data = new Object[size];head = 0;tail = 0;}public boolean isFull(){return (tail + 1) % size == head;}public boolean isEmpty(){return head == tail;}/*** 入队* @param e* @return*/public boolean enqueue (E e){if (isFull()){return false;}data[tail] = e;tail =  (tail + 1) % size;//注意这里不能直接 tail++,否则无法循环使用return true;}/*** 出队* @return*/public E dequeue (){if (isEmpty()){return null;}E e = (E)data[head];head =  (head + 1) % size;//head 也同样不能直接 head++return e;}
}

这时候我们发现,虽然队列空间有 3 个,但是实际上只能存放 2 个元素,而最后两条输出语句的输出结果也说明了循环队列不会出现单向队列的“假溢出问题”。

队列实战

队列其实在 Javajuc 中有广泛应用,比如 AQS 等,在这里,我们继续来看一看队列的相关算法题来加深对队列的理解。

两个栈实现队列

前面我们讲栈的时候,用了两个队列来实现栈,这道题却正好相反,是利用两个栈来实现队列。

LeetCode232 题:请你仅使⽤两个栈实现先⼊先出队列,队列应当⽀持⼀般队列⽀持的所有操作(push、pop、peek、empty)。

这道题目其实相比较之前两个队列实现栈还是会更简单一点,因为栈是后入先出,所以我们只需要将入队和出队使用不同的栈就可以解决了。

具体解题思路为:将⼀个栈当作输⼊栈,⽤于压⼊(push)传⼊的数据;另⼀个栈当作输出栈,⽤于 pop(出队) 和 peek(查看队列头部元素) 操作。 每次 poppeek 时,若输出栈为空则将输⼊栈的全部数据依次弹出并压⼊输出栈,再将元素从输出栈输出。

具体代码实现为:

package com.lonely.wolf.note.queue;import java.util.Stack;/*** LeetCode 232* 请你仅使⽤两个栈实现先⼊先出队列。队列应当⽀持⼀般队列⽀持的所有操作(push、pop、peek、empty):** void push(int x) 将元素 x 推到队列的末尾* int pop() 从队列的开头移除并返回元素* int peek() 返回队列开头的元素* boolean empty() 如果队列为空,返回 true ;否则,返回 false** 说明:* 你只能使⽤标准的栈操作 —— 也就是只有 push to top, peek/pop from top, size, 和 is empty 操作是合法的。* 你所使⽤的语⾔也许不⽀持栈。你可以使⽤ list 或者 deque(双端队列)来模拟⼀个栈,只要是标准的栈操作即可。** 解题思路* 将⼀个栈当作输⼊栈,⽤于压⼊ push 传⼊的数据;另⼀个栈当作输出栈,⽤于 pop 和 peek 操作。* 每次 pop 或 peek 时,若输出栈为空则将输⼊栈的全部数据依次弹出并压⼊输出栈,** @author lonely_wolf* @version 1.0* @date 2021/12/26* @since jdk1.8*/
public class MyQueueByTwoStack<Integer> {private Stack<Integer> inStack;//输入栈private Stack<Integer> outStack;//输出栈/*** 即入队:enqueue 操作* @param e*/public void push(Integer e){inStack.push(e);//压入输入栈}/*** 查看并移除队列头部元素,即:出队 dequeue 操作* @return*/public Integer pop(){if (!outStack.isEmpty()){//输出栈不为空则直接出栈return outStack.pop();}while (!inStack.isEmpty()){//输出栈为空,则检查输入栈outStack.push(inStack.pop());//输入栈不为空,则将其压入输出栈}if (!outStack.isEmpty()){//再次检查输出栈是否有元素出栈return outStack.pop();}return null;}/*** 查看队列头部元素,相比较 pop,这里只查看元素,并不移除元素**/public Integer peek(){if (!outStack.isEmpty()){return outStack.peek();}while (!inStack.isEmpty()){outStack.push(inStack.pop());}if (!outStack.isEmpty()){return outStack.peek();}return null;}/*** 队列是否为空* @return*/public boolean empty(){return inStack.isEmpty() && outStack.isEmpty();}
}

总结

本文主要讲述了队列这种操作受限的数据结构,文中通过一个例子说明了单向链表为什么会存在“假溢出“问题,并最终引出了循环链表,而循环链表的实现同样有三种不同思路,并通过以空间换时间的方法基于数组实现了一个简单的循环链表。最后我们还讲述了如何利用两个栈来实现一个队列。

为什么循环队列要浪费一个存储空间相关推荐

  1. java循环队列_Java版-数据结构-队列(循环队列)

    前情回顾 在上一篇,笔者给大家介绍了数组队列,并且在文末提出了数组队列实现上的劣势,以及带来的性能问题(因为数组队列,在出队的时候,我们往往要将数组中的元素往前挪动一个位置,这个动作的时间复杂度O(n ...

  2. 如何设计一个循环队列

    文章目录 前言 一.思路 二.代码实现 1.MyCircularQueue(k) 构造方法 2.Front 从队首获取元素 3.Rear 获取队尾元素 4.enQueue(value) 向循环队列插入 ...

  3. 力扣-设计一个循环队列

    设计应该支持以下操作: MyCircularQueue(k): 构造器,设置队列长度为 k . Front: 从队首获取元素.如果队列为空,返回 -1 . Rear: 获取队尾元素.如果队列为空,返回 ...

  4. 循环队列(Circular Queue)

    循环队列作用 为充分利用向量空间,克服"假溢出"现象的方法. 循环队列的原理 在环状顺序表中,最后一个位置(a[6])和第一个位置(a[0])紧挨着,这样做的好处是: 随着元素做入 ...

  5. 数据结构与算法(3-2)队列(顺序队列、循环队列与链队列)

    目录 一.顺序队列 1.存储结构 2.入队和出队 总代码 二.循环队列 总代码: 三.链队列 1.存储结构 2.入队和出队 总代码 一.顺序队列 队列特征:先进后出.后进后出. 1.存储结构 //队列 ...

  6. 【Java】 LeetCode 622. 设计循环队列 (有关实现循环队列的讲解)

    题目: 设计你的循环队列实现. 循环队列是一种线性数据结构,其操作表现基于 FIFO(先进先出)原则并且队尾被连接在队首之后以形成一个循环.它也被称为"环形缓冲器". 循环队列的一 ...

  7. 【数据结构与算法】循环队列和链队列的比较

    前言 本文原本是一篇随感+笔记,被翻出来后就整理发了博客. 不曾想能上搜索头条,既如此,决定更新一下,加上必要的阐释,避免影响有需求的读者. (我这么理解大家,如果有需要的话,是不是可以考虑点个赞或者 ...

  8. socketmq 设置队列大小_LeetCode 622:设计循环队列 Design Circular Queue

    LeetCode 622:设计循环队列 Design Circular Queue 首先来看看队列这种数据结构: 队列:先入先出的数据结构 在 FIFO 数据结构中,将首先处理添加到队列中的第一个元素 ...

  9. C++数据结构:循环队列基本运算的实现

    实验要求 编写一个程序,以菜单形式实现循环队列的各种基本运算,并在此基础上设计一个主程序,完成如下功能: (1)初始化空队列 (2)建立循环队列 (3)入队 (4)出队 (5)判断队列是否为空,为空返 ...

最新文章

  1. HDU 2704 Bulletin Board
  2. TouTiao开源项目 分析笔记7 加载数据的过程
  3. 程序员终身发展规划---应对中国软件发展的大环境要求
  4. 【poi xlsx报错】使用POI创建xlsx无法打开
  5. 一行代码值 200 万?雷军公开小米新 Logo 引吐槽
  6. 从身体扮演到身份确认:谈谈EMI的人体自拍作品
  7. 序列计数(动态规划/自动机/前缀和优化)
  8. 计算机网络期末考长沙学院,校内用-第二套计算机网络试卷A..doc
  9. Linux ar命令
  10. 两间三层小型别墅图片_占地仅120平的现代风别墅,带KTV和健身房,引领时尚新潮流...
  11. Executesql 实例及介绍
  12. MFC Date Time Picker的使用
  13. star法则 java_STAR法则(示例代码)
  14. # Day8:类的方法、三大特征、装饰器、组合、多态、设计模式
  15. Windows 中包括的 USB 设备类驱动程序
  16. 淘宝开店经验 心得 攻略
  17. Hud检测之图像点数计算
  18. 骑行运动耳机哪个好,五款最适合在骑行中佩戴的耳机分享
  19. McNemar test麦克尼马尔检验
  20. spring listener详尽篇

热门文章

  1. 视频类APP体验报告
  2. 嗖嗖移动业务大厅小项目
  3. 移动开发福利:GMTC全球移动技术大会解决方案专场免费报名!
  4. 北京人都是什么文案鬼才,被他们的抗阳段子笑死了
  5. 分布式原理:一文了解 Gossip 协议
  6. Qt笔记11:qt如何设置应用程序图标和可执行程序图标
  7. 会员管理有哪些功能呢?
  8. テーブルTVARVとTVARVCについて
  9. 记录学习向量的铭心刻骨的句子
  10. NYOJ-71:独木舟上的旅行