数据结构(二)-队列(对数组的重复利用)
数组模拟队列
队列的一个应用场景
银行叫号排队的案例
队列介绍
- 队列是一个有序列表,可以用数组或链表来实现。
- 遵循先入先出的原则。例如:银行先叫号的人先于后叫号的人办理业务。谁先叫号谁先办理,谁后叫号谁后办理。即:先进入队列的数据先取出,后进入队列的数据后取出。
- 示意图(使用数组模拟队列示意图)
数组模拟队列思路
-
队列本身是有序列表,若使用数组的结构来存储队列的数据,则队列数组的声明如上图,其中, maxSize 为队列最大容量。
-
队列的输出、输入分别是从队列的头部和尾部来处理,因此需要两个变量 front 和 rear 来记录队列前后端的位置。front 随着数据的取出而改变,rear 随着数据的输入而改变。
front:指向队列的头部数据的前一个位置
rear:指向队列的尾部数据 -
当我们将数据存入队列时称为"addQueue",addQueue 有两个步骤
① 将队列的尾部指针往后移一位:rear + 1;
② 当 rear == maxSize - 1;则队列已满,无法添加数据到队列中。反之,arr[++rear] = value。注意,添加一个数据只需要队尾后移一位即可。 -
当我们将数据从队列中取出时称为"getQueue",getQueue 有两个步骤
① 将队列的头部指针后移一位:front + 1;
② 当 front == rear;则队列为空,没有数据可取,否则,返回arr[++front]。
代码实现
public class ArrayQueueTest {
public static void main(String[] args) {
ArrayQueue queue = new ArrayQueue(3);
Scanner scanner = new Scanner(System.in);
boolean loop = true;
char selection = ' ';
while(loop) {
System.out.println("s(show)打印队列");
System.out.println("a(add)添加数据");
System.out.println("g(get)获取数据");
System.out.println("p(peek)查看队列的头部");
System.out.println("q(exit)退出程序");
System.out.print("请输入你的选择:");
selection = scanner.next().trim().charAt(0);
switch (selection) {
case 's':
queue.showQueue();
break;
case 'a':
System.out.print("请输入要添加的数据:");
int value = scanner.nextInt();
queue.addQueue(value);
break;
case 'g':
try {
int value1 = queue.getQueue();
System.out.printf("获取到的数据为:%d", value1);
System.out.println();
} catch (Exception e) {
System.out.println(e.getMessage());
}
break;
case 'p':
try {
queue.peek();
} catch (Exception e) {
System.out.println(e.getMessage());
}
break;
case 'q':
scanner.close();
loop = false;
break;
default:
break;
}
}
}
}
class ArrayQueue{
private int maxSize;
private int front;
private int rear;
private int[] arr;
public ArrayQueue(int maxSize) {
this.maxSize = maxSize;
front = -1;
rear = -1;
arr = new int[maxSize];
}
public boolean isFull() {
return rear == maxSize - 1;
}
public boolean isEmpty() {
return front == rear;
}
public void addQueue(int value) {
if(isFull()) {
throw new RuntimeException("队列已满,无法添加");
}
arr[++rear] = value;
System.out.println("添加成功");
}
public int getQueue() {
if(isEmpty()) {
throw new RuntimeException("队列为空~~");
}
return arr[++front];
}
public void showQueue() {
if(isEmpty()) {
System.out.println("队列为空~~");
}
for(int i = 0; i < arr.length; i++) {
System.out.print(arr[i] + "\t");
}
System.out.println();
}
public void peek() {
if(isEmpty()) {
throw new RuntimeException("队列为空~~");
}
System.out.println(arr[front + 1]);
}
}
数组模拟环形队列
针对上述数组模拟队列,在测试的时候发现,当队列数据已满时,无法添加数据,但是当取出一个数据时,还是提示队列已满。
问题分析并优化
目前数组模拟的队列的只能使用一次,没有达到复用的效果。
将这个数组使用一个算法,改进成 "环形队列":取模 %
思路分析1
> 注意
front:在数组模拟环形队列中指向数组头部的数据,初始值为0
rear:在数组模拟环形队列中执行尾部数据的后一位(跟数据模拟队列有区别),因为希望空出一个空间作为约定,初始值为0
- 数组模拟队列之所以不能复用的原因是因为,一旦 rear == maxSize - 1; 就判断为队列已满,但是在数据添加满之后,这个 rear 在数组模拟队列中就不会再发生改变,没有形成一个环形结构。
- 思路就是如何让这个 rear 在满了之后还能再回到起点重新来过(操场跑圈),就需要在rear == maxSize - 1之后,手动将 rear 的值值为 0;front 随着队列中数据的取出也会逐渐趋向于 maxSize - 1,所以也需要手动将 front 的值置为0,判断队列已满的条件就是 rear 所处的位置的下一个位置就是 front 所处的位置:即 rear + 1 = front;
- 手动将达到最大值置为0的思路理解起来较容易,实现起来较繁琐。
思路分析2(取模)
- 首先判断已满的条件为(rear + 1) % maxSize == front;maxSize 为数组的长度,而实际队列存储数据的个数为 maxSize - 1 个
这样取模的原因,如果向后面这样取,rear % maxSize == front;在还没向队列添加数据时,则等式成立,无法添加数据
- 判断为空的条件:rear == front;
- 当我们如上述分析时,这时队列中的有效数据个数为 (rear + maxSize - front) % maxSize;
+maxSize 的原因,是因为,如果出现 rear 比 front 快一圈的情况(跑圈),rear - front 就会出现负数,与需求相悖。
代码实现
public class CircleArrayQueueTest {
public static void main(String[] args) {
CircleArrayQueue queue = new CircleArrayQueue(4);
Scanner scanner = new Scanner(System.in);
boolean loop = true;
char selection = ' ';
while (loop) {
System.out.println("s(show)打印队列");
System.out.println("a(add)添加数据");
System.out.println("g(get)获取数据");
System.out.println("p(peek)查看队列的头部");
System.out.println("q(exit)退出程序");
System.out.print("请输入你的选择:");
selection = scanner.next().trim().charAt(0);
switch (selection) {
case 's':
queue.showQueue();
int front = queue.getFront();
int rear = queue.getRear();
System.out.printf("front=%d\n", front); // 在打印队列的时候可以看出front和rear的变化
System.out.printf("rear=%d\n", rear); // 从而可以理解size()函数的写法
break;
case 'a':
System.out.print("请输入要添加的数据:");
int value = scanner.nextInt();
queue.addQueue(value);
break;
case 'g':
try {
int value1 = queue.getQueue();
System.out.printf("获取到的数据为:%d", value1);
System.out.println();
} catch (Exception e) {
System.out.println(e.getMessage());
}
break;
case 'p':
try {
queue.peek();
} catch (Exception e) {
System.out.println(e.getMessage());
}
break;
case 'q':
scanner.close();
loop = false;
break;
default:
break;
}
}
}
}
class CircleArrayQueue {
private int maxSize;
private int front; // 指向队列头部的数据
private int rear; // 指向队列尾部的后一个位置,预留一个空位,以方便下面的取余操作
private int[] arr;
public CircleArrayQueue(int maxSize) {// maxSize为数组长度,不是队列的长度
this.maxSize = maxSize;
arr = new int[maxSize];
}
public boolean isFull() {
// 不写(rear+1)的话就会出现rear=0;front=0;还没添加数据就判断为队列为满
return (rear + 1) % maxSize == front; // 队列实际存储的数据个数最多为maxSize-1个
}
public boolean isEmpty() {
return front == rear;
}
public void addQueue(int value) {
if (isFull()) {
System.out.println("队列已满,无法添加");
return;
}
arr[rear] = value;
rear = (rear + 1) % maxSize; // 防止索引越界
}
public int getQueue() {
if (isEmpty()) {
throw new RuntimeException("队列为空~~");
}
int value = arr[front];
front = (front + 1) % maxSize; // 防止索引越界
return value;
}
public void showQueue() {
if (isEmpty()) {
System.out.println(("队列为空~~"));
return;
}
for (int i = front; i < front + size(); i++) {
System.out.printf("arr[%d]=%d\n", i % maxSize, arr[i % maxSize]);
}
}
public int size() {
// 写rear + maxSize是为了防止出现rear在第二圈,front在第一圈的情况
return (rear + maxSize - front) % maxSize;
}
public void peek() {
if (isEmpty()) {
throw new RuntimeException("队列为空~~");
}
System.out.println(arr[front]);
}
public int getFront() {
return front;
}
public int getRear() {
return rear;
}
}
本篇随笔内容是来自于学习尚硅谷韩顺平老师的数据结构和算法课(java版)的笔记,如有整理疏漏或错误之处,请大家多多指出