数据结构笔记_02 数组模拟环形队列

对上文的数组模拟队列的优化方法是:将数组看做是一个环形的队列。(通过取模的方式来实现)

思路不难,主要是一些小算法。

环形队列有不同的实现思路,这里采用设置一个 ”状态位“ 的方法。
例如:循环队列长度为4,则包含3个元素位 + 1个状态位。
这个状态位,就是队列的最后一个元素(也就是队首元素的前一个元素)。

为啥需要这个状态位呢?

因为如果不留这一个空位,那队列为空和队列已满都表现为front=rear,就需要更复杂的逻辑区分。简言之,帮助我们区分队列为空、满的状态。

环形队列实现思路如下:
1.front 的初始值 = 0
front 变量的含义做一个调整: front 指向队列的第一个元素, 也就是说 arr[front] 就是队列的第一个元素。
2.rear 的初始值 = 0
rear 变量的含义做一个调整:rear 指向队列的最后一个元素的后一个位置(也就是队列最后一个位置). 因为希望空出一个空间,用来区分是 队列满 / 队列空。(下文将详细叙述)
3. 队列满时,条件是 (rear + 1) % maxSize == front
4. 队列为空时, rear == front
5. 队列中有效的数据个数 (rear + maxSize - front) % maxSize // rear = 1 front = 0
6. 我们就可以在原来的队列上修改得到一个环形队列。

注意:front、rear、状态位等,我们是在考虑它们之间的相对位置关系,比如状态位在front指向元素的前一位。又比如当(rear+1)%maxSize==front成立,则说明队列已满。

说这些为啥呢?因为千万要摆脱之前一个萝卜一个坑的观点,认为某个固定位置就是rear、front、状态位,这是错误的。在循环队列中,整体是动态变化的。

下面看一张图,理解环形队列的运行机制:
在这里插入图片描述

编写一个CircleArray类

1、定义基本量

	private int maxSize;// 表示数组的最大容量
	/*
	 * front 变量的含义做一个调整: front 就指向队列的第一个元素, 也就是说 arr[front] 就是队列的第一个元素。front 的初始值 = 0
	 */
	private int front;// 队列头
	/*
	 * rear 变量的含义做一个调整:rear 指向队列的最后一个元素的后一个位置. 因为希望空出一个空间用作状态位。rear 的初始值 = 0
	 */
	private int rear;// 队列尾
	private int[] arr;// 该数组用于存放数据,模拟队列

2、构造器:(因为已定义front、rear默认初始值为0,所以省略不写)

public CircleArray(int arrMaxSize) {
    
    
	maxSize = arrMaxSize;
	arr = new int[maxSize];
}

在主函数中调用:

CircleArray queue = new CircleArray(4);// 设置4,其队列的有效数据最大是3

3、判断队列是否满

本文拿长度为4的队列来举例,maxSize为4,只能存储3个元素:0,1,2(数组下标)。当rear(rear 指向队列的最后一个元素的后一个位置)的值为maxSize - 1等于3时,表示已经存满了,因为此时队列的最后一个元素下标为2,而2就是最后一个元素。rear+1(即maxSize)就表示rear此时指向了队列最后一个元素的后一个状态位。此时,通过与maxSize求模,值为0。实现了循环的效果。

注意!此时rear指向循环队列 队首元素的前一个元素,也就是队列的最后一个元素。

public boolean isFull() {
    
    
	return (rear + 1) % maxSize == front;// 当队列满时,rear总处在front的前一个位置。
}

4、判断队列是否空

	public boolean isEmpty() {
    
    
		return rear == front;
	}

注意!状态位的作用,在此体现。仅当rear和front指向相同,即循环队列中没有元素时,队列为空。

有两种情况二者指向相同:
1)一开始
2)循环队列中的元素全部出队后

举个例子:循环队列(长度为4)中,原本有两个元素,存储在下标为1,2的位置。此时,front 指向下标为1的位置。第一次出队,将front 此时指向的元素出队,并将其指向由1改为2。第二次出队,同上,将其指向改为3。这里就是命门!
我们都知道,rear是指向循环队列最后一个元素的后一个位置的。它指向下标为3的位置

这,就验证了第二种情况!

5、添加数据到队列

1)判断队列是否已满。
2)直接将数据加入,再将rear后移。这里需要用到一个取模的小算法,否则会出现数组越界!

	public void addQueue(int n) {
    
    
		// 判断队列是否满
		if (isFull()) {
    
    
			System.out.println("队列满,不能加入数据~");
			return;
		}
		// 直接将数据加入
		arr[rear] = n;
		// 将rear后移,这里必须考虑取模(实现环形)。否则会出现数组越界
		rear = (rear + 1) % maxSize;
	}

6、获取队列的数据,出队列

1)判空。
2)用一个临时变量保存front对应的值,再将front后移,再将临时存储的变量值返回。
这里为啥要一个临时变量呢?
因为若直接返回的话,会直接终止这个方法,也就没有将front后移的机会了。

	public int getQueue() {
    
    
		// 判断队列是否空
		if (isEmpty()) {
    
    
			// 通过抛出异常
			throw new RuntimeException("队列空,不能取数据");
		}
		// 这里需要分析出front 是指向队列的第一个元素
		// 1、先把front 对应的值保留到一个临时变量(若直接返回,则没有了将front 后移的机会了!)
		// 2、将front 后移,考虑取模,否则越界!
		// 3、将临时存储的变量返回
		int value = arr[front];
		front = (front + 1) % maxSize;
		return value;
	}

7、显示队列的所有数据

1)判空。
2)从front开始遍历,遍历队列中有效的数据个数

	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]);
		}
	}

这里for循环的判断语句中,用到一个求出有效数据个数的小算法,如下:

	public int size() {
    
    
		// rear = 1
		// front = 0
		// maxSize = 3
		return (rear + maxSize - front) % maxSize;
		// 其实有效个数始终等于rear-font的绝对值,%纯粹为了抵消负数
	}

8、显示队列的头数据,注意不是取出数据

1)判空。
2)返回front指向的数据。

public int headQueue() {
    
    
	// 判断
	if (isEmpty()) {
    
    
		throw new RuntimeException("队列空的,没有数据~~");
	}
	return arr[front];
}

完整源码

package com.huey.queue;

import java.util.Scanner;

public class CircleArrayQueue {
    
    
	public static void main(String[] args) {
    
    
		// 测试
		System.out.println("测试数组模拟环形队列的案例~~~");

		// 创建一个环形队列
		CircleArray queue = new CircleArray(4);// 说明设置4,其队列的有效数据最大是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) {
    
    // 快捷键:swi+Alt+/
			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();// reult;
					System.out.printf("取出的数据是%d\n", res);
				} catch (Exception e) {
    
    // getQueue抛出的异常会被catch抓住,在下面输出异常信息
					// TODO: handle exception
					System.out.println(e.getMessage());
				}
				break;
			case 'h':// 查看队列头的数据
				try {
    
    // 运行时的异常不一定非得捕获或者抛出,这里的try catch是为了防止出现了异常而终止程序。
					int res = queue.headQueue();
					System.out.printf("队列头的数据是%d\n", res);
				} catch (Exception e) {
    
    
					// TODO: handle exception
					System.out.println(e.getMessage());
				}
				break;
			case 'e':
				scanner.close();
				loop = false;
				break;

			default:
				break;
			}
		}
		System.out.println("程序退出~~");

	}

}

class CircleArray {
    
    
	private int maxSize;// 表示数组的最大容量
	/*
	 * front 变量的含义做一个调整: front 就指向队列的第一个元素, 也就是说 arr[front] 就是队列的第一个元素 
	 * front 的初始值 = 0
	 */
	private int front;// 队列头
	/*
	 * rear 变量的含义做一个调整:rear 指向队列的最后一个元素的后一个位置. 因为希望空出一个空间用作状态位。rear 的初始值 = 0
	 */
	private int rear;// 队列尾
	private int[] arr;// 该数组用于存放数据,模拟队列

	public CircleArray(int arrMaxSize) {
    
    
		maxSize = arrMaxSize;
		arr = new int[maxSize];
	}

	// 判断队列是否满
	public boolean isFull() {
    
    // maxSize-1就是队列的最后一个位置。rear==maxSize-1也就是说满了
		return (rear + 1) % maxSize == front;// 当队列满时,rear总处在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 对应的值保留到一个临时变量(若直接返回,则没有了将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() {
    
    
		// rear = 1
		// front = 0
		// maxSize = 3
		return (rear + maxSize - front) % maxSize;
		// 其实有效个数始终等于rear-font的绝对值,%纯粹为了抵消负数
	}

	// 显示队列的头数据,注意不是取出数据
	public int headQueue() {
    
    
		// 判断
		if (isEmpty()) {
    
    
			throw new RuntimeException("队列空的,没有数据~~");
		}
		return arr[front];
	}
}

运行分析

添加并显示一个数据。
在这里插入图片描述
再输入两个数据,并取出一个数据,查看队列此时的情况,可见,队首元素变成了arr[1]了。原来的arr[0]处的元素111,在被取出数据后,arr[0] 此时已变成状态位了。因为循环列表 存储数据的位置 + 状态位,是随着取出元素,而发生动态变化的(每取出一个元素,同步后移一位)。

在这里插入图片描述

下面输出队首元素,并尝试添加第四个元素,由于队列长度为4(3个元素位+1个状态位),故报错。
在这里插入图片描述
下面上大菜了,通过数组下标的变化,清晰可见其元素位的变化过程。
在这里插入图片描述

猜你喜欢

转载自blog.csdn.net/qq_45909299/article/details/113186510