轻松掌握队列操作
3.1 如何实现一个排队系统
在日常生活中,处处都能看到排队。在银行、医院这种场景中还会加入排队系统,通过系统叫号的方式解决拥堵问题。在虚拟游戏世界中,为了服务器限流,也会加入一些排队策略。这样的排队列表用的就是数据结构中的 —— 队列(queue) 存储
队列和栈一样,都有约束条件,不同的约束条件决定它们的不同的使用场景
队列和排队非常类似,大家排成一排就像队列存储的元素,以食堂打饭举例,永远是排在最前面的人先离队进入影院,后来进入队列的人排在最后
在计算机中,用先进先出来总结这个特征,或者缩写为 FIFO
(first in,first out)。约束条件为:
- 只能在末尾插入元素
- 只能读取开头的元素
- 只能移除开头的元素
3.2 队列的数组实现
队列可以从头部删除和尾部插入元素,因此需要两个指针分别标记头部和尾部位置,分别定义为 front
和 rear
front
指向的是即将删除的头部元素
rear
指向的是即将插入元素的位置
最开始的时候,front
和 rear
都指向第一个空元素,如下所示:
每添加一个元素时,让 rear 指针向右移动一位。每删除一个元素时,让 front 指针向右移动一位
在添加 a, b, c, d, e
五个元素后,数组的存储如下图所示:
我们继续进行添加和删除操作,在某一个时刻,数组可能出现如下情况:
发现:随着 front 的右移,数组的左侧空间全部浪费了
那该如何解决这个问题呢?
第一个方案:数组调整
每删除一次后,将右侧的元素依次左移
但这是一个不靠谱的方案,这让每次删除操作的时间复杂度为 O(N)
第二个方案:循环数组
循环数组是指数组末尾下一个元素并不是越界,而是第一个元素
用图表示如下:
如果使用循环队列,可以完美的解决队列导致空间浪费问题
代码实现
第一步:加入基本对象和变量
public class OrangeArrayQueue<T> {
// 前后两个指针
private int front;
private int rear;
// 底层存储数组
private T[] queue;
public OrangeArrayQueue(int size) {
this.front = 0;
this.rear = 0;
// 数组泛型的写法
this.queue = (T[]) new Object[size];
}
}
OrangeArrayQueue
提供了一个创建队列指定大小的构造函数。需要注意数组泛型的写法
第二步:队尾添加元素
// 队尾添加元素
public void add(T o) {
this.queue[this.rear] = o;
// 因为是循环队列,所以处理数组长度取余
int newRear = (rear + 1) % this.queue.length;
// 指针相遇表示队列已满,暂不考虑扩容情况
if (newRear == this.front) {
// 如果加入元素以后指针碰撞,则抛出越界提示
throw new IndexOutOfBoundsException("队列已满");
}
this.rear = newRear;
}
注意:
- 因为是循环链表,所以用取余的方式获取指针位置
- 使用头尾相碰作为队列已满条件
- 利用Java异常机制,抛出队列已满异常
为了方便查看队列数据,在这里加入一个 toString
方法将队列数据连接成字符串返回
public String toString() {
StringBuffer sb = new StringBuffer();
int i = this.front;
while (i != this.rear) {
sb.append(this.queue[i]);
sb.append(" ");
i++;
}
return sb.toString();
}
第三步:获取队列长度
长度计算公式如下:
int size = this.rear - this.front;
但循环数组有可能出现 rear < front
,所以如果出现小于的情况,可以加上数组的长度再进行相减。代码如下:
// 获取队列的长度
public int size() {
if (this.rear < this.front) {
return this.rear + this.queue.length - this.front;
}
return this.rear - this.front;
}
队列的数组实现代码完整版如下:
public class OrangeArrayQueue<T> {
// 前后两个指针
private int front;
private int rear;
// 底层存储数组
private T[] queue;
public OrangeArrayQueue(int size) {
this.front = 0;
this.rear = 0;
// 特别注意此处的数组泛型的写法
this.queue = (T[]) new Object[size];
}
// 队列尾部添加元素
public void add(T o) {
this.queue[this.rear] = o;
// 因此是循环队列,所以处理数组长度取余
int newRear = (rear + 1) % this.queue.length;
// 暂不考虑扩容情况
if (newRear == this.front) {
// 如果加入元素以后指针碰撞,则抛出越界提示
throw new IndexOutOfBoundsException("队列已满");
}
this.rear = newRear;
}
// 删除队列头部元素
public T remove() {
if (this.front == this.rear) {
throw new IndexOutOfBoundsException("队列为空,不允许remove");
}
T item = this.queue[this.front];
this.front = (this.front + 1) % this.queue.length;
return item;
}
// 获取队列中索引位置元素
public T get(int i) {
if (i < 0 || i >= this.size()) {
throw new IndexOutOfBoundsException("获取队列元素,越界");
}
int index = (i + this.front) % this.queue.length;
return this.queue[index];
}
// 获取队列的长度
public int size() {
if (this.rear < this.front) {
return this.rear + this.queue.length - this.front;
}
return this.rear - this.front;
}
public String toString() {
StringBuffer sb = new StringBuffer();
int i = this.front;
while (i != this.rear) {
sb.append(this.queue[i]);
sb.append(" ");
i++;
}
return sb.toString();
}
public static void main(String[] args) {
OrangeArrayQueue<Integer> queue = new OrangeArrayQueue<>(20);
queue.add(2);
queue.add(5);
queue.add(8);
System.out.println(queue.get(0));
System.out.println(queue.get(2));
System.out.println(queue.remove());
System.out.println(queue.remove());
System.out.println(queue.toString());
System.out.println(queue.size());
}
}
3.3 队列的链表实现
对于栈、队列这种类似的数据结构,用数组实现其实是一件非常繁琐的事情,主要因为两个原因:
- 数组天生对频繁的操作很不友好,每次插入删除操作都需要调整数组
- 数组连续空间存储特性,导致用数组实现的数据结构都存在越界或者扩容的问题
而链表是栈、队列底层存储最好的选择
如图所示:
- 为了方便在头部删除节点,需要一个 front 指针指向链表的第一个节点
- 为了方便在尾部插入节点,需要一个 rear 指针指向链表的最后一个节点
- 链表只能遍历统计节点个数,会额外有时间的开销,所以增加 size 用于存储节点个数
接下来,add、remove、get、size的方法实现就比较容易了
add:类似于在列表尾部加入第一个节点
remove:类似于链表如何删除第一个节点
get:遍历链表返回对应索引的值
size:直接返回变量size即可
完整代码如下:
public class OrangeLinkedQueue<T> {
// 前后两个指针
private Node<T> front;
private Node<T> rear;
private int size = 0;
// 队列尾部添加元素
public void add(T o) {
Node<T> node = new Node<>(o);
if (this.front == null) {
this.front = node;
} else {
this.rear.setNext(node);
}
this.size++;
this.rear = node;
}
// 删除队列头部元素
public T remove() {
if (this.front == null) {
throw new IndexOutOfBoundsException("队列为空,不能删除");
}
Node<T> temp = this.front;
this.front = temp.getNext();
temp.setNext(null);
this.size--;
return temp.getContent();
}
// 获取队列中索引位置元素
public T get(int i) {
if (i < 0 || i >= this.size()) {
throw new IndexOutOfBoundsException("获取队列元素,越界");
}
Node<T> node = this.front;
while (i > 0){
node = node.getNext();
i--;
}
return node.getContent();
}
// 获取队列的长度
public int size() {
return this.size;
}
public String toString() {
StringBuffer sb = new StringBuffer();
Node<T> node = this.front;
while (node.getNext() != null) {
sb.append(node.getContent());
sb.append(" ");
node = node.getNext();
}
return sb.toString();
}
public static void main(String[] args) {
OrangeLinkedQueue<Integer> queue = new OrangeLinkedQueue<>();
queue.add(2);
queue.add(5);
queue.add(8);
System.out.println(queue.get(0));
System.out.println(queue.get(2));
System.out.println(queue.remove());
System.out.println(queue.remove());
System.out.println(queue.toString());
System.out.println(queue.size());
}
}