一、队列的定义和运算
1、队列的定义
队列简称队,它也是一种运算受限的线性表,其限制是仅允许在表的一端进行插入,而在表的另一端进行删除。把进行插入的一端称作队尾,进行删除的一端称作队首。向队列中插入新元素称为进队或入队,新元素进队后就成为新的队尾元素;从队列中删除元素称为离队或出队,元素离队后,其后继元素就成为队首元素。由于队列的插入和删除操作分别是在各自的一端进行,每个元素必然按照进入的先后次序离队,所以又把队列称为先进先出表(FIFO表)。
2、队列接口的定义
public interface Queue {
//抽象队列接口的定义
void enter(Object obj); //元素入队,即向队列尾部添加一个新元素obj
Object leave(); //元素出队,即从队列首部删除队首元素并返回
Object peek(); //返回队首元素的值
boolean isEmpty(); //判断队列是否为空
void clear(); //清除队列中的所有元素使之变为一个空队
}
二、队列的顺序存储结构和操作实现
每次向队列插入一个元素,需要首先使队尾指针后移一个位置,然后再向这个位置写入新元素。当队尾指针指向数组空间的最后一个位置(即queueArray.length-1)时,若队首元素的前面仍存在空闲的位置,则表明队列未占满整个数组空间,下一个存储位置应是下标为0的空闲位置,因此,首先要使队尾指针指向下标为0的位置,然后再向该位置写入新元素。通过赋值表达式rear=(rear+1)%queueArray.length可使存储队列的整个数组空间变为队尾相接的一个环,所以顺序存储的队列又称为循环队列。在循环队列中,其存储空间是首尾循环利用的,当rear指向最后一个存储位置时,下一个所求的位置自动为数组空间的开始位置(即下标为0的位置)。
每次从队列中删除一个元素时,若队列非空,则首先把队首指针后移,使之指向队首元素,然后再返回该元素的值。使队首指针后移也必须采用取模运算,该计算表达式为front=(front+1)%queueArray.length,这样才能够实现存储空间的首尾相接,即循环利用。
当一个顺序队列中的长度域len域的值为0时,表明该队列为空,则不能进行出队和读取队首元素的操作,当len域的值等于queueArray.length时,表明队列已满,即存储空间已被用完,此时应动态扩大存储空间,接着才能插入新元素。
在顺序存储的队列类型的定义中,若省略长度域len也是可行的,但此时的队列长度只能为queueArray.length-1,也就是说必须有一个位置空闲着。这是因为若使用全部queueArray.length个位置存储队列,则当队首和队尾指针指向同一个位置时,也可能为空队,也可能为满队,就存在二义性,无法判断。为了解决这个矛盾,只有牺牲一个位置的存储空间,利用队首和队尾指针是否相等作为判断空队的条件,而利用队尾指针加1并为queueArray.length取模后是否等于队首指针(即队尾是否从后面又追上了队首)作为判断满队的条件。
对于队列的插入和删除操作分别在两端进行,并且通过使用循环队列后,不需要比较和移动任何元素,所以其时间复杂度均为O(1)。
类的具体定义和每个方法的定义如下:
1、向队列插入元素
当向队列插入一个元素时,是把它写入到当前队尾元素的后面,为此,要首先让队尾指针循环后移一个位置,若队列已满,则需要重新分配更大的数组存储空间,通常是原数组空间的二倍,此时还需要把原数组的内容复制到新数组空间中,然后才能正常插入元素。
//元素入队,即向队列尾部添加一个新元素obj
public void enter(Object obj) {
if((rear+1)%queueArray.length==front) //队列满,重新分配空间
{
Object [] p=new Object[queueArray.length*2];//扩大为二倍
if(rear==queueArray.length-1)
{
//把原队列内容复制到新空间中的对应位置,此时front值为0
for(int i=1;i<=rear;i++)
{
p[i]=queueArray[i];
}
}
else
{
//先复制原队列的前面内容,再复制其后面内容
int i,j=1;
for(i=front+1;i<queueArray.length;i++,j++)
{
p[j]=queueArray[i];
}
for(i=0;i<=rear;i++,j++)
{
p[j]=queueArray[i];
}
front=0;
rear=queueArray.length-1; //复制后修改首尾指针
}
queueArray=p; //使queueArray指向新队列的存储空间
}
rear=(rear+1)%queueArray.length; //求出队尾的下一个循环位置
queueArray[rear]=obj; //把obj的值赋给新的队尾位置
}
各个方法的具体定义如下:
//顺序队列类的定义
public class SequenceQueue implements Queue {
final int minSize=10; //假定存储队列的一维数组的初始长度为10
private Object queueArray[]; //定义存储队列的数组引用
private int front ,rear; //定义队首和队尾指针
//无参的构造方法的定义
public SequenceQueue() {
front=rear=0; //队列的初始为空,置队首和队尾指针值为0
queueArray=new Object[minSize]; //数组初始长度为minSize的值10
}
//带初始长度参数的构造方法的定义
public SequenceQueue(int n)
{
front=rear=0; //置队首和队尾指针值为0,初始队列为空
if(n<=minSize)
{
n=minSize; //将n的值最小设置为minSize
queueArray=new Object[n]; //给数组创建具有n大小的存储空间
}
}
//元素入队,即向队列尾部添加一个新元素obj
public void enter(Object obj) {
if((rear+1)%queueArray.length==front) //队列满,重新分配空间
{
Object [] p=new Object[queueArray.length*2];//扩大为二倍
if(rear==queueArray.length-1)
{
//把原队列内容复制到新空间中的对应位置,此时front值为0
for(int i=1;i<=rear;i++)
{
p[i]=queueArray[i];
}
}
else
{
//先复制原队列的前面内容,再复制其后面内容
int i,j=1;
for(i=front+1;i<queueArray.length;i++,j++)
{
p[j]=queueArray[i];
}
for(i=0;i<=rear;i++,j++)
{
p[j]=queueArray[i];
}
front=0;
rear=queueArray.length-1; //复制后修改首尾指针
}
queueArray=p; //使queueArray指向新队列的存储空间
}
rear=(rear+1)%queueArray.length; //求出队尾的下一个循环位置
queueArray[rear]=obj; //把obj的值赋给新的队尾位置
}
//元素出队,即从队列首部删除队首元素并返回
public Object leave() {
if(front==rear)
{
return null; //若队列为空,则返回空值
}
front=(front+1) % queueArray.length; //使队首指针指向下一个位置
return queueArray[front]; //返回队首元素的值
}
//返回队首元素的值
public Object peek() {
if(front==rear)
{
return null; //若队列为空则返回空值
}
return queueArray[(front+1) % queueArray.length];//返回队首元素
}
//判断队列是否为空
public boolean isEmpty() {
return front==rear;
}
//清除队列中的所有元素使之变为一个空队
public void clear() {
front=rear=0;
}
}
三、队列的链接存储结构和实现
队列的链接存储结构也是通过由结点构成的单链表实现的,此时只允许在单链表的表头进行删除和在单链表的表尾进行插入,因此它需要使用两个指针:队首指针front和队尾指针rear。用front指向队首(即表头)结点,用rear指向队尾(即表尾)结点。用于存储队列的单链表简称链接队列或链队。
在链接存储的队列类中,其私有数据成员就是front和rear,而操作方法就是对Queue接口中定义的所有抽象方法的实现。假定链接队列类的名称用LinkQueue表示,该类的具体定义如下:
public class LinkQueue implements Queue{
private Node front,rear; //定义队首和队尾指针
//无参构造函数的定义
public LinkQueue() {
front=rear=null; //初始队列为空,即使队首和队尾指针值为空
}
//向队列插入一个新元素obj,即插入到队列尾部,成为新的队尾
public void enter(Object obj) {
if(rear==null) //若链队为空,则新结点既是队首结点又是队尾结点
{
front=rear=new Node(obj,null);
}
else //若链队非空,则把新结点链接到队尾并修改队尾指针
rear=rear.next=new Node(obj,null);
}
//元素出队,即从队列首部删除队首元素并返回
public Object leave() {
if(front==null)
{
return null; //对队列为空时的处理情况
}
Node x=front; //队首结点的引用暂存x
front=front.next; //删除队首结点,相当于删除表头结点
if(front==null)
{
rear=null; //若队列变为空,则队尾指针也必须变为空
}
return x.element; //返回原队首结点的值
}
//返回队首元素的值
public Object peek() {
if(front==null)
{
return null; //对队列为空时的处理情况
}
return front.element; //返回队首结点的值
}
//判断队列是否为空
public boolean isEmpty() {
return front==null;
}
//清除队列中的所有元素使之变为一个空队
public void clear() {
front=rear=null;
}
}
对于链接队列的插入和删除运算都不需要进行结点的比较和移动,只是简单地根据队尾指针插入结点和根据队首指针删除结点,所以其时间复杂度均为O(1),当然,其他在队列上进行运算的时间复杂度也为O(1)。