队列(queue)是只允许在一端进行插入操作,在另一端进行删除操作的线性表,允许插入(也称入队、进队)的一端称为队尾(queue-tail),允许删除(也称出队)的一端称为队头(queue-head)。
循环队列
队列的顺序存储结构称为顺序队列(sequential queue)。假设队列有n个元素,顺序队列把队列的所有元素存储在数组的前n个单元。如果把队头元素放在数组下标为0的一端,则入队操作相当于追加,不需要移动元素,其时间性能为O(1);但是出队操作的时间性能为O(n),因为要保证剩下的(n-1)个元素仍然存储在数组的前(n-1)个单元,所有元素都要向前移动一个元素。
如果放宽队列的所有元素都必须存储在数组的前n个单元这一条件,就可以得到一种更为有效的存储方法,即循环队列,可以实现入队和出队操作的时间性能都是O(1),因为没有移动任何元素,但是队列的队头和队尾都是活动的,因此,需要设置队头、队尾两个位置变量front和rear,入队时rear加1,出队时front加1。
关于循环队列对于空队列、满队列的判定,它们的条件都为:rear=front,那如何将队空和队满的判定条件加以区分呢?可以浪费一个数组元素空间,在队列还差一个元素达到队满的条件时,此时认为队列已满(即浪费一个数组元素空间),此时队尾的位置和队头的位置正好差1,即队满的条件是:(rear+1)%QueueSize=front。
算法设计有一个重要的原则:时空权衡,一般来说,牺牲空间或者其他替代资源,通常可以减少时间代价。例如,在单链表中的开始结点之前附设一个头结点,使得单链表的插入和删除等操作不用考虑表头的特殊情况;在双链表中,结点设置了指向前驱结点的指针指向后续结点的指针,增加了指针的结构性开销,减少了查找前驱和后驱的时间代价。
循环队列的实现
const int QueueSize = 100;
template <typename DataType>
class CirQueue{
public:
CirQueue(); // 构造函数,初始化空队列
~CirQueue(){} // 析构函数
void EnQueue(DataType x); // 入队操作,将元素x入队
DataType DeQueue(); // 出队操作,将队头元素出队
DataType GetHead(); // 取队头元素(并不删除)
bool Empty(); // 判断队列是否为空
private:
DataType data[QueueSize]; // 存放队列元素的数组
int front,rear; // 游标,队头和队尾指针
};
template <typename DataType>
CirQueue<DataType>::CirQueue(){
// 只需要front与rear同时指向数组的某一个位置
front = rear = 0;
}
template <typename DataType>
void CirQueue<DataType>::EnQueue(DataType x){
if ((rear+1)%QueueSize == front)
throw "overflow";
rear = (rear+1)%QueueSize;
data[rear] = x;
}
template <typename DataType>
DataType CirQueue<DataType>::DeQueue(){
if (front == rear)
throw "queue is empty";
front = (front+1)%QueueSize;
return data[front];
}
template <typename DataType>
DataType CirQueue<DataType>::GetHead(){
if (front == rear)
throw "queue is empty";
return data[(front+1)%QueueSize]; // 注意不修改队头指针
}
template <typename DataType>
bool CirQueue<DataType>::Empty(){
return front==rear?true:false; // 简化为front==rear皆可
}
链队列
队列的链接存储结构称为链队列(linked queue),通常用单链表表示,其结点结构与单链表的结点相同,为了使空队列和非空队列的操作一致,链队列也加上头结点,并设置队头指针指向链队列的头结点,队尾指针指向终端结点。
链队列的实现
template <typename DataType>
struct Node{
DataType data;
Node<DataType> *next;
};
template <typename DataType>
class LinkQueue{
public:
LinkQueue(); // 初始化空的链队列
~LinkQueue(); // 释放链队列的存储空间
void EnQueue(DataType x); // 入队操作,将元素x入队
DataType DeQueue(); // 出队操作,将队头元素出队
DataType GetHead(); // 取链队列的队头元素(并不删除)
bool Empty(); // 判断链队列是否为空
private:
Node<DataType> *front,*rear; // 队头和队尾指针
};
template <typename DataType>
LinkQueue<DataType>::LinkQueue(){
Node<DataType> *s = new Node<DataType>;
s->next = nullptr;
front = rear = s; // 将队头指针和队尾指针都指向头结点s
}
template <typename DataType>
LinkQueue<DataType>::~LinkQueue(){
while (front != rear){
Node<DataType> *p = front;
front = front->next;
delete p;
}
delete front; // 删除头结点
}
template <typename DataType>
void LinkQueue<DataType>::EnQueue(DataType x){
Node<DataType> *s = new Node<DataType>;
s->data = x;
s->next = nullptr;
rear->next = s;
rear = s;
}
template <typename DataType>
DataType LinkQueue<DataType>::DeQueue(){
if (front == rear)
throw "queue is empty";
Node<DataType> *p = front->next;
DataType x = p->data;
front->next = p->next;
if (p->next == nullptr)
rear = front; // 出队前队列长度为1的特殊情况处理
delete p;
return x;
}
template <typename DataType>
DataType LinkQueue<DataType>::GetHead(){
if (front == rear)
throw "queue is empty";
return front->next->data;
}
template <typename DataType>
bool LinkQueue<DataType>::Empty(){
return front==rear?true:false;
}
双端队列
双端队列(double-ended queue)是队列的扩展,如果允许在队列的两端进行插入和删除操作,则称为双端队列。
具体实现可参照如下网址,需要注意的是head与tail关于第一个元素的使用有些许规定区别,自行设计代码时需要加以区分对待。
循环队列和链队列的比较
循环队列和链队列基本操作的时间复杂度均为O(1),因此,可以比较的只有空间性能。初始时循环队列必须确定一个固定的长度,所以有存储元素个数的限制和浪费空间的问题。链队列没有溢出问题,只有当内存没有可用空间时才会出现溢出,但是每个元素都需要一个指针域,从而产生了结构性开销。
作为一般规律,当队列中元素个数变化大时,应该采用链队列,反之,应该采用循环队列。