鲁迅先生曾经说过:数据结构来源于生活,又高于生活。今天要讲的这个队列,在我们的日常生活中随处可见,例如在饭堂排队打饭、在银行排队办理业务,这些就是活生生的例子。队列没有那么的复杂,但它却是一种应用很广泛的结构。搬好小板凳,听我慢慢道来!
简介
引用百度百科中关于队列的定义:队列是一种特殊的线性表,特殊之处在于它只允许在表的前端(front)进行删除操作,而在表的后端(rear)进行插入操作,和栈一样,队列是一种操作受限制的线性表。进行插入操作的端称为队尾,进行删除操作的端称为队头。
队列是对数据的”存”和”取”操作有着严格要求的一种线性存储结构。队列的两端都有”开口”,且要求数据只能从一端进,从另一端出。记住一点即可:先入先出。
简单的说,就是你到银行去办理业务,办理业务的柜台都是叫号的,先到先取了号的就能够先办理没有人可以插队,这就是先入先出。
应用场景
在日常生活中关于队列这种数据结构的应用场景就很多,例如:医院的挂号系统、银行排队办理业务等等。在计算机或计算机之间为了提高工作效率,常常是采用队列机制,避免线程间、程序间的冲突。根据队列机制作用的不同,队列还可以分为工作队列机制、消息队列机制 。
代码实现
一般,队列有两种实现方式,数组实现与链表实现。
这里使用数组来模拟队列,具体代码实现使用的是java语言。队列还有普通队列、环形队列之分,我们直接跳过普通队列,普通队列有一些缺点在实际的应用中也不会使用。环形队列是普通队列的升级版本,针对普通队列复用性差的缺点进行了改进。
环形队列的实现方法
基本操作:队列应该包含但不限于以下操作:入队、出队、获取队列头、求队列长度、判空、判满、遍历。
头尾指针:队列的一大特点是先入先出,这就要求队列元素的添加操作在队列的一端进行,队列元素的删除操作在队列的另一端进行,需要使用两个指针(变量)来标记队列的前后端的下标。队列头指针,使用front
变量;队列尾指针,使用rear
变量。
队列容量:队列数据可以使用一维数组进行存储,队列的容量使用maxSize
变量记录。(即数组长度=maxSize
)。
实现闭环:将队列的存储区想象成一个头尾相连的圆环,当队尾指针rear
到达数组的最大容量时,如果还有元素入队并且前边有剩余的空间,则队尾指针rear
又指向前边,形成一个闭环(逻辑上形成一个闭环)。那怎么实现队尾指针rear
又重新指向前边,形成一个闭环呢?每次执行入队操作时都判断一下rear
是否达到了数组的最大容量?不不不,有一个很巧秒的方法:由于front
(头指针)、rear
(尾指针)始终要在数组内,即front或rear
< Array.length=maxSize
,可以使用模运算%实现,模运算可以简单理解为求余数。尾指针的后移使用该表达式实现rear=(
rear+1 ) % maxSize
结果始终在0~maxSize
内(包括前者不包括后者)。传送门:什么是模运算。
实现闭环这里是不是开始挠头了?那就对了,不要忘记了标题,哈哈。跟着下边的代码敲一遍可能就理解了,仅仅靠看就想理解透有点困难,要多思索,多练习。
总结一下:
基本操作。入队、出队、获取队列头、求队列长度、判空、判满、遍历。
- 入队操作
addQueue()
- 出队操作
getQueue()
- 获取队列头
headQueue()
- 获取队列长度
getSize()
- 判空
checkEmpty()
- 判满
checkFull()
- 遍历
showQueue()
- 入队操作
变量定义。队列头指针
1
front
、队列尾指针
1
rear
、队列容量
1
maxSize
。
- 队列头指针
front
,指向队列的第一个元素,初始值为0
(可以理解为,该变量存放队列中第一个元素在数组中的下标)。 - 队列尾指针
rear
,指向队列的最后一个元素的后一个位置,初始值为0
。 - 队列的容量使用
maxSize
变量记录,通过构造方法进行定义。
- 队列头指针
1 |
class CircleQueue{ |
构造方法中为什么加1,看后边的判空、判满操作的解释就知道了。(我是传送门)
入队操作
入队操作,方法名记为“addQueue()
”。
入队操作,思路:
- 队列尾指针
rear
,指向队列的最后一个元素的后一个位置,可直接存储数据到此处。 - 将尾指针往后移
rear
尾指针,指向的是队列最后一个元素的后一个位置,故直接赋值。
1 |
public void addQueue(int value) {//入队列操作 |
尾指针rear
后移一位,为什么使用模运算,请回顾一下前边关于环形队列的实现闭环操作的解释。
出队操作
出队操作,方法名记为“getQueue()
”。
出队操作,思路:
- 取出数据存放到变量
value
中 - 头指针后移
- 返回数据
1 |
public int getQueue() {//出队列操作 |
判空、判满操作
判断队列是空还是满,有一点点复杂。我们可以通过rear==front
来判断队列是否为空。但是这里有一个问题:假设一直都是执行入队列操作,没有执行过出队列操作。根据前边对于环形队列的实现方法的描述,当队列满了的时候,front
将会指向0,也就是说此时rear==front
,这时候没法区分是空还是满。
解决方法还是有的,这里有两个方法:
- 添加一个变量,当执行入队列操作时加1,执行出队列操作时减1,通过判断该变量是否为正数再结合
rear==front
,就可以区分队列是空还是满。 - 队列空出一个元素的存储空间,规定
(rear+1)==front
表示队列为满。
方法一,多出来了一个变量,增加了系统开销,不推荐。这里采用第二种方法。
判空操作,方法名记为“checkEmpty()
”。
判空操作,思路:
- 通过判断
rear==front
1 |
public void checkEmpty() {//判断队列是否为空 |
判满操作,方法名记为“checkFull()
”。
判满操作,思路:
- 通过判断
(rear+1)==front
1 |
public void checkFull() { //判断队列是否已满 |
图中(f)即为满状态,已无法执行入队操作添加数据,牺牲了一个存储空间。
获取队列长度操作
遍历操作,方法名记为“getSize()
”。
遍历操作,思路:
- 队列长度,通过式子
(rear - front + maxSize) % maxSize
来计算
1 |
public int getSize() {//计算队列中元素个数 |
队列头指针front
,指向队列的第一个元素,初始值为0。队列尾指针rear
,指向队列的最后一个元素的后一个位置,初始值为0。rear-front
即可得到队列中元素的个数,可以尝试代入几个数据测试一下。加maxSize
可以看作是给模运算加个绝对值,防止出现负数。传送门:什么是模运算。
1 |
队列中没有数据: rear=front=0; 队列中有rear-front=0个数据 |
遍历操作
遍历操作,方法名记为“showQueue()
”。
遍历操作,思路:
- 获取队列中元素的个数(队列长度)
- 遍历数组
1 |
public void showQueue() {//显示队列中的所有数据 |
遍历操作中的for循环判断条件可以改成for(int i=front; i<front+getSize(); i++)
,这样子更加容易理解(我是为了实现先加入的数据打印在底下的效果)。队列头指针front
,指向队列的第一个元素,从front
开始打印出队列中所有的数据。队列中数据的个数由getSize()
方法获得,由于front+size()
有可能会大于maxSize
,为了防止出现数组越界异常i
不能直接使用,0<=i%maxSize<maxSize
(不理解为什么使用模运算,请回顾一下前边关于环形队列的实现闭环操作的解释)。
获取队列头操作
获取队列头操作,方法名记为“headQueue()
”。
获取队列头操作,思路:
- 队列头指针
front
,指向队列的第一个元素。
1 |
public int headQueue() {//查看队列头的数据 |
测试
准备工作
新建一个测试类Test。
初始化一个容量maxSize
为3的队列,打印出一个菜单让用户选择相应的操作。
1 |
import java.util.Scanner; |
开始测试
我先偷偷的给队列添加三个数据10、20、30。打印队列,结果如下:
1 |
s(show):显示队列 |
队列容量为3,继续添加数据40,显示队列已满无法继续添加:
1 |
s(show):显示队列 |
现在取出两个数据,再重新添加数据40、50。(注意观察取出的数据是哪个,注意数组下标的变化)
1 |
s(show):显示队列 |
先入先出,执行出队列操作后被取出的是10、20,30没有被取出来。
当队尾指针rear
到达数组的最大容量时,如果还有元素入队并且前边有剩余的空间,则队尾指针rear
又指向前边,形成一个闭环。所以继续添加数据50时,其在数组中的下标为0。