学 无 止 境 , 与 君 共 勉 。
相关系列
介绍
队列有点类似栈,在队列中最先插入的数据也最先被移除,它是一种先进先出(FIFO)的数据结构。就像我们排队买票一样,先到的先买。
队列的基本操作是入队(enque),在队尾(rear)插入一个数据和出队(deque),移除并返回队头(front)的数据。
由于入队和出队,队头和队尾的位置都是+1,所以队列中的数据项并不是总是从数组的0下标开始的

存在问题
如下图,每次有新的数据加入后,队尾rear就往后移动一位,很快队尾就到达数组的边界了,无法再添加数据了,此时的队列中可能有部分数据出队了,实际队列中可能只存在几个数据。这该怎么办?

解决方案
环绕式处理:为了避免队列不满却不能插入新数据项的问题,只要队头(front)或者队尾(rear)到达数组的边界,下次操作时让它重新回到数组的开头,这就是循环队列。如下图,原队尾指向数组边界数组F处,现在新增数据G,队尾指向了数组的头部,数据添加在数组下标为0的位置。

折断的序列
随着数据的添加,队尾(rear)在回绕之后,会出现队尾在队头的前面的情况,这时队列中的数据项存在数组的两个不同的序列中,如下图,A、B在数组后后半段为一个序列,C、D、E在数组的前半段为另一个序列,这种情况可以成为“折断的序列”。

入队(enque)
队列未满的情况下,如果当前队尾指向了数组的末端,则将队尾指向数组的头部,否则直接将队尾指向数组索引的位置+1,然后在新的队尾插入数据,时间复杂度为O(1)。
出队(deque)
在队列不为空的情况下,返回当前队头所指向的元素,并将队头指向数组的索引+1,如果当前队头指向的是数组的末端,则把队头指向数组的头部,时间复杂度为O(1)。
完整实现
public class Queue<E> {
/**
* 容量
*/
private int maxSize;
/**
* 当前队列已存在的元素数量
*/
private int items;
/**
* 队头
*/
private int front;
/**
* 队尾
*/
private int rear;
/**
* 数据数组
*/
private Object[] array;
public Queue(int maxSize) {
this.maxSize = maxSize;
this.array = new Object[maxSize];
this.front = 0;
this.rear = -1;
this.items = 0;
}
/**
* 入队
* 校验是否满了
* 如果当前队尾指向数组最后一位,删除后需要循环数组,将队尾指向数组头部
*
* @param data 数据
*/
public void enque(E data) {
if (isFull()) {
System.out.println("新增" + data + "失败:队列已经满啦!!!");
return;
}
if (rear == maxSize - 1) {
rear = -1;
}
array[++rear] = data;
items++;
}
/**
* 出队
* 校验是否是空队列
* 如果当前队头指向数组最后一位,新增后需要循环数组,将队头执行数组头部
*
* @return 队头数据
*/
@SuppressWarnings("unchecked")
public E deque() {
if (isEmpty()) {
System.out.println("删除失败:队列已经空啦!!!");
return null;
}
E frontData = (E) array[front++];
if (front == maxSize) {
front = 0;
}
items--;
return frontData;
}
/**
* 查看队头元素
*
* @return E
*/
@SuppressWarnings("unchecked")
public E peekFront() {
return (E) array[front];
}
/**
* 查看队尾元素
*
* @return E
*/
@SuppressWarnings("unchecked")
public E peekRear() {
return (E) array[rear];
}
public boolean isFull() {
return this.items == this.maxSize;
}
public boolean isEmpty() {
return this.items == 0;
}
}
双端队列
双端队列的头部和尾部都可以插入和移除数据。如果禁用其头插入和头移除的方法(或者禁用尾操作),那它的功能就和栈(传送门:栈)一样了;如果禁用左插入和右移除的方法(或相反的另一对方法)那它的功能就和队列一样了。它是一种多用途的数据结构,有时会用它来提供栈和队列的两种功能。
优先级队列(数组实现)
在优先级队列中,数据项按照优先级排序的,所以他并不满足先进先出的原则。优先级最低的数据不会移动,永远指向数组下标为0的位置。有限级越高的数据对应数组的索引也越大。通常用堆实现,堆的介绍后续文章会讲。
入队
数据在插入的时候会按照优先级顺序插入到合适的位置,这里进行了
- 比对:从优先级高的数据开始对比,获取要插入的索引位置
- 位移:将比插入数据优先级高的数据索引+1
两步操作,因此插入效率比普通队列要低,时间复杂度为O(N);
出队
优先级最高的数据永远在数组索引的高位,这样移除后不需要移动其他的数据,时间复杂度为O(1);
完整代码
/**
* 优先级队列(数组实现)
* 优先级最高的永远在队头,即下标为 items-1 处
* 优先级最低的永远在队尾,即数组下标为 0 处
* 这里指定参数必须为实现Comparable接口的类,用于比较优先级
*/
public class PriorityQueue<E extends Comparable<E>> {
/**
* 队列的最大容量
*/
private int maxSize;
/**
* 当前队列已存在的元素数量
*/
private int items;
/**
* 存储数据的数组
*/
private Object[] array;
public PriorityQueue(int maxSize) {
this.maxSize = maxSize;
this.array = new Object[maxSize];
this.items = 0;
}
/**
* 入队
* 校验是否已满
* 从队列顶部开始比较,优先级比插入数据大的,索引位置+1
*/
@SuppressWarnings("unchecked")
public void enque(E data) {
if (isFull()) {
System.out.println("新增" + data + "失败:队列已经满啦!!!");
return;
}
// 要插入的索引位置
int index;
for (index = items - 1; index >= 0; index--) {
// 插入数据的优先级大于当前元素,在当前元素后面插入
if (data.compareTo((E) array[index]) >= 0) {
break;
}
// 优先级比插入元素大的数据往后移一位
array[index + 1] = array[index];
}
// 插入数据
array[index + 1] = data;
items++;
}
/**
* 出队
* 校验队列是否为空
* 优先级最高的永远在队列顶部,items-1处
*/
@SuppressWarnings("unchecked")
public E deque() {
if (isEmpty()) {
System.out.println("删除失败:队列已经空啦!!!");
return null;
}
return (E) array[--items];
}
/**
* 查看优先级最高的元素
*/
@SuppressWarnings("unchecked")
public E peekFront() {
return (E) array[items - 1];
}
public boolean isFull() {
return this.items == this.maxSize;
}
public boolean isEmpty() {
return this.items == 0;
}
}
总结
- 先进先出(FIFO)
- 在一端插入,在另一端删除
- enque(入队):在队尾(rear)插入一个元素,队尾指向位置+1
- deque(出队):删除并返回在队头(front)的元素,队头指向位置+1
- 循环队列:通过循环数组的方式,使队头和队尾重新回到数组的开头
- 数据项不总是从数组的下标0开始的
- 队尾的下标比队头的下标小,称为“折断的序列”
- 双端队列,队头、队尾都可以插入删除的队列,包含了栈和队列的操作
- 优先级队列
- 数据按优先级排序
- 不遵循FIFO
- 优先级最高的永远在队头,即下标为(元素数量-1)处
- 优先级最低的永远在队尾,即数组下标为 0 处
- 通常用堆去实现
日常求赞
创作不易,如果各位觉得有帮助,求点赞支持
求关注
