1. 队列
1.1 生活实例
公路上有一条单行隧道, 所有通过隧道的车辆只允许从隧道入口驶入, 从
隧道出口驶出, 不允许逆行。
因此, 要想让车辆驶出隧道, 只能按照它们驶入隧道的顺序, 先驶入的车辆先驶出, 后驶入的车辆后驶出, 任何车辆都无法跳过它前面的车辆提前驶出。
1.2 队列的介绍
队列是一个有序列表,可以用 数组 或是 链表 来实现。
- 遵循 先入先出 的原则 (First In First Out,简称
FIFO) :- 先存入队列的数据,要先取出
- 后存入的要后取出
- 队列的出口端叫作队头( front) , 队列的入口端叫作队尾( rear)
示意图:
◀ 队列的数组实现如下:
用数组实现时, 为了入队操作的方便, 把队尾位置规定为最后入队元素的下一个位置。
◀ 队列的链表实现如下:
1.3 队列的基本操作
1.3.1 入队
入队( enqueue) 就是把新元素放入队列中, 只允许在队尾的位置放入元素,新元素的下一个位置将会成为新的队尾。
1.3.2 出队
出队( dequeue) 就是把元素移出队列, 只允许在队头一侧移出元素, 出队元素的后一个元素将会成为新的队头。
1.3.3 数组队列实现代码
import java.util.Scanner;
public class ArrayQueue {
public static void main(String[] args) {
// 创建一个队列
ArrayQueueContainer queue = new ArrayQueueContainer(3);
char key=' '; // 接收用户输入
Scanner scanner=new Scanner(System.in);
boolean loop=true;
//输出一个菜单
while (loop){
System.out.println("s(show); 显示队列");
System.out.println("e(exit); 退出程序");
System.out.println("a(add); 添加数据到队列");
System.out.println("g(get); 从队列取出数据");
System.out.println("h(head); 查看队列头的数据");
key=scanner.next().charAt(0); // 接收一个字符
switch (key){
case 's':
queue.showQueue();
break;
case 'a':
System.out.println("输出一个数");
int value=scanner.nextInt();
queue.addQueue(value);
break;
case 'g': // 取出数据
try{
int res =queue.getQueue();
System.out.printf("取出的数据是%d\n",res);
}catch (Exception e){
System.out.println(e.getMessage());
}
break;
case 'h': // 查看队列头的数据
try{
int res =queue.headQueue();
System.out.printf("队列头的数据是%d\n",res);
}catch (Exception e){
System.out.println(e.getMessage());
}
break;
case 'e': // 退出
scanner.close();
loop=false;
break;
default:
break;
}
}
System.out.println("程序退出~~");
}
}
// 使用数组模拟队列----编写一个 ArrayQueue类
class ArrayQueueContainer{
private int maxSize; // 表示数组的最大容量
private int front; // 队列头
private int rear; // 队列尾
private int[] arr; // 该数据用于存放数据,模拟队列
// 创建队列的构造器
public ArrayQueueContainer(int arrMaxSize){
maxSize=arrMaxSize;
arr=new int[maxSize];
front=-1; // 指向队列头部,分析出 front 是指向队列头的前一个位置
rear=-1; // 指向队列尾,指向队列尾的数据(即就是队列最后一个数据)
}
// 判断队列是否满
public boolean isFull(){
return rear==maxSize-1;
}
// 判断队列是否为空
public boolean isEmpty(){
return rear==front;
}
// 添加数据到队列
public void addQueue(int n){
// 判断队列是否满
if(isFull()){
System.out.println("队列满,不能加入数据~~");
return;
}
rear++; // 让 rear 后移
arr[rear]=n;
}
// 获取队列的数据,出队列
public int getQueue(){
// 判断队列是否空
if(isEmpty()){
// 通过抛出异常
throw new RuntimeException("队列空,不能取数据");
}
front++; // front 后移
return arr[front];
}
// 显示队列的所有数据
public void showQueue(){
// 遍历
if(isEmpty()){
System.out.println("队列空的,没有数据~~");
return;
}
for (int i=0;i<arr.length;i++){
System.out.printf("arr[%d]=%d\n",i,arr[i]);
}
}
// 显示队列的头数据,注意不是取出数据
public int headQueue(){
// 判断
if(isEmpty()){
throw new RuntimeException("队列空的,没有数据~~");
}
return arr[front+1];
}
}
★ 产生的问题
█ 问题:如果不断出队, 队头左边的空间失去作用, 那 队列的容量 就会越来越小了。
█ 解决办法:用数组实现的队列可以采用循环队列的方式来维持队列容量的恒定
1.3.4 循环队列
假设一个队列经过反复的入队和出队操作, 还剩下2个元素, 在“ 物理 ” 上分布于数组的末尾位置。 这时又有一个新元素将要入队。
在数组不做扩容的前提下, 如何让新元素入队并确定新的队尾位置呢? 我们可以利用已出队元素留下的空间, 让队尾指针重新指回数组的首位。
这样一来, 整个队列的元素就“循环” 起来了。 在物理存储上, 队尾的位置也可以在队头之前。 当再有元素入队时, 将其放入数组的首位, 队尾指针继续后移即可。
一直到( 队尾下标+1) %数组长度 = 队头下标时, 代表此队列真的已经满了。
需要注意的是, 队尾指针指向的位置永远空出1位, 所以队列最大容量比数组长度小1。
把循环队列当作周期,效果图如下:
-
front 变量: 指向队列的第一个元素, 也就是说 arr[front] 就是队列的第一个元素,front 的初始值 = 0
-
rear 变量:指向队列的最后一个空位置. 因为希望空出一个空间做为约定,rear 的初始值 = 0
-
当队列满时,条件是 (rear + 1) % maxSize == front 【满】
-
对队列为空的条件, rear == front 空
-
当我们这样分析, 队列中有效的数据的个数 (rear + maxSize - front) % maxSize,这里之所以 取模 是为了 循环,也就是为了把下标循环接起来,因为出队front后移,入队rear后移【这里需要细品,最好自己手动带入数据去试一下】
要考虑 front 和 rear 的值不能超出 maxSize-1。
具体代码:
import java.util.Scanner;
public class CircleArrayQueue {
public static void main(String[] args) {
// 创建一个环形队列
CircleArray queue = new CircleArray(4); // 说明设置4,其队列的有效数据最大容量是3,rear要预留一个空位置
char key=' '; // 接收用户输入
Scanner scanner=new Scanner(System.in);
boolean loop=true;
//输出一个菜单
while (loop){
System.out.println("s(show); 显示队列");
System.out.println("e(exit); 退出程序");
System.out.println("a(add); 添加数据到队列");
System.out.println("g(get); 从队列取出数据");
System.out.println("h(head); 查看队列头的数据");
key=scanner.next().charAt(0); // 接收一个字符
switch (key){
case 's':
queue.showQueue();
break;
case 'a':
System.out.println("输出一个数");
int value=scanner.nextInt();
queue.addQueue(value);
break;
case 'g': // 取出数据
try{
int res =queue.getQueue();
System.out.printf("取出的数据是%d\n",res);
}catch (Exception e){
System.out.println(e.getMessage());
}
break;
case 'h': // 查看队列头的数据
try{
int res =queue.headQueue();
System.out.printf("队列头的数据是%d\n",res);
}catch (Exception e){
System.out.println(e.getMessage());
}
break;
case 'e': // 退出
scanner.close();
loop=false;
break;
default:
break;
}
}
System.out.println("程序退出~~");
}
}
class CircleArray{
private int maxSize; // 表示数组的最大容量
private int front; // 队列头,front 变量指向队列的第一个元素, 也就是说 arr[front] 就是队列的第一个元素,front 的初始值 = 0
private int rear; // 队列尾,rear 变量指向队列的最后一个空位置. 因为希望空出一个空间做为约定,rear 的初始值 = 0
private int[] arr; // 该数据用于存放数据,模拟队列
public CircleArray(int arrMaxSize){
maxSize=arrMaxSize;
arr=new int[maxSize];
}
// 判断队列是否满
public boolean isFull(){
return (rear+1)%maxSize==front;
}
// 判断队列是否为空
public boolean isEmpty(){
return rear==front;
}
// 添加数据到队列
public void addQueue(int n){
// 判断队列是否满
if(isFull()){
System.out.println("队列满,不能加入数据~~");
return;
}
// 直接将数据加入
arr[rear]=n;
// 将 rear 后移,这里必须考虑取模
rear=(rear+1)%maxSize;
}
// 获取队列的数据,出队列
public int getQueue(){
// 判断队列是否空
if(isEmpty()){
// 通过抛出异常
throw new RuntimeException("队列满,不能取数据~~");
}
// 这里需要分析出 front 是指向队列的第一个元素
// 1. 先把 front 对应的值保留到一个临时变量
// 2. 将 front 后移,考虑取模
// 3. 将临时保存的变量返回
int value=arr[front];
front=(front+1)%maxSize;
return value;
}
// 显示队列的所有数据
public void showQueue(){
// 遍历
if(isEmpty()){
System.out.println("队列空的,没有数据~~");
return;
}
// 思路:从 front 开始遍历,遍历多少个元素
for(int i=front;i<front+size();i++){
System.out.printf("arr[%d]=%d\n",i%maxSize,arr[i%maxSize]);
}
}
// 求出当前队列有效数据的个数
public int size(){
return (rear+maxSize-front)%maxSize;
}
// 显示队列的头数据, 注意不是取出数据
public int headQueue(){
// 判读
if (isEmpty()){
throw new RuntimeException("队列空的,没有数据~~");
}
return arr[front];
}
}