队列(queue)
只允许在一端进行插入操作,而在另外一端进行删除操作的线性表。队列是一种先进先出(First In First Out)的线性表,简称FIFO.允许插入的一端称为队尾,允许删除的一端称为队头。如果队列是q=(a1,a2,a3.....,an),那么a1是队头元素,an为队尾元素。删除时从a1开始,插入时列在an后面。
循环队列
线性表有顺序存储和链式存储,栈是线性表,所以其有这两种方式,同样,队列作为一种特殊的线性表,同样存在这两种模式。
队列顺序存储的不足:
如果我们有一个n个元素的队列,则顺序存储队列需要简历一个大于n的数组,并把队列的所有元素存储在数组的前n的单元。数组下标为0的一端为队头。
入队操作,其实是在队尾添加元素,不需要移动元素,则时间复杂度为O(1)。
出队操作,队列的出列是在队头,即在0的位置,也就以为着队列中所有元素都得向前移动,以保证队头位置不为空,则其时间复杂度O(n)为了避免位置移动,可以引入两个指针,front指向队头元素,rear指针指向队尾元素的下一个位置,这样当front和rear相等时,队列为空。
这样会引入另外的一个问题,在一个有限的队列中加入元素,直到满,头部移除2个元素,front指针后移2次,再加入元素,会产生数组越界错误,可是实际上队列后还有一个位置空闲(0位置),把这种现象称为“假溢出”
循环队列解决了这个问题,我们把队列的头尾相连的顺序存储结构称为循环队列。当上面例子在加入一个元素时,rear移向0位置。
接着a6入队,将其放入到0处,rear的指针指向下标为1,
若再入队a7,则rear指针将会和front指针重合,通知指向下标为2的位置。
又有问题,之前说队空时是,front == rear, 现在队满也出现front == rear.
解决这个问题常用方法是:
当队空时,front == rear,
当队满时,修改其条件,保留一个元素位置。也就是说,队满时,还有一个空闲单元。
由于是循环队列,有可能rear比front大,也有可能比front小,所以尽管两者相差一个位置是满的情况,但也可能是相差了整整一圈,
所以更改队满条件为 (rear+1)%QueueSize == front
通用的计算队列长度的公式:
(rear - front + QueueSize) %QueueSize
初始时: Q.front = Q.rear = 0
队首指针进1: Q.front = (Q.front + 1) % MaxSize
队尾指针进1: Q.rear = (Q.rear + 1)%MaxSize
队列长度:(Q.rear - Q.front + MaxSize)%MaxSize
判断队满条件:(Q.rear + 1)%MaxSize == Q.Front
/**************************************************循环队列**********************************************/
#define MAXSIZE 20 //存储空间初始分配量
#define OK 1
#define ERROR 0
#define TRUE 1
#define FALSE 0
#include <stdlib.h>
typedef int QElemType; //QElemType 数据类型
typedef int Status;
// 循环队列顺序存储结构
typedef struct
{
QElemType data[MAXSIZE];
int front; //头指针
int rear; //尾指针
}SqQueue;
// 循环队列初始化代码
Status InitQueue(SqQueue *Q)
{
Q->front = 0;
Q->rear = 0;
return OK;
}
// 循环队列求队列长度
// 返回Q个元素个数,也就是返回当前队列长度
int QueueLength(SqQueue Q)
{
return (Q.rear-Q.front+MAXSIZE)%MAXSIZE;
}
// 入队操作
// 若队列未满,则插入元素e到Q新的队尾
Status EnQueue(SqQueue *Q, QElemType e)
{
if((Q->rear+1)%MAXSIZE == Q->front) //判断队满
return ERROR;
Q->data[Q->rear] = e; //将元素e赋值到队尾
Q->rear = (Q->rear+1)%MAXSIZE; //rear指针向后移动一个位置,
//若到最后通过(Q->rear + 1)%MAXSIZE移动到头部
return OK;
}
// 出队操作
// 若队列不空,则删除Q中队头的元素,并返回其值
Status DeQueue(SqQueue *Q, QElemType *e)
{
if(Q->front == Q->rear) //判断队空
return ERROR;
*e = Q->data[Q->front]; //将队头元素赋值给e
Q->front = (Q->front + 1)%MAXSIZE; //front指针向后移动一个位置,若到最后则返回头部
return OK;
}
队列链式存储结构及其实现
队列的链式存储结构,其实就是线性表的单链表,只不过是它只能尾进头出而已,把这种简称为链队列。
需要有一个头结点,当空队列时,front和rear都指向头结点。
typedef int QElemType;
typedef struct //结点结构
{
QElemType data;
struct QNode *next;
}QNode,*QueuePtr;
typedef struct //队列链表结构
{
QueuePtr front,rear; //队列头尾指针
}LinkQueue;
// 入队操作
// 插入元素e到Q的新队尾元素,直接插入,不管大小
Status EnQueue(LinkQueue *Q, QElemType e)
{
QueuePtr s = (QueuePtr)malloc(sizeof(QNode));
if(!s) // 存储分配失败
exit(OVERFLOW);
s->data = e;
s->next = NULL;
Q->rear->next = s; //把拥有元素e的新结点放到原队列结点的后继
Q->rear = s; //把当前结点s设为队尾结点
return OK;
}
// 出队操作
// 头结点的后继结点出队,将头结点的后继改为他后面结点,删除该结点
// 如果链表除头结点外只剩一个元素时,则需要将rear指向头结点
Status DeQueue(LinkQueue *Q, QElemType *e)
{
QueuePtr p;
if(Q->front == Q->rear)
return ERROR;
p = Q->front->next; //将要删除结点暂存
*e = p->data; //将要删除结点值赋值给e
Q->front->next = p->next; // 将原队头结点后继指向p->next,
if(Q->rear == p) //若队头是队尾,则删除后将rear指向头结点
Q->rear = Q->front;
free(p);
return OK;
}
循环队列与链队列,其从时间上,其实他们的基本操作都是常数时间O(1),
循环队列是要事先申请好空间,使用期间不释放; 链队列,每次申请和释放结点会存在一些时间开销,如果入队出队频繁,两者差异细微。
空间上,
循环队列必须有一个固定的长度,所以有了存储元素个数和空间的浪费。
链队列则不存在这个问题,尽管他需要一个指针域,会产生一些时间上的开销,但是可以接受。在空间上,链队列更加灵活。
SUMMARY
栈(stack) 是限定仅仅在表尾插入和删除操作的线性表。
队列(queue)是只允许在一端进行插入操作,而在另外一端进行删除操作的线性表。
这两种线性表均可以用顺序存储结构实现,但是会有顺序存储的弊端。
对于栈,如果是两个相同的数据类型栈,则可以用数组两端作为栈底的方法,让两个栈进行共享数据,这样可以最大化利用数组空间。
对于队列,为了避免数组插入和删除需要移动数据,于是引入了循环队列,使得队头和队尾可以在数组中循环变化,解决了移动数据时间的损耗,使得原本O(n)的插入和删除动作编程了O(1);
这两种数据结构也可以通过链式的存储结构来实现。
本文详细介绍了队列这一数据结构,包括其基本概念、特点、应用场景以及两种主要的存储方式——顺序存储和链式存储。重点讲解了循环队列如何解决传统队列存在的问题,并对比了循环队列与链队列的优缺点。

被折叠的 条评论
为什么被折叠?



