1. 队列模型
队列的基本操作是 enqueue(入队),它是在表的末端(叫做队尾(rear))插入一个元素,以及dequeue(出队),它是删除(并返回)在表的开头(叫做对头(front))的元素。所以,与栈不同的是,队列是一种使用两端的结构:一端用来加入新元素,另一端用来删除元素。因此,最后一个元素必须等到排在它之前的所有元素都删除之后才能操作。
队列是先进先出 (FIFO)的结构
2. 队列的数组实现
如同栈的情形一样,对于队列而言,任何表的实现都是合法的。像栈一样,对于每一种操作,链表实现和数组实现都给出快速的 O(1) 运行时间。
队列的一种可能实现是使用数组,但是这并不是最佳的选择。元素从队尾加入而从队首删除,这会释放数组中的某些单元,这些单元不应该浪费。因此,很容易想到,应该利用这些单元来存放新的元素,这样队列的尾部可能会出现在数组的开头。在这种情况下,可以使用循环数组来表示。
如果在逆时针方向上,最后一个元素紧接着第一个元素,则队列已满。但是,由于循环数组是使用“普通”数组实现的,因此,如果第一个元素在第一个单元中,最后一个元素在最后一个单元中,或者第一个元素与最后一个元素相邻且在其右边,则均说明队列已满。
在添加或者删除时,enqueue 和 dequeue 必须考虑元素在数组中移动的可能性。例如,enqueue() 可以看成循环数组上的操作,但是实际上这是在一维数组上的操作。因此,如果最后一个元素在最后一个单元中,而数组的开始单元为空,则将新的元素放在开始单元。如果最后一个元素在其他位置,且空间允许的话,新的元素就放在它的后面。用循环数组实现队列的时候,必须将这两种情况区分清楚。
具体程序如下所示:
# ifndef ARRAY_QUEUE
# define ARRAY_QUEUE
template<class T, int size = 100>
class ArrayQueue {
public:
ArrayQueue(){
first = last = -1;
}
void enqueue(T);
T enqueue();
bool isFull() {
return first == 0 && last == size-1 || first == last+1
}
bool isEmpty(){
return first == -1;
}
private:
int first,last;
T storage[size];
};
template<class T, int size>
void ArrayQueue<T,size>::enqueue(T e1){
if(!isFull()){
if (last == size-1 || last == -1){
storage[0] = e1;
last = 0;
if (first == -1)
first = 0;
}
else
storage[++last] = e1;
}
else
cout<< "Full queue.\n";
}
template<class T,int size>
T ArrayQueue<T,size>::dequeue(){
T tmp;
tmp = stroage[first];
if (first == last)
last = first = -1;
else
first = 0;
else
first++;
return tmp;
}
# endif
3. 队列的链表实现
使用双端量表可以更自然地实现队列,STL 的 list 也包含双向链表。
具体程序如下所示:
#ifndef DLL_QUEUE
#define DLL_QUEUE
#include <list>
template<class T>
class Queue{
public:
Queue(){
}
void clear() {
lst.clear();
}
bool isEmpty() const {
return lst.empty();
}
T& front() {
return lst.front();
}
T dequeue() {
T e1 = lst.front();
lst.pop_front();
return e1;
}
void enqueue(const T& e1)
{
lst.push_back(e1);
}
private:
list<T> lst;
};
#endif
如果在队列的链表实现中使用双链表,这两种实现(数组和双向链表)执行入队列和出队列操作都需要常数时间O(1)。在单向链表结构中,出队列需要 O(n)次基本操作扫描链表,并在倒数第二个节点处停止。
队列常用于模拟,它的数学理论已经发展的很完善了,也就是数学中所谓的“排队论 (queuing theory)”。这个理论分析多种情况并用队列建立模型。
在排队的过程中,有许多顾客接受服务员的服务,而服务员的服务能力有限,这样,顾客在接受服务前就需要排队等候,而且他们接受服务也需要花费一定的时间。此处的“顾客”,不是指实际的人,也可以用来指各种对象。例如在生产流水线上用于组装的零件,在州际称重站排队的卡车,排队等待闸门打开以通过通道的驳船等。最熟悉的例子是在商店、邮局和银行里排队的情形。在模拟中设计的问题类型包括:需要多少服务员才能避免排队?等候的空间需要多大才能容纳所有排队的顾客?增大空间与增加服务员哪个花费更小?
4.优先队列
在许多情况下,简单的队列结构是不够的,先入先出机制需要使用某些有限规则来完善,再由居中,残疾人应该比其他人享有一定的优先权。因此,当一个职员有空时,应该马上为者为残疾人服务而不是排在队列最前面的人。公路上的收费亭应该允许某些车辆(警车、救护车、消防车等等)即使没有付费,也可以立即通过。在进程队列中,由于系统的功能要求,即使在等待队列中进程 P1 排在晋城 P2 之前,P2 也需要在 P1 之前执行。在此类情况下,需要一种修正的队列,这就是所谓的 优先队列(Priority Queue)。在优先队列中,根据元素的优先级以及在队列中的当前位置决定出队列的顺序。
优先队列的关键在于如何找到一种有效的实现方法,使入队列和出队列操作更快的实现。因为元素会随机到达队列,所以不能保证排在最前面的元素最先出队列,队尾元素最后一个出队列。不同情况可以使用不同的优先级标准,包括使用频率,出生日期、薪水、位置、状态、以及其他因素,因此问题有些复杂。在进程队列中还可以使用预计执行时间作为判据,这也是人们在讨论优先队列时习惯于用小的优先级数表示高优先级的原因。
优先队列可以使用链表的变种实现。一种联表示虽有的元素都按进入顺序排序,另一种链表是根据元素的优先级决定新增元素的位置。在这两种情况下,总的执行时间都是 O(n),因为对于无需链表,可以立即添加元素,不过去处元素时需要 O(n) 的时间进行搜索,而对于有序链表,可以立即去除元素,但是加入新元素需要时间 O(n)。
另一种优先队列的表示方式是使用一个短的有序链表和一个无需链表,这种方法需要决定阈值优先级。有序链表中的元素数目取决于阈值优先级。这意味着某些情况下有序链表为空,为了在链表中加入元素,阈值优先级可以动态变化。另一种方法是使有序链表中的元素数目保持不变, n‾√ 是一个比较好的选择。入队列操作平均需要时间 O( n‾√ ),出队列操作立即执行。
还有一种优先队列实现方法是使用一个简单的链表,附带一个指向该链表的指针数组,用于确定新加入元素应该在链表的哪个范围中。
实验结果表明,链表实现形式的效率为 O(N),最适合于 10 个或 10 个一下元素的队列。双链表的结构效率和简单链表结构差不多。最后一种实现形式的复杂度是 O( n‾√ ),适合用在任意尺寸的队列。
5. 标准模板库中的队列
5.1 普通队列
队列容器默认由 deque 来实现,但是用户也可以选择 list 容器来实现。如果用 vector 容器会导致编译错误,因为 pop() 是通过调用 pop_front() 来实现的,假定 pop_front() 是底层容器的成员函数,但是向量容器不包括这样的成员函数,下面的表格中给出了队列的成员函数。
deque成员函数列表
成员函数 | 操 作 |
---|---|
T &back() | 返回队列的最后一个元素 |
const T& back() const | 返回队列的最后一个元素 |
bool empty() const | 如果队列为空,返回true,否则返回false |
T& front() | 返回队列的第一个元素 |
const T& front() const | 返回队列的第一个元素 |
void pop() | 删除队列中的第一个元素 |
void push(const T& e1) | 在队尾插入元素e1 |
queue() | 创建一个空队列 |
size_type size() const | 返回队列中的元素数目 |
5.2 优先队列
如下表所示,priority_queue 容器默认使用 vector 容器实现,用户也可以使用 deque 实现。priority_queue 容器总是把优先级最高的元素放在队列的最前方以维持队列的顺序。为此,插入操作 push() 使用一个双参数的布尔函数,将队列中的元素重新排序以满足这个要求。该函数可以由用户提供,此外也可已使用 < 运算符,元素值越大,优先级越高。如果元素值越小优先级越高,则需要使用函数对象 greater,表明在决定向优先队列中插入新元素时 push() 应该应用运算符 > 而不是 <。
成员函数 | 操作 |
---|---|
bool empty() const | 如果队列为空,则返回true,否则返回 false |
void pop() | 删除队列中优先级最高的元素 |
void push(const T& e1) | 将元素e1插入优先队列中的合适位置 |
priority_queue(comp f()) | 创建一个空的优先队列,使用一个双变量的布尔函数f对队列中的元素进行排序 |
priority_queue(iterator first, iterator last, comp f()) | 创建一个优先队列,使用一个双变的布尔函数 f 对队列中的元素排序: 队列初始化为迭代器 first 和 last 之间的元素 |
size_type size() const | 返回优先队列中的元素数目 |
T &top() | 返回优先队列中优先级最高的元素 |
const T &top() const | 返回优先队列中优先级最高的元素 |
5.3 双端队列
双端队列(double-ended queue) 是允许在两端访问的线性表。因此,双端队列可以使用双向两标示线,这个链表具有指针数据成员 head 以及 tail。在容器 list 中已经使用了双向链表,而 STL 在双端队列中添加了其他功能,也就是随机访问双端队列任意位置的功能,就如同数组以及向量一样。
之前曾经说过,在向量的前端插入或删除元素,性能并不高,但是双向链表的这种操作却非常迅速。这意味着 STL 的双端队列应该结合向量以及链表的功能。
通过观察双端队列 deque 的成员函数,可以看出这些函数与链表的函数相比基本相似,只有少许的不同,deque 没有包含函数 splice()( 该函数仅适用于链表 )、merge()、remove()、sort()、以及unique()( 这些函数实现算法,list只是简单的将其作为成员函数)。最大的不同在于 at()(其及等价物 operator[]),list 中没有这个函数。
与 vector 相比,vector 中也包含了 at() 函数,如果将 vector 中的函数与 deque 中的函数作比较,会发现二者差别不大。vecter 中没有 pop_front() 和 push_front(),而 deque 中有;deque 中没有包含 capacity() 和 reserver(),但是 vector 中有。
需要注意的是:
对于链表,迭代器只能使用自增以及自检,但是双端队列的迭代器可以增加任何数字。例如
dq1.begin()+1
对于双端队列是合法的,但是对于链表是不合法的。