Hello,everybody.我们又见面了。今天我们来学习一下队列这个数据结构,let’s Go,开始我们的征程吧。
首先,举两个生活中的常见例子。相信大家,在用电脑工作娱乐时,都会碰到这样的现象。当我们点击程序或进行其他操作时,电脑处于死机状态。正当我们准备Reset时,它突然像打了鸡血似的,突然把刚才我们的操作,按顺序执行了一遍。之所以会出现这个现象,是因为操作系统的多个程序,需要通过一个管道输出,而按先后顺序排队造成的。
还有有个例子,在我们打客服热线时,有时会出现等待的现象。当其他客户挂断电话,客服人员才会接通我们的电话。因为客服人员相对于客户而言,总是不够的,当客户量大于客服人员时,就会造成排队等待的想象。
操作系统、客服系统,都是应用了一种数据结构才实现了这种先进先出的排队功能,这个数据结构就是队列。
队列(Queue):是只允许在一端进行插入操作,在另一端进行删除操作的线性表。
队列也是一种特殊的线性表,是一种先进先出的线性表。允许插入的一端称为表尾,允许删除的一端称为表头。
上图,很形象的表示了队列的结构。排在前面的先出,排在后面的后出。换句话,先进的先出,后进额后出。我们在队尾插入数据,队头删除数据。
队列的抽象数据类型:
同样是线性表,队列也有类似线性表的操作,不同的是,插入操作只能在队尾,删除操作只能在队头。
上图是队列的抽象数据类型。
顺序存储的队列:
我们在学习线性表时,知道线性表分为顺序存储与链式存储两种存储方式。队列是特殊的线性表,所以它也分为两种存储方式。我们先来看看它的顺序存储结构吧。
队列顺序存储的不足:
假设有n个元素,我们需要初始化一个长度大于n的数组,来存放这n个元素,下标为0的位置为队头。队列的插入(入队)操作,是在队尾进行操作的,队列中的其他元素不用移动。入队操作的时间复杂度为O【1】.但是如果,是删除(出队)操作,需要在队头操作,需要移动队列中所有元素,以确保我们队头(下标为0的位置)不为空。所以,时间复杂度为O【n】。
我们可以改进一下这个队列,以提高它的效率。我们大可不必,把元素放在数组的前n个位置。也就是说,我们没必要把下标为0的位置定位队头位置。如下图:
为了避免当只有一个元素时,队头与队尾重合,影响我们的操作。所以,我们引入了front、rear指针。front指向第一个元素的位置,rear指向最后一个元素的下一个位置。
这样,当rear=front时,不是队列中只有一个元素,而是队列为空。
这样我们在进行出队操作时,队列中其他元素就不用动了。我们的时间复杂度为o[1].
但是,我们的问题又来了,看下图:
此时,rear已经超出了数组的界限,但是下标为0、1的位置还是空的,这样是不是挺浪费的?此时,我们的循环队列就横空出世了。
循环队列:队列的头尾相接的顺序存储结构称为循环队列.
如下图:
这里有一个问题,大家看下图:
此图中,rear=front,此时队满。可是,我们刚才说rear=front时,队列为空。那么,rear=front时,是空还是满呢?对于这个问题,我们提供了2中解决方法。
方法一:初始化一个flag变量,当flag=1,rear==front时,队列满。当flag=0,rear==front时,队列空。
方法二:当rear==front时,队列为空。当rear与front中间仅差一个存储单元时,队列为满。
这里,我们讨论一下方法二。看下图:
front与rear之间相处一个存储单元,此时我们就说队列已满。因为rear有时>front,有时<front。我们假设队列的 最大尺寸为QueueSize,那么我们可以得到计算队列为满的公式。
(rear+1)%QueueSize==front.
当rear>front时,rear-front就是队列的长度。如下图:
当rear<front时,此时的队列长度分两部分,一部分为QueueSize-front,另一部分为rear+0。如下图:
将两部分加在一起,就是队列的长度。最后,我们得出计算队列长度的通用公式:
(rear-front+QueueSize)%QueueSize
我们看一下循环队列的顺序存储结构代码:
typedef int QElemType
typedef struct
{
QElemType data[MAXSIZE];
int front;
int rear;
}SqQueue;
循环队列的初始化代码:
Status InitQueue(SqQueue *Q)
{
Q->front=0;
Q->rear=0;
return ok;
}
循环队列求队列长度:
int QueueLength(SqQueue Q)
{
return (Q.rear-Q.front+MAXSIZE)%MAXSIZE;
}
循环队列的入队操作
Status EnQueue(SqQueue *Q,QElemType e)
{
if((Q->rear+1)==Q->front)/*队列满的判断*/
return ERROR;
Q->data[Q->rear]=e;
Q-rear=(Q->rear+1)%MAXSIZE
return Ok;
}
循环队列的出队操作
Status DeQueue(SqQueue *Q,QElemType *e)
{
if(Q->front=Q->rear)/*队列空的判断*/
return ERROR;
*e=Q->data[Q->front];
Q->front=(Q->front+1)%MAXSIZE;
return ok;
}
从这一段讲解,我们发现,单单使用队列的顺序存储结构,性能是不高的。我们应该使用循环队列,但是循环队列又面临着数组溢出的问题,所以我们还有学习一下不用队列长度的链式存储结构。
队列的链式存储:
队列的链式存储,其实就是线性表的单链表。只不过,只能尾进头出。我们把它简称为链队列。为了操作的方便,我们把front指向头结点,把rear指向终端结点。空队时,front、rear都指向头结点。如下图:
链队列的结构:
typedef int QElemType;
/*结点的结构*/
typedef struct QNode
{
QElemType data;
struct QNode *next;
}QNode,*QueuePtr;
/*链表的结构*/
typedef struct
{
QueuePtr front, rear;
}LinkQueue;
链队的入队操作:
Status EnQueue(LinkQueue *Q,QElemType e)
{
QueuePtr s=(QueuePtr)malloc(size(QNode));
if(!s)/*存储分配失败*/
exit(OVERFLOW);
s->data=e;
s->next=NULL;
Q->rear->next=s;
Q->rear=s;
return ok;
}
链队的出对操作:
Status DeQueue(LinkQueue *Q,QElemType *e)
{
QueuePtr P;
if(Q-front==Q->rear)
return ERROR;
P=Q->front-next;
*e=p->data;
Q->front->next=p->next;
if(Q->rear==p)
Q->rear=Q->front;
free(p);
return ok;
}
我们来比较一下循环队列与链队的区别:
关于他们的区别,我们从两方面来分析。时间、空间。
时间:时间复杂度都为O【1】,但是链队在申请、释放结点时会消耗一些时间。
空间:循环队列需要固定的长度,会出现存储元素数量,空间浪费的问题。链队不会出现空间浪费的问题。
总的来说,当我们可以确定长度时,我们选择循环队列,否则使用链队。
总结:
这一章,我们主要学习的数据结构是栈(stack)、队列(Queue).
Stack:只允许在一端进行插入删除操作。
Queue:只能在一端插入,另一端删除。
Stack、Queue都是特殊的线性表。所以它们都可以用顺序存储结构来实现,但是都存在一些弊端。它们各自都有解决这些弊端的方法。
Stack,它把相同的数据类型的栈,存放在一个数组中,让数组一头为一个栈的栈顶,另一头为另一个栈的栈顶。最大化的利用了数组空间。
Queue:为了避免出队,而移动队元素,于是引入了循环队列,让头尾相连。使得时间复杂度为O【1】.
他们又都可以用链式存储结构实现。
这就是这一章的内容了,接下来我们一起学习串。