一、前言
从上一篇内容中我们知道了栈最基本的操作是“出栈”和“入栈”,特点是“先进后出,后进先出”。而队列有很多相似的地方,最基本的操作是“入队”和“出队”,不过特点和栈恰恰相反,“先进先出,后进后出”。最形象的就是排队买票,排在最前的会先买到票,而排在后面的也就后买到票了。
上面的图是不是更新是一个水管子,队列的入栈和出栈就是像是水管中的水,最先进入水管子中的水,最先流出水管子。队列和栈一样也是中操作受限的线性表结构,同样,队列也是中抽象的数据结构,支持在队尾插入元素,在队头删除元素。用数组实现的队列叫作顺序队列,用链表实现的队列叫作链式队列。
二、队列的具体操作及简单实现
下面我们用数组实现对队列的基本操作:
声明队列的顶级接口:和栈的方法基本一致
/**
* 队列的顶级接口
* @author Administrator
*/
public interface Queue<T> {
/**
* 队列是否为空
* @return
*/
boolean isEmpty();
/**
* 入队
* @param data
*/
boolean enqueue(T data);
/**
* 出队删除data元素
* @param data
*/
T dequeue();
/**
* 队列的大小
*/
int size();
}
三、队列的具体实现:
队列也是可以通过数组和链表实现,数组实现的是顺序队列,链表实现的是链式队列 。下面先用顺序队列来具体演示队列的这种数据结构。
首先实现顶级接口Queue,实现具体主要方法入队和出队操作,实现指定队列大小和默认队列大小两个构造方法:
//存放数据的数组
private T []items;
//数组的大小
private int size;
//队头下标
private int head=0;
//队尾下标
private int tail=0;
//队列默认大小
private static final int DEFULE_LENGTH=10;
/**
* 指定队列大小
* @param capactiyt
*/
public ArrayQueue(int capactiyt){
items = (T[]) new Object[capactiyt];
head=tail;
}
/**
* 默认队列大小为10
*/
public ArrayQueue(){
items = (T[]) new Object[DEFULE_LENGTH];
head=tail;
}
具体操作:
在实现具体操作之前,先通过下图来了解下队列的入队和出队整个过程
实现代码如下:
/**
* 入队操作
*/
@Override
public boolean enqueue(T data) {
//当队尾的下标等于size时表示队列已满不能入队
if(tail==size){
return false;
}
//将数据赋值到队尾的位置
items[tail]=data;
++tail;//队尾的位置+1
size++;//队列大小+1
return true;
}
/**
* 出队操作
*/
@Override
public T dequeue() {
//当对头和队尾为相同时表示队列为空,返回null
if(head==tail){
return null;
}
//取出队头数据返回
T oldData = items[head];
++head;//队头head向后移动
size--;//队列的大小-1
return oldData;
}
在顺序表的实现过程中,可以发现,数组的实现的队列和数组存在一样的问题,在图E中,随着元素不停的入队和出队操作,head和tail都会持续的往后移动,当队尾tail移动到最后边时,及时数组还有空闲空间,队列也无法继续添加元素,也就是假溢出。此时数据搬移的思想就可以应用在此处,这样每次都只需要删除数组下标为0的数据,不过涉及整个队列的数据进行搬移,时间复杂度就会从原来的O(1)变成O(n)。这个问题该如何优化呢?
先来看下一种思路:
在出队时不做数据搬移,只在没有空闲空间时,在入队时,触发一次数据搬移的操作,这样出队函数我们保持不变,只需要修改下入队操作。具体代码如下:
/**
* 入队操作
* 队满时触发数据搬移
*/
@Override
public boolean enqueue(T data) {
//当队尾的下标等于size时表示队列已满
if(tail==size){
//当tail==size && head==0表示整个队列已经满
if(head==0){
return false;
}
//数据搬移
for (int i = head; i < tail; i++) {
items[i-head]=items[i];
}
//搬移数据之后更新head和tail
tail-=head;
head=0;
}
//将数据赋值到队尾的位置
items[tail]=data;
++tail;//队尾的位置+1
size++;//队列大小+1
return true;
}
从代码中,我们可以看出,虽然解决了假溢出的问题,但是空间浪费的问题依然存在,而且入队时在特殊场景下会变成O(n),这当然是我们不想看到的,此次出现了一种特殊的队列——循环队列。
四、循环队列
循环队列顾名思义是一个循环结构,可以有效利用存储空间,不过该如何判断队列已满,话不多说,先上图:
从图中可以看出,在队列已满时,再添加元素时,不是将队尾置位7,而是将其在环中后移一位,到下标为0的位置,然后依次入队。而如何判断队列已满。从图d中可以看出,当队列已满时(队尾+1)%size=head的规律。队头和队尾的计算方式为(队尾+1)%size=队尾,(队头+1)%size=队头,此时队尾是没有存储元素的,因此会浪费数据的一个存储空间。
代码实现:
/**
* 入队操作
*/
@Override
public boolean enqueue(T data) {
//判断队列是否已满,已满队列返回false
if((tail+1)%size==head){
return false;
}
//将元素赋值到队尾
items[tail]=data;
//将队尾下标+1
tail=(tail+1)%size;
size++;//队列大小+1
return true;
}
/**
* 出队操作
*/
@Override
public T dequeue() {
//如果队列为空,返回null
if(head==tail){
return null;
}
//取得要出队的数据
T oldData = items[head];
//队头下标后移一位
head=(head+1)%size;
size--;//队列大小减1
return oldData;
}
从代码中看出,出队和入队的时间复杂度都为O(1),同时结合图例可以看出,循环队列有效解决了空间浪费的问题。