队列
**
一、队列的定义
**
队列(queue)简称队 ,它也是 一种 操作受限的线性表 ,其限制为仅允许在表的一端进行插入操作,而在表的另一端进行删除操作。
- 把进行插入的一端成为队的队尾(rear),把进行删除的一端称为队头或队首(front)。
- 向队列中插入新元素称为进队或入队(enqueue),新元素进队后就成为新的队尾元素;从队列中删除元素称为出队或离队(dequeue),元素出队后 , 其直接后继元素就成为队首元素。
//队列抽象数据类型的定义如下:
ADT Queue
{
数据对象:
D = {a(i)|1<= i <= n, a(i) 为ElemType类型} //ElemType 是自定义类型标识符
数据关系:
R = {<a(i),a(i+1)>|a(i),a(i+1)∈D i=1,2,...,n-1}
基本运算:
InitQueue(&q):初始化队列,构造一个空队列q。
DestroyQueue(&q):销毁队列,释放队列q占用的存储空间。
QueueEmpty(q):判断队列是否为空,若队列q为空,则返回真;否则返回假。
enQueue(&q,e):进队列,将元素e进队,作为队尾元素。
deQueue(&q,&e):出队列,将队列q中出队一个元素,并将其赋值给e。
}
二、队列的顺序存储结构及其基本运算的实现
队列中数据元素的逻辑关系呈线性关系,所以队列可以像线性表一样采取顺序存储结构进行存储,即分配一块连续的存储空间来存放队列中的元素,并采用两个整型变量来反映队列中元素的变化,它们分别存储队首元素和队尾元素的下标位置,分别称为队首指针(队头指针)和队尾指针。采用顺序存储结构的队列为顺序队(sequential queue)。
假设队列中元素个数最多不超过整数MaxSize,所有的元素都具有ElemType数据类型,则顺序队类型SqQueue声明如下:
//自定义数据类型
typedef char ElemType;
typedef struct Queue
{
ElemType data[MaxSize];
int front,rear;
}SqQueue;
在顺序队中队头指针front指向当前队列中队头元素的前一个位置,队尾指针rear指向当前队列中队尾元素的位置。
(1)顺序队中实现队列的基本运算
空列
a,b,d,c,e进队
出队两次
出队三次
综上所述,对于q所指的顺序队,初始时设置q->front = q->rear = -1,可以归纳出对后面算法设计来说非常重要的4个元素:
- 队空的条件:q->front == q->rear;
- 队满的条件:q->rear == MaxSize-1;
- 元素e进队时,先将rear++,然后将元素e放在data数组的rear位置;
- 出队操作,先将front++,然后取出data数组中front位置的元素;
在顺序队上对应队列的基本运算算法设计如下:
1)初始化队列InitQueue(SqQueue *&q):
//构造一个空队列q,将front和rear指针均设置成初始状态,即-1。
void InitQueue(SqQueue *&q)
{
q = (SqQueue *)malloc(sizeof(SqQueue));
q->front = q->rear = -1;
}
2)销毁队列DestroyQueue(SqQueue *&q):
//释放队列q占用的存储空间。
void DestroyQueue(SqQueue *&q)
{
free(q);
q = NULL;
}
3)判断队列是否为空QueueEmpty(SqQueue *q):
//若队列q为空,返回值为真;否则返回假。
bool QueueEmpty(SqQueue *q)
{
return (q->front == q->rear);
}
4)进队列EnQueue(SqQueue *&q,ElemType e):
//首先判断队列q是否满,若满,则返回假,结束函数;
//若不满,rear++,将元素e存储在data的rear位置,返回值为真。
bool EnQueue(SqQueue *&q,ElemType e)
{
if(q->rear == MaxSize-1)
{
return false;
}
else
{
q->rear++;
q->data[q->rear] = e;
return true;
}
}
5)出队列DeQueue(SqQueue *&q,ElemType &e):
//与上述的进队列类似,首先判断队列是否为空,若为空,则返回假;
//若不空,front++,然后data的值赋给元素e,返回值为真。
bool DeQueue(SqQueue *&q,ElemType &e)
{
if(QueueEmpty(q))
{
cout << "The queue is empty" << endl;
return false;
}
else
{
q->front++;
e = q->data[q->front];
return true;
}
}
存在问题: 这样的操作容易出现假满的情况,即上面图3、4,若出现假满的情况,队列q将无法继续进队,从而浪费空间,占内存。
即存在问题:如何解决假满问题,使之能持续循环使用?
(2)环形队中实现队列的基本运算
因为队满条件设置不合理导致队满条件成立而队列中仍然有空位置的情况称为假溢出(false overflow)。可以看出,在出现假溢出时队尾指针rear指向data数组的最大下标,而另一端仍存在若干空位置。解决的方法是把data数组的前端和后端连接起来,形成一个环形数组,即把存储队列元素的数组从逻辑上看成一个环,称为环队列或者循环队列(circular queue)。
环形队列首尾相连后,当队尾指针rear = MaxSize-1后,再进入一个位置就达到了0,于是就可以使用另一端的空位置存放队列元素了。实际上存储器中的地址总是连续编号的,为此采用数学上的求余运算(%)来实现:
//队头指针front循环增1:front = (front + 1)% MaxSize
//队尾指针rear循环增1:rear = (rear + 1)% MaxSize
环形队列的队头指针front和队尾指针rear初始化都设置为0,即front = rear =0。在进队元素和出队元素时,队尾指针和队头指针分别循环加1.
那么,循环队列q的队满和队空条件如何设置呢?显然,队空的条件是q->front==q-> rear,当队满的条件也是q->front ==q-> rear。
那,如何区分队满和队空呢?改为以“队尾指针循环增1时等于队头指针”作为队满条件,也就是尝试仅对一次,若达到队头,就认为队满了,不能再进队。这样的话,队列就少用一个元素空间,即该队列在任何时刻最多只能有MaxSize-1个元素。
在这样设计的环形队列中,实现队列的基本运算算法如下:
1)初始化队列InitQueue(SqQueue *&q):
//构造一个空队列q,将front和rear指针均设置成初始状态,即-1。
void InitQueue(SqQueue *&q)
{
q = (SqQueue *)malloc(sizeof(SqQueue));
q->front = q->rear = 0;
}
2)销毁队列DestroyQueue(SqQueue *&q):
//释放队列q占用的存储空间。
void DestroyQueue(SqQueue *&q)
{
free(q);
q = NULL;
}
3)判断队列是否为空QueueEmpty(SqQueue *q):
//若队列q为空,返回值为真;否则返回假。
bool QueueEmpty(SqQueue *q)
{
return (q->front == q->rear);
}
4)进队列EnQueue(SqQueue *&q,ElemType e):
//首先判断队列q是否满,若满,则返回假,结束函数;
//若不满,q->rear = (q->rear + 1) % MaxSize,将元素e存储在data的rear位置,返回值为真。
bool EnQueue(SqQueue *&q,ElemType e)
{
if((q->rear + 1) % MaxSize == q->front)
{
return false;
}
else
{
q->rear = (q->rear + 1) % MaxSize;
q->data[q->rear] = e;
return true;
}
}
5)出队列DeQueue(SqQueue *&q,ElemType &e):
//与上述的进队列类似,首先判断队列是否为空,若为空,则返回假;
//若不空,q->front = (q->front + 1) % MaxSize,然后data的值赋给元素e,返回值为真。
bool DeQueue(SqQueue *&q,ElemType &e)
{
if(QueueEmpty(q))
{
cout << "The queue is empty" << endl;
return false;
}
else
{
q->front = (q->front + 1) % MaxSize;
e = q->data[q->front];
return true;
}
}
环形队列解决了假满现象,更加充分地利用了队列空间。但并非所有情况下都采用环形队列。因为在环形队列中,随着多次元素的进队和出队,出队元素的空间有可能被新入队的元素所覆盖。在有些情况下,需要利用出队的元素来求解,所以是否使用环形队列视情况而定。