队列本质
队列跟栈一样,也是一种操作受限的线性表数据结构 ,只不过栈是是后进先出 (Last In First Out,LIFO),队列是先进先出(First In First Out,FIFO):
⨳ 把它想象成排成一队的人更容易理解,在队列中,处理总是从第一名开始往后进行,而新来的人只能排在队尾;
⨳ 也可以把它想象隧道也许,先进入隧道的车辆先出隧道,不能插队。
跟栈一样,队列可以用数组来实现,也可以用链表来实现。用数组实现的队列叫作顺序队列
,用链表实现的队列叫作链式队列
,其主要API如下:
⨳ void enqueue(E)
:入队操作,即放一个数据到队列尾部
⨳ E dequeue()
:出队操作,即从队列头部取一个元素
⨳ E getFront()
:查看队列头部的元素
如果不考虑性能,只要阉割数组和链表的操作,就能实现队列,比如:
⨳ 顺序队列 :数组头为队列头,数组尾为队列尾,向队列尾追加元素还好说,时间复杂度为 O(1)
,但在队列头拿数据(移除数组头部的元素)就麻烦了,这涉及到数据的搬移,时间复杂度为 O(n)
。
⨳ 链式队列 :链表头节点为队列头,链表尾节点为队列尾,这时在队列头拿数据的时间复杂度为O(1)
,在队列尾追加元素的时间复杂度为 O(n)
,因为要遍历整个队列。
要想使链式队列的 enqueue
操作也是 O(1)
,可以再维护 tail
节点,很容易就实现了。
那顺序队列应该怎么改造呢,怎么让顺序队列的 enqueue
和 dequeue
操作都是 O(1)
呢?
下面介绍循环队列。
循环队列
循环队列 是把顺序队列首尾相连,把线性表从逻辑上看成一个环,用head
指针和tail
指针标明队列的边界。
啥意思呢?
原本数组是有头有尾的,是一条直线,现在我们把首尾相连,扳成了一个环。假设 head
指针指向队列头,更精确点是队列中第一个元素,tail
指针指向队尾,更精确点是下一个元素入队的位置。
下图就是向循环队列队尾添加元素的示意图,即向tail
指针指向的位置添加元素:

从队头取元素,就是取head
指针指向的元素,如下图:

由上图可知,当 tail
指针追上 head
指针时,表示队列已满,当 head
指针追上 tail
指针时表示队列已空,现在仔细确认一下追上
到底是什么意思:
⨳ 队列为空条件 :初始情况下 head
和 tail
都指向 arr[0]
,上图在队尾添加四个元素,又在队头取出四个元素,head
和 tail
都指向 arr[4]
,也就是说当 head == tail
时表示队列为空。
⨳ 队列已满条件 :初始情况下,在队尾添加四个元素时, head
指向 arr[0]
, tail
指向 arr[4]
,数组 arr[4]
还没有元素,如果此时还能在队尾追加元素,那会导致 tail
加一,指向 arr[0]
,head
和 tail
都指向一个元素的情况不就是队列为空的条件嘛。
为了和队列为空条件作区分,只能浪费一个空间,让 tail
再向下走一步 等于 head
时为队列已满条件,又因为在循环队列中,索引 4
下一个位置是索引是 索引0
,所以最终队列已满条件为 (tail + 1) % capacity
,其中 capacity
为数组的容量。
初始化循环队列
还是以 int
数组为例构建循环队列:
java
public class IntCircleQueue {
private int[] data;
private int head; // 队头指针
private int tail; // 队尾指针
public IntCircleQueue(int capacity){
data = new int[capacity+1];// 因为要浪费一个空间,所以要加一
head = 0;
tail = 0;
}
public IntCircleQueue(){
this(10); // 默认队列可存储十个元素
}
}
入队操作
向循环队列队尾添加元素,就是向tail
指针指向的位置添加元素:
java
public void enqueue(int e){
if((tail + 1) % data.length == head)
throw new IllegalArgumentException("enqueue failed. Queue is full.");
data[tail] = e;
tail = (tail + 1) % data.length;
}
这里当 tail 追上 head 时,报了一个队列已满的错,完全可以引入动态数组的思想,构造一个更大的数组:
java
public void enqueue(int e){
if((tail + 1) % data.length == head){
//throw new IllegalArgumentException("enqueue failed. Queue is full.");
int[] new_data = new int[(data.length-1)*2+1]; // 两倍扩容
for(int i = 0 ; i < data.length ; i ++)
new_data[i] = data[i];
data = new_data;
}
data[tail] = e;
tail = (tail + 1) % data.length;
}
出队操作
从队头取元素,就是取head
指针指向的元素:
java
public int dequeue(){
if(head == tail)
throw new IllegalArgumentException("Cannot dequeue from an empty queue.");
int ret = data[head];
head = (head + 1) % data.length;
return ret;
}
不想浪费一个空间
如果队满的时候不想浪费那一个 tail
指向的空间怎么办?
引入一个 size
属性即可,size
表示队列中的元素个数,入队的时候 size++
,出队的时候 size--
,当 size == 0
时,队列已空,当 size == data.length
时,队列已满,简单粗暴。
双端队列
前文讲栈的时候,提到Java官方推荐使用 Deque
代替 Stack
,Deque
就是双端队列,既可以在队尾添加元素又可以在队尾取出元素,既可以在队头取出元素,又可以在队头插入元素。
也就是说双端队列的两端,都可以进行数据的存取。
为了方便实现,这里使用带有 size
属性的队列进行改造:
java
public class IndDeque {
private int[] data;
private int head; // 队头指针
private int tail; // 队尾指针
private int size; // 队列中元素的个数
public IndDeque(int capacity){
data = new int[capacity];
head = 0;
tail = 0;
}
public IndDeque(){
this(10); // 默认队列可存储十个元素
}
}
在队尾添加元素,就是在 tail
指针指向位置添加元素,那在队尾取元素,就是取 tail
指针上一个位置的数据:
java
public int removeLast(){
if(size == 0) // 如果队列为空
throw new IllegalArgumentException("Cannot removeLast from an empty queue.");
// 计算删除掉队尾元素以后,新的 tail 位置
tail = tail == 0 ? data.length - 1 : tail - 1;
size --;
return data[tail];
}
在队头取元素,就是取 head
指针指向的元素,那在队头插入元素,就要在 head
指针上一个位置插入数据:
java
public void addFront(int e){
if(size == data.length)
throw new IllegalArgumentException("addFront failed. Queue is full.");
// 我们首先需要确定添加新元素的索引位置
// 这个位置是 head - 1 的地方
// 但是要注意,如果 head == 0,新的位置是 data.length - 1 的位置
head = head == 0 ? data.length - 1 : head - 1;
data[head] = e;
size ++;
}