队列抽象数据类型
队列(queue)是一种特殊的线性表,其插入和删除操作分别在线性表的两端进行。向队列中插入元素的过程称为入队,删除元素的过程称为出队。允许入队的一端叫做队尾,允许出队的一端叫做队头。没有元素的队列称为空队列。
由于插入和删除分别在队尾和队头进行,最先入队的总是最先出队。因此队列的特点是“先进先出”。队列的基本操作有创建队列、判断队列是否为空、入队和出队。队列和栈一样,不支持对指定位置的插入和删除。
声明队列接口Queue< T >如下
package Queue;
public interface Queue<T> {
public abstract boolean isEmpty(); //判断队列是否为空
public abstract boolean add(T x); //元素x入队,若添加成功,则返回true
public abstract T peek(); //返回队头原速度,不做删除。若队列空,返回null
public abstract T pool(); //出队,返回队头元素。若队列空,返回null
}
实现Queue< T >队列接口的类有顺序循环队列、链式循环队列和优先队列。
队列也有顺序和链式两种存储结构,分别称为顺序队列和链式队列。
顺序队列
1.顺序队列
(1)使用顺序表,出队效率低
如果使用一个顺序表作为队列的成员变量,入队操作执行顺序表尾插入,时间复杂度为0(1);出队操作执行顺序表头删除,时间复杂度都为O(n),效率较低。希望出队操作效率也是O(1),因此,不使用一个顺序表作为队列的成员变量。
(2)使用数组,存在假溢出
顺序队列使用数组存储数据元素,用front、rear记住队列头、尾元素下标,入队、出队时,改变front、rear取值,则不需要移动元素。我们举个例子来分析一下:
假设数组长度为5.
(a)队列初始空状态,令front和rear都为-1;
(b)10入队,此时front=rear=0;
(c)20和30入队,此时front=0,rear=2;
(d)10和20出队,此时front=rear=2;
(e)40和50入队,front=2,rear=4;
(f)60想要入队,front=2,rear=length=5,队尾下标越界,假溢出。
思路可描述如下:
(1)当队列空时,设置队头、队尾下标front和rear=-1.
(2)当第一个元素入队,front=rear=0,同时改变了两个下标。
(3)入队操作,元素存入rear位置,rear++。
(4)出队操作,返回front队头元素,front++。
(5)当入队的元素个数超过数组容量,rear下标越界,数据溢出,此时,由于之前已有若干元素出队,数组前部已多出许多存储单元,所以这种溢出不是因为存储空间不够,称为假溢出。
顺序队列存在两个缺点:假溢出以及一次入队/出队操作需要同时改变两个下标。为解决这个问题,我们可以把顺序队列设计成循环结构。
2.顺序循环队列
顺序循环队列就是逻辑上首尾相连的循环结构,这样可以连续使用存储单元。
设front是队头元素下标,rear是下一个入队元素下标。
(1)设置初始空队列为font=rear=0,约定队列空的条件就是front=rear。
(2)入队操作改变rear,出队操作改变front,变化规律如下,其中length表示数组容量。
设length=5:
(a)初始化空队列,令front=rear=0;
(b)10和20入队。front=0,rear=2;
(c)10和20出队。front=rear=2;
(d)30、40、50、60入队。此时front=2,rear=1,队列满front==(rear+1)%length
(e)扩充容量后,front=0,rear=4.
(3)约定队列满条件是front==(rear+1)%length,此时队列中依然有一个空的位置,因为如果不保留一个位置,队列满的条件也是front==rear,与队列空条件相同。
(4)当队列满时再入队,将数组容量扩充一倍,按照队列元素次序复制数组元素。
3.顺序循环队列类
声明顺序循环队列类SeqQueue< T >如下,实现队列接口。
package Queue;
public class SeqQueue<T> implements Queue<T> {
private Object element[]; //存储队列数据元素的数组
private int front,rear;
public SeqQueue(int length) {
if(length<64) {
length=64;
}
this.element=new Object[length];
this.front=this.rear=0; //设置空队列
}
public SeqQueue() {
this(64); //设置默认容量的队列
}
@Override
public boolean isEmpty() {
return this.front==this.rear;
}
@Override
public boolean add(T x) { //元素x入队,空对象不能入队
if(x==null) {
return false;
}
if(this.front==(this.rear+1)%this.element.length) { //若队列满,则扩充数组
Object[] temp=this.element;
this.element=new Object[temp.length*2]; //重新申请一个容量更大的数组
int j=0;
for(int i=this.front;i!=this.rear;i=(i+1)%temp.length) {
this.element[j]=temp[i]; //按照队列元素次序复制数组元素
j++;
}
this.front=0;
this.rear=j;
}
this.element[this.rear]=x;
this.rear=(this.rear+1)%this.element.length;
return true;
}
@Override
public T peek() { //返回队头元素,没有删除。若队列空,则返回nu
return this.isEmpty()?null:(T)this.element[this.front];
}
@Override
public T pool() { //出队,返回队头元素。若队空则返回null
if(this.isEmpty()) {
return null;
}
T temp=(T)this.element[this.front];
this.front=(this.front+1)%this.element.length;
return temp;
}
}
链式队列
(1)使用单链表,入队效率低
1.使用一个单链表作为队列的成员变量,入队操作执行单链表尾插入,时间复杂度为O(n),效率较低;出队操作执行单链表头删除,时间复杂度为O(1)。因此,不能用一个单链表作为队列的成员变量,因为入队操作达不到O(1)。
2.使用一个循环双链表作为队列的成员变量,入队操作执行循环双链表尾插入,出队操作执行循环双链表头删除,时间复杂度都为O(1),但占用较多的空间。
(2)单链表设计,增加尾指针
以下为单链表(不带头结点)实现链式队列的过程。设front和rear分别指向队头和队尾结点,增加一个尾指针,即可使入队和出队操作的空间复杂度都是O(1).
1.设置初始空队列,front=rear=null,队列空条件是front == null&&rear==null。
2.入队操作,将x结点链在rear之后,并使rear指向x结点成为新的队尾。
3.出队操作,当队列不空时,取得队头结点元素,删除队头结点,并使front指向后继结点。
4.当第一个元素入队或最后一个元素出队时,同时改变front和rear。
声明链式队列类LinkedQueue如下,实现队列接口,其中成员变量front和rear分别指向队列头结点和尾节点,数据类型都是单链表结点类Node。
package Queue;
import dataStructure.Node;
public final class LinkedQueue<T> implements Queue<T> {
private Node<T> front,rear; //front和rear分别指向队头和队尾结点
public LinkedQueue() { //构造空队列
this.front=this.rear=null;
}
@Override
public boolean isEmpty() {
return this.front==null&&this.rear==null;
}
@Override
public boolean add(T x) {
if(x==null) {
return false;
}
Node<T> q=new Node<T>(x,null);
if(this.front==null) {
this.front=q; //空队插入
}else {
this.rear.next=q; //队列尾插入
}
this.rear=q;
return true;
}
@Override
public T peek() { //返回队头元素,没有删除
return this.isEmpty()?null:this.front.data;
}
@Override
public T pool() {
if(isEmpty()) {
return null;
}
T x=this.front.data; //取得队头元素
this.front=this.front.next; //删除队头结点
if(this.front==null) {
this.rear=null;
}
return x;
}
}
结点Node类:
package dataStructure;
public class Node<T> {
public T data;//数据域,存储数据元素
public Node<T> next; //地址域,引用后继结点
public Node(T data,Node<T> next) {
this.data=data;
this.next=next;
}
public Node() {
this(null, null);
}
public String toString() { //返回结点数据域的描述字符串
return this.data.toString();
}
}