队列(queue)是只允许在一端进行插入操作,而在另一端进行删除操作的线性表。
队列是一种先进先出(First In First Out)的线性表,简称FIFO。允许插入的一端称为队尾,允许删除的一端称为队头。假设队列是q=(a1,a2,......,an),那么a1就是队头元素,而an是队尾元素。这样我们就可以删除时,总是从a1开始,而插入时,列在最后。这也比较符合我们通常生活中的习惯,排在第一个的优先出列,最后来的当然排在队伍最后。
2. 队列的抽象数据类型
同样是线性表,队列也有类似线性表的各种操作,不同的就是插入数据只能在队尾进行,删除数据只能在队头进行。
ADT 队列(Queue)
Data
同线性表。元素具有相同的类型,相邻元素具有前驱和后继关系。
Operation
InitQueue(*Q): 初始化操作,建立一个空队列Q。
DestroyQueue(*Q): 若队列Q存在,则销毁它。
ClearQueue(*Q): 将队列Q清空。
QueueEmpty(Q): 若队列Q为空,返回true,否则返回false。
GetHead(Q, *e): 若队列Q存在且非空,用e返回队列Q的队头元素。
EnQueue(*Q, e): 若队列Q存在,插入新元素e到队列Q中并成为队尾元素。
DeQueue(*Q, *e): 删除队列Q中队头元素,并用e返回其值。
QueueLength(Q): 返回队列Q的元素个数
endADT
3. 循环队列
初始化时,rear = front=0,当队列不为空时,front指向队列中的第一个元素,rear指向队列中最后一个元素的下一个位置,当队列满时 rear=front,但不一定是位置0;
插入后rear+1,删除后front+1,但是,无论是删除还是插入,一旦rear或front加一超过了所分配的空间,则让指针指向这片空间的起始位置;设所分配的空间为Maxsize,一旦rear+1,或front+1 =Maxsize, 则rear或front指向起始位置;如上图,当front和rear都在位置3事,此时如果想要插入,则判断3+1=4 与Maxsize 4的关系,相等,则rear=0,这个可以通过取余实现,rear=(rear+1)%Maxsize;
因此,循环序列判空的方法是rear = front; 判满的方法是 (rear+1)%Maxsize ==front;
4. 进队列步骤
void EnQueue(Queue *Q, int key)
{
if ( (Q->rear+1) % Q->maxsize == Q->front) //此时队列没有空间 取余保证,当quil=queuesize-1时,再转回0
{
printf("the queue has been filled full!");
}
else
{
Q->q[Q->rear] = key;
Q-> rear =(Q->rear+1) % Q->maxsize;
}
}
5.出队列步骤
1.判断数列是否为空; 2, 将front现在的时间表保存至temp; 3,将front指针后移一个;4. 返回temp;
int DeQueue(Queue *Q)
{
int tmp;
if(Q->rear== Q->front) //判断队列不为空
{
printf("the queue is NULL\n");
}
else
{
tmp = Q->q[q->front];
Q->front= (Q->front+1) % Q->maxsize;
}
return tmp;
}
6.队列的链式存储结构及实现
队列的链式存储结构,其实就是线性表的单链表,只不过它只能尾进头出而已,我们把它简称为链队列。为了操作上的方便,我们将队头指针指向链队列的头结点,而队尾指针指向终端结点。空队列时,front和rear都指向头结点。
链队列的结构为:
/* QElemType类型根据实际情况而定,这里假设为int */
typedef int QElemType;
/* 结点结构 */
typedef struct QNode
{
QElemType data;
struct QNode *next;
} QNode, *QueuePtr;
/* 队列的链表结构 */
typedef struct
{
/* 队头、队尾指针 */
QueuePtr front, rear;
} LinkQueue;
7.队列的链式存储结构——入队操作
/* 插入元素e为Q的新的队尾元素 */
Status EnQueue(LinkQueue *Q, QElemType e)
{
QueuePtr s =
(QueuePtr)malloc(sizeof(QNode));
/* 存储分配失败 */
if (!s)
exit(OVERFLOW);
s->data = e;
s->next = NULL;
/* 把拥有元素e新结点s赋值给原队尾结点的后继, */
Q->rear->next = s;
/* 见上图中① */
/* 把当前的s设置为队尾结点,rear指向s,见上图中② */
Q->rear = s;
return OK;
}
8.队列的链式存储结构——出队操作
/* 若队列不空,删除Q的队头元素,用e返回其值,
并返回OK,否则返回ERROR */
Status DeQueue(LinkQueue *Q, QElemType *e)
{
QueuePtr p;
if (Q->front == Q->rear)
return ERROR;
/* 将欲删除的队头结点暂存给p,见上图中① */
p = Q->front->next;
/* 将欲删除的队头结点的值赋值给e */
*e = p->data;
/* 将原队头结点后继p->next赋值给头结点后继, */
Q->front->next = p->next;
/* 见上图中② */
/* 若队头是队尾,则删除后将rear指向头结点,见上图中③ */
if (Q->rear == p)
Q->rear = Q->front;
free(p);
return OK;
}
9.循环队列与链队列的比较
从时间上,其实它们的基本操作都是常数时间,即都为O(1)的,不过循环队列是事先申请好空间,使用期间不释放,而对于链队列,每次申请和释放结点也会存在一些时间开销,如果入队出队频繁,则两者还是有细微差异。对于空间上来说,循环队列必须有一个固定的长度,所以就有了存储元素个数和空间浪费的问题。而链队列不存在这个问题,尽管它需要一个指针域,会产生一些空间上的开销,但也可以接受。所以在空间上,链队列更加灵活。
总的来说,在可以确定队列长度最大值的情况下,建议用循环队列,如果你无法预估队列的长度时,则用链队列。
10.总结
栈(stack)是限定仅在表尾进行插入和删除操作的线性表。
队列(queue)是只允许在一端进行插入操作,而在另一端进行删除操作的线性表。
它们均可以用线性表的顺序存储结构来实现,但都存在着顺序存储的一些弊端。因此它们各自有各自的技巧来解决这个问题。
对于栈来说,如果是两个相同数据类型的栈,则可以用数组的两端作栈底的方法来让两个栈共享数据,这就可以最大化地利用数组的空间。
对于队列来说,为了避免数组插入和删除时需要移动数据,于是就引入了循环队列,使得队头和队尾可以在数组中循环变化。解决了移动数据的时间损耗,使得本来插入和删除是O(n)的时间复杂度变成了O(1)。