同栈类似,队列也是一种受限的线性表。其中受限指队列的操作是线性表的子集,它要求所有元素必须从一端插入新元素,从另外一端删除元素。添加元素的一端叫队尾 ,删除元素的一端叫队首。
根据底层使用的数据容器的不同,队列可分为以下两大类
- 顺序队列:使用数组作为数据容器
- 链式队列:使用链表作为数据容器
队列的逻辑定义是其具备先进先出(first in first out,FIFO)的特点,这种特点决定了队列的应用场景。
队列接口
根据队列的描述,可知队列的操作包括出队、入队、查看队首、获取队列大小、判空操作。其接口设计如下。
/**
* 队列接口,同java.util.Queue接口名称不同
* @param <E>
*/
public interface Queue<E> {
void enqueue(E e);
E dequeue();
E getFront();
int getSize();
boolean isEmpty();
}
基于动态数组的队列
当使用数组作为数据容器时,数组索引为0的元素是队首元素,数组最后一个有效值是队尾元素。本节使用动态数组作为队列底层存储数据的容器。其中,其中动态数组的实现参考实现java动态数组
/**
* 基于动态数组实现队列
* 数组的第一个元素arr[0]是队首,最后一个元素arr[size-1]是队尾
* @param <E>
*/
public class ArrayQueue<E> implements Queue<E> {
private Array<E> arr;
public ArrayQueue() {
arr = new Array<E>();
}
public ArrayQueue(int capacity){
arr = new Array<>(10);
}
/**
*入队,在动态数组尾部添加元素,可能触发数组扩容
* 但考虑复杂度均摊,其最终复杂度为O(1)
* @param o
*/
@Override
public void enqueue(E o) {
arr.addLast(o);
}
/**
* 出队,在动态数组首元素arr[0]移除元素,可能触发缩容操作
* 同时,移出第一个元素,后续所有元素都要移动,
* 因此其复杂度为O(n)
* @return
*/
@Override
public E dequeue() {
return arr.remove(0);
}
/**
* 获取队首元素,不出队
* 由于数组随机访问特点,
* 其时间复杂度为O(1)
* @return E
*/
@Override
public E getFront() {
return arr.getFirst();
}
/**
* 获取当前队列已有大小
* 由于数组随机访问特点,
* 其时间复杂度为O(1)
* @return int
*/
@Override
public int getSize() {
return arr.getSize();
}
/**
* 判空
* 由于数组随机访问特点,
* 其时间复杂度为O(1)
* @return boolean
*/
@Override
public boolean isEmpty() {
return arr.isEmpty();
}
@Override
public String toString() {
StringBuilder stringBuild = new StringBuilder();
stringBuild.append("arrayQueue top is [");
for (int i = 0; i < arr.getSize(); i++) {
stringBuild.append(arr.get(i));
if(i != arr.getSize() -1){
stringBuild.append(",");
}
if(i == arr.getSize() -1){
stringBuild.append("] end");
}
}
return stringBuild.toString();
}
}
基于静态数组的循环队列
上一小节使用动态数组实现的队列在出队时,在动态数组移除首元素arr[0]时,存在两个问题
- 可能触发缩容操作。
- 移出第一个元素,数组后续所有元素都要移动重排,造成出队操作复杂度为O(n)
为解决基于动态数组的队列出队操作渐进时间复杂度O(n)的问题,本节介绍一种基于静态数组实现的队列。该队列同样使用数组索引为0的元素是队首元素,数组最后一个有效值是队尾元素,需要维护以下变量:
- 静态数组data:用于存储队列元素
- 头指针front:指向队首(已有元素位置)。指向已有元素的位置
- 尾指针tail:指向队尾(下一个元素要存储的位置,第一个为空的位置)。指向没有元素的位置
- 队列容量size: 表示当前队列已使用的容量,不是队列的总容量
在刚创建队列后,队列的头尾指针相等,都指向下标为0的位置。并且,头尾指针相等也是判断队列为空的唯一条件。即当且仅当队列为空,即数组为空时,头指针和尾指针指向同一个位置front == tail.

对于入队操作,即数组添加元素时,尾指针后移一位。
对于出队操作,即数组删除元素时,头指针后移一位。这种出队方式只需移动一下头指针,使出队操作的时间复杂度由O(n)降为O(1),解决了基于动态数组的队列出队操作渐进时间复杂度O(n)的问题。
循环的来由: 当尾指针指向数组最后一个元素,且头指针指向的数组位置索引大于0(此时数组的剩余容量>1)时,当一个元素入队时,只能进入尾指针位置。入队之后,尾指针重新指向数组第一个元素的位置。需要特别指出的是,尾指针下一个指向的数组位置的计算方式为 (i + 1) % arr.length。

假设循环队列一直入队,出现以下情况。则再入队一个元素,就会出现front ==tail,即可队列为空的判断条件一致,此时就无法区分队列时空还是满,因此当只剩一个元素容量时(浪费一个空间),不再入队,表示队列已满,即tail +1 = front表示队列已满,此时需要触发扩容操作。由于循环队列,因此队列已满的通用表达式为 (tail +1)% capacity = front

循环队列代码实现
循环队列的代码实现如下
/**
* 基于数组实现的循环队列
*/
public class LoopQueue<E> implements Queue<E> {
//不能使用动态数组,因为动态数组只有一个游标,这个游标相当于循环队列的尾指针
//逻辑上这是一种普通队列,并没有实现循环
//private Array<E> arr;
private E[] data ;
//头指针 头指针front指向队首(已有元素位置);
private int front;
//尾指针.尾指针tail指向队尾(下一个元素要存储的位置,第一个为空的位置,没有元素的位置)。
private int tail ;
//当前已有容量
private int size;
public LoopQueue(){
this(10);
}
public LoopQueue(int capacity){
//capacity是用户传递进来的目标容量,而循环队列已满时,会浪费一个容量
//因此想存储capacity个元素需要capacity + 1个空间
// 因此构造的数组容量 + 1
//2、涉及了创建泛型数组的变相方式
data = (E[])new Object[capacity + 1];
front = 0;
tail = 0;
size = 0;
}
@Override
public void enqueue(E e) {
if((tail + 1) % data.length == front){
resize(getCapacity() * 2);
}
data[tail] = e;
tail = (tail + 1) % data.length;
size++;
}
/**
* 扩容或缩容
* @param newCapacity
*/
private void resize(int newCapacity){
E[] newData = (E[])new Object[newCapacity + 1];
for (int i = 0; i < size; i++) {
//front +i有意思
//当数组循环起点不是从0开始时,可将其拆分为 basePoint + i(基本坐标 + 步长)
newData[i] = data[(front + i) % data.length];
}
data = newData;
front = 0;
tail = size;
}
@Override
public E dequeue() {
if(isEmpty()){
throw new IllegalArgumentException("数组为空");
}
E e = data[front];
//置空 使其被gc回收
data[front] = null;
front = (front + 1) % data.length;
size--;
//缩容
if(size == getCapacity() / 4 && getCapacity() / 2 != 0){
resize(getCapacity() / 2);
}
return e;
}
@Override
public E getFront() {
if(isEmpty()){
throw new IllegalArgumentException("数组为空");
}
return data[front];
}
@Override
public int getSize() {
return size;
}
@Override
public boolean isEmpty() {
return front == tail;
}
public int getCapacity(){
//逻辑设计上要浪费一个数组空间
// 减1操作是因为在定义队列时,底层数组的长度已加1
return data.length - 1;
}
@Override
public String toString(){
StringBuilder stringBuilder = new StringBuilder();
stringBuilder.append("loopqueue top is [");
for (int i = 0; i < size ; i++) {
//此处不能使用getCapacity()作为数组的长度,这个方法是暴露给外部用户使用的,
//其值比数组长度少1,而此处的取模操作是基于真实的数组长度
//getCapacity()是上层接口,逻辑层面的。
// 而data.length是内部实现,底层数据容器支撑。两者不在一个层面
//stringBuilder.append(data[(front + i) % getCapacity()]);
stringBuilder.append(data[(front + i) % data.length]);
//与上一行同样的道理
//if((front + i + 1) % getCapacity() != tail) {
if((front + i + 1) % data.length != tail) {
stringBuilder.append(",");
}
}
stringBuilder.append("]end");
return stringBuilder.toString();
}
}
延申
- 在有限空间,出现循环多使用取模操作。取模结果可能等于0,这一点似乎也呼应数组从0计数