和栈相反,队列(Queue)是一种先进先出(First In First Out,缩写为FIFO)的线性表。它只允许在表的一端进行插入,而在另一端进行删除元素。这和我们日常生活中的排队是一致的,最早进入队列的元素最早离开。
在队列中,允许插入的一端叫做队尾(rear),允许删除的一端则成为队头(front)。向队尾插入元素称为进队或入队,新元素入队后成为新的队尾元素;从队头删除元素称为离队或出队,其后续元素成为新的队首元素。
1.队列的顺序存储与实现
在队列的顺序存储中,我们可以将队列当作一般的数组加以实现,但这样做的效果并不好。尽管我们可以用一个指针rear来指示队尾,使得enqueue
运算可以在O(1)时间内完成,但是在执行dequeue
时,为了删除队首元素,必须将数组中其他所有元素都向前移动一个位置,这样,当队列中有n个元素时,dequeue
就需要O(n)时间。
为此呢,我们也可以考虑增加一个front指针来指向队首的位置,这样的话删除队首元素只需要将front指针后移即可,如下图所示:
但是这样又有一个问题,就是当rear指针移动到数组最后的时候,这时候队列并没有满,数组的前面还有空余的空间,所以也不宜进行存储再分配来扩大数组空间。
一个比较巧妙的办法是将顺序队列臆造为一个环状的空间,即数组A[0..capacity-1]中的单元不是排成一排,而是围成一个圆环,即A[0]接在A[capacity-1]后面。如下图所示:
上面的数组,我们可以称之为循环数组,而用循环数组实现的队列称为循环队列。我们用front指针指向队首元素所在的单元,用rear指针指向队尾元素所在单元的下一个单元。如上图所示,队首元素存储在数组下标为0的位置,front=0,队尾元素存储在数组下标为2的位置,rear=3。
但是,这里又有一个问题,即如何表示满队列和空队列。
举例来说,如下图(b)所示,在该循环队列中,队首元素为e0,队尾元素为e3。当e4、e5、e6、e7相继入队列后,如图(c)所示,队列空间被占满,此时队尾指针rear追上队首指针front,即有rear=front。反之呢,如果从图(b)所示的状态开始,e0、e1、e2、e3相继出队,则得到空队列,此时队首指针front追上队尾指针rear,所以也有front=rear。
可见,仅凭front=rear是无法判断队列状态是“空”还是“满”的。
解决这个问题有两个处理思路,一种即增加一个变量size,用它表示队列中的元素的个数,通过size的大小即可判断队列是否为满或者空。第二种思路就是少用一个存储单元,当队尾指针的下一个单元就是队首指针所指单元时,则队列已满,停止入队。这样队尾指针就不会追上队首指针,而队列满时就不会有front=rear了,而是需要满足(rear+1) % capacity = front
。而队列判空的条件不变,仍是front=rear
。
下述代码基于第二种思路,利用循环数组实现了循环队列:
public class ArrayQueue<T> {
private final int DEFAULT_CAPACITY = 8; // 默认容量
private T[] elements;// 数组
private int front;// 队头指针
private int rear; // 队尾指针
@SuppressWarnings("unchecked")
public ArrayQueue() {
elements = (T[]) new Object[DEFAULT_CAPACITY];
front = 0;
rear = 0;
}
/**
* 队列的大小,即队列中元素的个数
*
* @return
*/
public int size() {
// 注意负数,-1 % 8 = -1 而不是 7,所以这里要加上elements.length
return (rear - front + elements.length) % elements.length;
}
/**
* 判空
*
* @return
*/
public boolean isEmpty() {
return front == rear;
}
/**
* 判断队列是否满
*
* @return
*/
private boolean isFull() {
return (rear + 1) % elements.length == front;
}
/**
* 入队
*
* @param e
*/
public void enqueue(T e) {
if (isFull()) {
ensureCapacity();
}
elements[rear] = e;
rear = (rear + 1) % elements.length;
}
/**
* 扩充容量
*/
@SuppressWarnings("unchecked")
private void ensureCapacity() {
// 新的数组
T[] temp = (T[]) new Object[elements.length * 2 + 1];
int size = size();
for (int i = 0; i < size; i++) {
temp[i] = elements[front];
front = (front + 1) % elements.length;
}
front = 0;
rear = size;
elements = temp;
}
/**
* 出队
*
* @return
*/
public T dequeue() {
if (isEmpty()) {
// 暂时先抛出这个异常
throw new RuntimeException();
}
T e = elements[front];
front = (front + 1) % elements.length;
return e;
}
}
2.队列的链式存储与实现
队列的链式存储使用单链表来实现。为了方便,这里使用带头结点的单链表。
根据单链表的特点,我们可以选择链表的头部作为队首,尾部作为队尾。
public class LinkedQueue<T> {
class Node {
private T data;// 数据域
private Node next;// 指针域
public Node() {
this(null, null);
}
public Node(T data, LinkedQueue<T>.Node next) {
super();
this.data = data;
this.next = next;
}
public T getData() {
return data;
}
public void setData(T data) {
this.data = data;
}
public Node getNext() {
return next;
}
public void setNext(Node next) {
this.next = next;
}
}
private Node front; // 队头指针
private Node rear; // 队尾指针
private int size; // 元素个数
public LinkedQueue() {
front = new Node();
rear = front;
size = 0;
}
/**
* 判空
*
* @return
*/
public boolean isEmpty() {
return size == 0;
}
/**
* 队列的大小,即元素的个数
*
* @return
*/
public int size() {
return size;
}
/**
* 入队
*
* @param e
*/
public void enqueue(T e) {
Node newNode = new Node(e, null);
rear.setNext(newNode);
rear = newNode;
size++;
}
public T dequeue() {
if (isEmpty()) {
// 暂时抛出这个异常
throw new RuntimeException();
}
front = front.getNext();
size--;
return front.getData();
}
}
代码使用了size这个成员变量来指示队列的大小。这样,所有的操作都可以在O(1)时间内完成。