队列【JAVA】

发布于:2022-12-18 ⋅ 阅读:(352) ⋅ 点赞:(0)

鲁迅先生曾经说过:数据结构来源于生活,又高于生活。今天要讲的这个队列,在我们的日常生活中随处可见,例如在饭堂排队打饭、在银行排队办理业务,这些就是活生生的例子。队列没有那么的复杂,但它却是一种应用很广泛的结构。搬好小板凳,听我慢慢道来!


简介

引用百度百科中关于队列的定义:队列是一种特殊的线性表,特殊之处在于它只允许在表的前端(front)进行删除操作,而在表的后端(rear)进行插入操作,和栈一样,队列是一种操作受限制的线性表。进行插入操作的端称为队尾,进行删除操作的端称为队头。

队列是对数据的”存”和”取”操作有着严格要求的一种线性存储结构。队列的两端都有”开口”,且要求数据只能从一端进,从另一端出。记住一点即可:先入先出

简单的说,就是你到银行去办理业务,办理业务的柜台都是叫号的,先到先取了号的就能够先办理没有人可以插队,这就是先入先出。


应用场景

在日常生活中关于队列这种数据结构的应用场景就很多,例如:医院的挂号系统、银行排队办理业务等等。在计算机或计算机之间为了提高工作效率,常常是采用队列机制,避免线程间、程序间的冲突。根据队列机制作用的不同,队列还可以分为工作队列机制、消息队列机制 。


代码实现

一般,队列有两种实现方式,数组实现与链表实现。

这里使用数组来模拟队列,具体代码实现使用的是java语言。队列还有普通队列、环形队列之分,我们直接跳过普通队列,普通队列有一些缺点在实际的应用中也不会使用。环形队列是普通队列的升级版本,针对普通队列复用性差的缺点进行了改进。


环形队列的实现方法

基本操作:队列应该包含但不限于以下操作:入队、出队、获取队列头、求队列长度、判空、判满、遍历。

头尾指针:队列的一大特点是先入先出,这就要求队列元素的添加操作在队列的一端进行,队列元素的删除操作在队列的另一端进行,需要使用两个指针(变量)来标记队列的前后端的下标。队列头指针,使用front变量;队列尾指针,使用rear变量。

队列容量:队列数据可以使用一维数组进行存储,队列的容量使用maxSize变量记录。(即数组长度=maxSize)。

实现闭环:将队列的存储区想象成一个头尾相连的圆环,当队尾指针rear到达数组的最大容量时,如果还有元素入队并且前边有剩余的空间,则队尾指针rear又指向前边,形成一个闭环(逻辑上形成一个闭环)。那怎么实现队尾指针rear又重新指向前边,形成一个闭环呢?每次执行入队操作时都判断一下rear是否达到了数组的最大容量?不不不,有一个很巧秒的方法:由于front(头指针)、rear(尾指针)始终要在数组内,即front或rear < Array.length=maxSize,可以使用模运算%实现,模运算可以简单理解为求余数。尾指针的后移使用该表达式实现rear=( rear+1 ) % maxSize结果始终在0~maxSize内(包括前者不包括后者)。传送门:什么是模运算

实现闭环这里是不是开始挠头了?那就对了,不要忘记了标题,哈哈。跟着下边的代码敲一遍可能就理解了,仅仅靠看就想理解透有点困难,要多思索,多练习。


总结一下:

  1. 基本操作。入队、出队、获取队列头、求队列长度、判空、判满、遍历。

    • 入队操作addQueue()
    • 出队操作getQueue()
    • 获取队列头headQueue()
    • 获取队列长度getSize()
    • 判空checkEmpty()
    • 判满checkFull()
    • 遍历showQueue()
  2. 变量定义。队列头指针

    front
    

    队列尾指针

    rear
    

    队列容量

    maxSize
    
    • 队列头指针front,指向队列的第一个元素,初始值为0(可以理解为,该变量存放队列中第一个元素在数组中的下标)。

    • 队列尾指针rear,指向队列的最后一个元素的后一个位置,初始值为0

    • 队列的容量使用maxSize变量记录,通过构造方法进行定义。

class CircleQueue{
	//表示队列的最大容量
	private int maxSize;
	//头指针
	private int front;
	//尾指针
	private int rear;
    //该数组用于存放队列的数据
	private int[] arr;
	public CircleQueue(int maxSize) {//构造方法
		this.maxSize=maxSize+1;
		this.arr=new int[this.maxSize];
		this.front=0;
		this.rear=0;
	}
	public void addQueue(int value) {//入队列操作
    ...
	}
	public int getQueue() {//出队列操作
    ...
	}
	public void checkEmpty() {//判断队列是否为空
    ...
	}
	public void checkFull() { //判断队列是否已满
    ...
	}
	public void showQueue() {//显示队列中的所有数据
    ...
	}
	public int getSize() {//计算队列中元素个数
    ...
	}
	public int headQueue() {//查看队列头的数据
    ...
	}
}

构造方法中为什么加1,看后边的判空、判满操作的解释就知道了。

入队操作

入队操作,方法名记为“addQueue()”。

入队操作,思路:

  1. 队列尾指针rear,指向队列的最后一个元素的后一个位置,可直接存储数据到此处。
  2. 将尾指针往后移

rear尾指针,指向的是队列最后一个元素的后一个位置,故直接赋值。

public void addQueue(int value) {//入队列操作
		checkFull();//判断队列是否已满
		arr[rear]=value;
		rear=( rear+1 ) % maxSize;//尾指针rear后移一位
}

尾指针rear后移一位,为什么使用模运算,请回顾一下前边关于环形队列的实现闭环操作的解释。

出队操作

出队操作,方法名记为“getQueue()”。

出队操作,思路:

  1. 取出数据存放到变量value
  2. 头指针后移
  3. 返回数据
public int getQueue() {//出队列操作
	checkEmpty();//判断队列是否为空
	int value = arr[front];
	front =  (front+1) % maxSize;//头指针front后移一位
	return value;
}

判空、判满操作

判断队列是空还是满,有一点点复杂。我们可以通过rear==front来判断队列是否为空。但是这里有一个问题:假设一直都是执行入队列操作,没有执行过出队列操作。根据前边对于环形队列的实现方法的描述,当队列满了的时候,front将会指向0,也就是说此时rear==front,这时候没法区分是空还是满。


解决方法还是有的,这里有两个方法:

  1. 添加一个变量,当执行入队列操作时加1,执行出队列操作时减1,通过判断该变量是否为正数再结合rear==front,就可以区分队列是空还是满。
  2. 队列空出一个元素的存储空间,规定(rear+1)==front表示队列为满。

方法一,多出来了一个变量,增加了系统开销,不推荐。这里采用第二种方法。

判空操作,方法名记为“checkEmpty()”。

判空操作,思路:

  • 通过判断rear==front
public void checkEmpty() {//判断队列是否为空
	if(rear==front) {
		throw new RuntimeException("队列为空,不能取数据");
	}
}

判满操作,方法名记为“checkFull()”。

判满操作,思路:

  • 通过判断(rear+1)==front
public void checkFull() { //判断队列是否已满
	if(((rear+1)-front)%maxSize==0) {
		throw new RuntimeException("队列已满,不能加入数据!");
	}
}

图中(f)即为满状态,已无法执行入队操作添加数据,牺牲了一个存储空间。

获取队列长度操作

遍历操作,方法名记为“getSize()”。

遍历操作,思路:

  • 队列长度,通过式子(rear - front + maxSize) % maxSize来计算
public int getSize() {//计算队列中元素个数
	checkEmpty();//判断队列是否为空
	return (rear - front + maxSize) % maxSize;
}

队列头指针front,指向队列的第一个元素,初始值为0。队列尾指针rear,指向队列的最后一个元素的后一个位置,初始值为0。rear-front即可得到队列中元素的个数,可以尝试代入几个数据测试一下。加maxSize可以看作是给模运算加个绝对值,防止出现负数。传送门:什么是模运算

队列中没有数据:	rear=front=0;	队列中有rear-front=0个数据
队列中有1个数据:	rear=1;front=0;	队列中有rear-front=1个数据
队列中有2个数据:	rear=2;front=0;	队列中有rear-front=2个数据
执行出队操作,队列中剩下1个数据:rear=2;front=1;	队列中有rear-front=1个数据
...

遍历操作

遍历操作,方法名记为“showQueue()”。

遍历操作,思路:

  1. 获取队列中元素的个数(队列长度)
  2. 遍历数组
public void showQueue() {//显示队列中的所有数据
	checkEmpty();//判断队列是否为空
	for(int i=front+getSize()-1;i>=front;i--) {//getSize()方法,获取队列中元素个数
		System.out.printf("arr[%d]=%d\n",i%maxSize,arr[i%maxSize]);
	}
}

遍历操作中的for循环判断条件可以改成for(int i=front; i<front+getSize(); i++),这样子更加容易理解(我是为了实现先加入的数据打印在底下的效果)。队列头指针front,指向队列的第一个元素,从front开始打印出队列中所有的数据。队列中数据的个数由getSize()方法获得,由于front+size()有可能会大于maxSize,为了防止出现数组越界异常i不能直接使用,0<=i%maxSize<maxSize(不理解为什么使用模运算,请回顾一下前边关于环形队列的实现闭环操作的解释)。

获取队列头操作

获取队列头操作,方法名记为“headQueue()”。

获取队列头操作,思路:

  • 队列头指针front,指向队列的第一个元素。
public int headQueue() {//查看队列头的数据
	checkEmpty();//判断队列是否为空
	return arr[front];//头指针,指向队列的第一个元素
}

测试

准备工作

新建一个测试类Test。

初始化一个容量maxSize为3的队列,打印出一个菜单让用户选择相应的操作。

import java.util.Scanner;
public class Test {
	public static void main(String[] args) {
		//初始化一个队列
		CircleQueue queue = new CircleQueue(3);//队列最大容量为3
		char key = ' ';//接受用户输入数据
		boolean loop = true;//菜单循环判断条件
		//打印出一个菜单
		while(loop) {
			System.out.println("\n  s(show):显示队列");
			System.out.println("  e(exit):退出程序");
			System.out.println("   a(add):添加数据到队列");
			System.out.println("   g(get):从队列取出数据");
			System.out.println("  h(head):查看队列头的数据");
			System.out.println("n(number):获取队列长度");
			System.out.print("请选择:");
			key = new Scanner(System.in).next().charAt(0);//接受用户输入的字符
			switch(key) {
			case 's':
				try {
					queue.showQueue();
				} catch (Exception e1) {
					System.err.println("队列为空,无数据!");
				}
				break;
			case 'a':
				System.out.print("请输入一个数据:");
				try {
					queue.addQueue(new Scanner(System.in).nextInt());
				} catch (Exception e1) {
					System.err.println("队列已满!");
				}
				break;
			case 'g':
				try {
					System.out.println("取出的数据是:"+queue.getQueue());
				} catch (Exception e) {
					System.err.println("队列为空,无数据!");
				}
				break;
			case 'h':
				try {
					System.out.println("队列头的数据是:"+queue.headQueue());
				} catch (Exception e) {
					System.err.println("队列为空,无数据!");
				}
				break;
			case 'n':
				try {
					System.out.println("队列长度为:"+queue.getSize());
				} catch (Exception e) {
					System.err.println("队列为空,无数据!");
				}
				break;
			case 'e':
				loop = false;
				break;
			default:
				break;
			}
		}
		System.err.println("程序已退出!");
	}
}

开始测试

我先偷偷的给队列添加三个数据10、20、30。打印队列,结果如下:

  s(show):显示队列
  e(exit):退出程序
   a(add):添加数据到队列
   g(get):从队列取出数据
  h(head):查看队列头的数据
n(number):获取队列长度
请选择:s
arr[2]=30
arr[1]=20
arr[0]=10

队列容量为3,继续添加数据40,显示队列已满无法继续添加:

  s(show):显示队列
  e(exit):退出程序
   a(add):添加数据到队列
   g(get):从队列取出数据
  h(head):查看队列头的数据
n(number):获取队列长度
请选择:a
请输入一个数据:40
队列已满!

现在取出两个数据,再重新添加数据40、50。(注意观察取出的数据是哪个,注意数组下标的变化)

  s(show):显示队列
  e(exit):退出程序
   a(add):添加数据到队列
   g(get):从队列取出数据
  h(head):查看队列头的数据
n(number):获取队列长度
请选择:s
arr[2]=30
arr[1]=20
arr[0]=10

请选择:g
取出的数据是:10

请选择:g
取出的数据是:20

请选择:a
请输入一个数据:40

请选择:a
请输入一个数据:50

请选择:s
arr[0]=50
arr[3]=40
arr[2]=30

先入先出,执行出队列操作后被取出的是10、20,30没有被取出来。

当队尾指针rear到达数组的最大容量时,如果还有元素入队并且前边有剩余的空间,则队尾指针rear又指向前边,形成一个闭环。所以继续添加数据50时,其在数组中的下标为0。


传送门:查看完整源码!
原文链接:队列

本文含有隐藏内容,请 开通VIP 后查看

网站公告

今日签到

点亮在社区的每一天
去签到