大家好,我是追忆,欢迎各位大佬前来指教。
本期呢我们将把我对队列的理解讲述给大家,谢谢大家的支持,观看。
我的博客空间:追忆
文章目录
前言
我们可以想象一下我们在餐厅排队吃饭的时候是如何进行的?
应该是谁先到,先排进队伍中,谁先打到饭,谁先走(当然啊,这是在无人插队的情况下)。
故队列也与此相同,谁先进,谁就先出。
一、定义
队列:队列是一种先进先出(FIFO) 的线性表。 在表一端插入,在另一端删除。
其入队和出队示例如图所示:
我们把a1,a2,到an入队:
此时我们把a1称为队头,an称为队尾。
然后我们把a1出队:
此时a2就成为了队头。
由此我们引出队列的定义:只能在表的一端(队尾)进行插入,在另一端(队头)进行删除运算的线性表。
而对于逻辑结构来说:与线性表相同,仍为一对一关系。
那么什么是一对一关系呢?
答案是
① 存在一个唯一的被称为“第一个”的数据元素;
② 存在一个唯一的被称为“最后一个”的数据元素;
③ 除第一个元素外,每个元素均有唯一一个直接前驱;
④ 除最后一个元素外,每个元素均有唯一一个直接后继。
而存储结构也与线性表相同:用顺序队列或链队存储均可。
运算规则:先进先出(FIFO)。
二、队列的基本结构
由于队列有两种存储方式,所以我们在这里用一种笼统的定义方式来创建一个队列,我们先考虑一下建立一个队列都需要什么?
数据元素。
我们要进行哪几项操作?
- 构建空队列
- 销毁队列
- 清空队列
- 判断是否为空
- 求队列长度
- 入队
- 出队
- 遍历
所以我们可以这样建立队列
ADT Queue {
数据对象:D= {a;|a;∈ElemSet,i= 1,2,-.,,n, n≥0}
数据关系:R ={<ai-1,ai>|ai-1,ai∈D,i= 1,2,-,n}
基本操作:
(1) InitQueue (&Q) //构造空队列
(2) DestroyQueue (&Q) //销毁队列
(3) ClearQueue (&Q) //清空队列
(4) QueueEmpty(Q) //判空. 空--TRUE,
(5) QueueLength(Q) //取队列长度
(6) GetHead (Q,&e) //取队头元素
(7) EnQueue (&Q,e) //入队列
(8) DeQueue (&Q,&e) //出队列
(9) QueueTraverse(Q) //遍历
}ADT Queue
由此,我们可以开始对队列进行编写程序了。
三、顺序队列
事先声明:由于顺序队列存在的问题过大,我们不再深入了解,故我不会过多的解释。相关操作会在循环队列中实现。
1.定义
在探讨顺序队列如何定义的时候,我们不妨考虑一下顺序表是如何定义的?
如下:
typedef struct
ElemType *base; //基指针
int Length;//顺序表长度
} SqList;
根据给出的顺序表我们可以推断出顺序队列该如何定义?
首先,肯定要有一个基指针让我们去动态的申请一块连续的空间。
而对于顺序表长度而言,对队列意义是不大的。
我们要考虑一下,顺序队列是如何操作的?
从队尾入队,从队头出队。
根据这个原因,我们是不是要知道队列的队头和队尾的位置。
故应该有头指针和尾指针,但我们要考虑一下,是否需要知道队头和队尾的位置?
我认为是不需要的,我们其实可以把顺序队列看成一个数组,知道下标,我们就知道相应的元素了。
故我们应该如下定义:
typedef struct {
QElemType *base; //初始化的动态分配存储空间
int front; //头指针
int rear; //尾指针
}SqQueue;
那么为了方便我们理解,在这里我有个小问题:假设我们此时创立了一个队列Q,我们该如何访问队头元素?
我们知道 Q.front是不是队头元素的下标,知道其下标,我们该如何知道对应的元素信息?
在这里我们可以利用数组来回答问题:
假设我们知道数组a,我们此时来个定义:
int *p=a;
如果我们想访问i所处位置的元素,我们可以直接用p来得到
p[i]=a[i];
故我们只需要知道指针和对应的下标来得之相应的元素。也就是
Q.base[Q.front]
可以得出队头元素。
2.操作实现
如果我们此时创建一个顺序队列Q:
我们可以看出最开始的时候 队头和队尾都指向同一个元素,也就是:
Q.rear=Q.base=0;
①入队
我们该如何实现入队操作呢?
入队只能在队尾实现,我们现在把J1进行入队操作:
如图
此时,下一步应该怎么做呢?
要记住,是从队尾入,队头不变,故应该
Q.rear++;
也就是
此时我们把J2再入队,也是同样的操作:
队头不变,队尾++。
这是入队的操作。代码实现也就是:
Q.base[Q.rear++]=x;
②出队
考虑完入队,我们再考虑一下出队:
出队是从哪里出的?
答案是从队头出队,队尾不变。
此时有一个队列Q:
我们此时要进行出队操作,肯定是把J1进行出队操作:
此时J1已经出队,Q.front变,还是Q.rear变呢?
答案是:
Q.front往后移,Q.rear不变。也就是
Q.front--;
故此时出队操作应该是:
x=Q.base[Q.front++];
③存在的一些问题
此时队列已经满了,如果我们此时再进行入队操作的话,就会造成溢出,由于此时队列本身就是满的,我们称为真溢出。
而对于下述情况来说:
此时队列并没有满,但是此时Q.rear处于最后一位, 在进行入队操作的话,就会造成溢出,但此时队列并没有满,我们称为:假溢出。
那么由以上两种情况来看,无论队列是否为满,都会造成溢出的情况,那么如何解决这种问题呢?
我们是不是可以把第二种情况当成一个小纸条,小纸条的上面写满了,我们是不是可以从下面写,然后再粘在一起,形成一个圆,也叫循环队列。
四、循环队列(首尾连接)
1.前提引入
如图
此为循环队列示意图。
注意:所谓的循环队列真的是让其在内存存储中成为一个环状的嘛?
答案当然是不是啦!!!
数据在内存中存储不可能是环状的,是线性的,我们在此只是用一种方法来让队列看起来像一个环状,但其还是顺序存储。
但此时还有一个问题,如图
如果此时我们将J5,J6进行出队,Q.front就会从4变成5,再从5变成0,如图:
那么此时有个问题:我们怎么把5变成0呢?
我们可以从上图看出:数值都是0-5,所以我们只要保证运算出来是从0·5就行。那么6个数怎么得出此结果呢?
取模!!!
2.如何取模
取模即为取余数,取到的值永远不会大于除数。
例如,队列总大小为6 (7 下标0~5)
正常入队:Q. rear=Q. rear+ 1
循环入队:Q.rear= (Q.rear+1) % 6
正常出队::Q.front=Q.front+1
循环出队:Q.front= (Q.front+ 1) % 6
3.空队和满队一样如何解决
那是否还会由有其他问题呢?
我们使用队列时会出现三种情况:
我们可以看出a和c的时候,rear=front
那么我们该如何区别呢?
这里我将会给出两种解决方案:
- 少用一个元素空间
- 另外设一个标志以区别队空、队满
①少用一个元素空间
如图,此时Q.rear+1=Q.front。
如果此时Q.front=0,Q.rear=5,队列已满,如何表示?
简单,还是利用取模。
(Q.rear+1)%M==Q.front
即:
队空:
Q.front==Q.rear
队满:
(Q.rear+1 )%M==Q.front
②另外设一个标志以区别队空、队满
如图
当队列为空时,我们设置一个标志flag,使其为0
当队列不为空时,我们使其等于1
2.队列操作的实现
①初始化
初始化即把原先定义的东西给一一赋值。
我们之前是如何定义的?
#define MAXQSIZE 100 //最大长度
typedef struct {
QElemType *base; //初始化的动态分配存储空间
int front; //头指针
int rear; //尾指针
}SqQueue;
现在我们要做的是,为其一一赋值,为指针分配空间。
也就是进行以下操作:
bool InitQueue (SqQueue &Q){
Q.base =new QElemType[MAXQSIZE];
if(!Q.base)
return false;
Q.front=Q.rear=0;
return true;
}
②求循环队列的长度
如果只根据上图,只使用队尾减去队头就可以得到队长了,但仅仅就这样可以吗?
答案当然是不对的。如下图
这时,用队尾减去队头是不可以的,是负数。
这时我们就要找一个通用公式了:
先看第一个图:4-0=4
第二个图: 2-4=-2
如果此时让第二个图加上长度可以不?试一下
-2+6=4;得到结果了
但是并不是通用的,因为图一再加上长度的话:4+6=10;不对
再考虑
取模
对喽
此时我们加上长度后再进行取模运算的话
图一:10%6=4;对
图二:4%6=4.;对
则公式为:(Q.rear-Q.front+MAXQSIZE)%MAXQSIZE;
则程序可写为:
int QueueLength(SqQueue Q){
return (Q.rear-Q.front+MAXQSIZE)%MAXQSIZE;
}
③入队
入队该如何入,我们之前也说过,队尾后移,队头不变。但此时我们要用循环队列的时候,要用取模运算,进行后移。
此时要考虑什么时候入队?
队不满的时候,这时我们要从两个方面考虑了:
I少用一个元素空间
什么时候队满?
上文提到过
在这里直接给出程序:
bool EnQueue(SqQueue &Q,QElemType e){
if((Q.rear+1)%MAXQSIZE==Q.front) return false;
Q.base[Q.rear]=e;
Q.rear=(Q.rear+1)%MAXQSIZE;
return true;
}
II另外设一个标志以区别队空、队满
在上面我们提到
flag=1:队列中有元素
flag=0: 队列为空
故此时的程序可以如下:
bool EnQueue(SqQueue &Q,QElemType e)
{
if((Q.flag==1) && (Q.rear==Q.front))
{ cout<<"队列已满"<<endl;
return false;
}
else { Q.base[Q.rear] =e;
Q.rear = (Q.rear+1) % MAXQSIZE;
}
if(Q.flag==0)
Q.flag=1;
//队列已不空
return true;
}
从上述程序中我们看到flag前面有个Q.可以得到我已经把flag定义到结构体中了。
④出队
出队如何出?
从队头出,队尾不变,队头后移。
什么时候不能出队?
队为空,有两种情况
I少用一个元素空间
什么时候队空?
上文提到过
在这里直接给出程序:
bool DeQueue (SqQueue &Q,QElemType &e){
if(Q.front==Q.rear) return false;
e=Q.base[Q.front];
Q.front=(Q.front+1)%MAXQSIZE;
return true;
}
II另外设一个标志以区别队空、队满
什么时候队空?
上文提到过
程序给出:
bool DeQueue(SqQueue &Q,QElemType &e)
{
if(Q.flag==0)
{
cout<<"队列为空"<<endl;
return false;
}
else{
e=Q.base[Q.front];
Q.front=(Q.front+1) % MAXQSIZE;
if(Q.front==Q.rear)
Q.flag=0;1/空队
return true;
}
⑤取队头元素
取队头元素非常简单,只需Q.base[Q.front],但仅仅如此可以吗?
当然不可以:我们要判断是否为空队:
QElemType GetHead (SqQueue Q)
{
if(Q.front!=Q.rear)
return Q.base[Q.front];
}
⑥遍历队列
非常简单,但需注意,遍历的时候,队头,队尾都不能进行移动,故此时需要再定义一个。
void TraverseQueue(SqQueue Q) {
int i = Q.front;
while (i != Q.rear) {
cout << Q.base[i] << " ";
i = (i + 1) % MAXQSIZE;
}
cout << endl;
}
3.整体程序
下面将用int类型示例
①少用一个元素空间
#include <iostream>
using namespace std;
typedef int QElemType;
#define MAXQSIZE 100 //最大长度
typedef struct {
QElemType* base; //初始化的动态分配存储空间
int front; //头指针
int rear; //尾指针
}SqQueue;
//初始化
bool InitQueue(SqQueue& Q) {
Q.base = new QElemType[MAXQSIZE];
if (!Q.base)
return false;
Q.front = Q.rear = 0;
return true;
}
//入队
bool EnQueue(SqQueue& Q, QElemType e) {
if ((Q.rear + 1) % MAXQSIZE == Q.front) return false;
Q.base[Q.rear] = e;
Q.rear = (Q.rear + 1) % MAXQSIZE;
return true;
}
//出队
bool DeQueue(SqQueue& Q, QElemType& e) {
if (Q.front == Q.rear) return false;
e = Q.base[Q.front];
Q.front = (Q.front + 1) % MAXQSIZE;
return true;
}
//求长度
int QueueLength(SqQueue Q){
return (Q.rear - Q.front + MAXQSIZE) % MAXQSIZE;
}
//取队头元素
QElemType GetHead(SqQueue Q)
{
if (Q.front != Q.rear)
return Q.base[Q.front];
}
//遍历元素
void TraverseQueue(SqQueue Q) {
int i = Q.front;
while (i != Q.rear) {
cout << Q.base[i] << " ";
i = (i + 1) % MAXQSIZE;
}
cout << endl;
}
int main() {
SqQueue Q;
QElemType e;
int n, i;
// 初始化队列
InitQueue(Q);
cout << "请输入您要插入元素的个数:";
cin >> n;
// 入队操作
cout << "请输入您要插入的元素:";
for (i = 0; i < n; i++)
{
cin >> e;
EnQueue(Q, e);
}
// 遍历队列
cout << "队列元素:";
TraverseQueue(Q);
cout << "队列长度:" << QueueLength(Q) << endl;
// 出队操作
if (DeQueue(Q, e)) {
cout << "出队元素:" << e << endl;
}
// 遍历队列
cout << "队列元素:";
TraverseQueue(Q);
// 释放队列占用的内存
delete[] Q.base;
return 0;
}
②另外设一个标志以区别队空、队满
#include <iostream>
using namespace std;
typedef int QElemType;
#define MAXQSIZE 100 //最大长度
typedef struct {
QElemType* base; //初始化的动态分配存储空间
int front; //头指针
int rear; //尾指针
int flag;
}SqQueue;
//初始化
bool InitQueue(SqQueue& Q) {
Q.base = new QElemType[MAXQSIZE];
if (!Q.base)
return false;
Q.flag = Q.front = Q.rear = 0;
return true;
}
//入队
bool EnQueue(SqQueue& Q, QElemType e)
{
if ((Q.flag == 1) && (Q.rear == Q.front))
{
cout << "队列已满" << endl;
return false;
}
else {
Q.base[Q.rear] = e;
Q.rear = (Q.rear + 1) % MAXQSIZE;
}
if (Q.flag == 0)
Q.flag = 1;
//队列已不空
return true;
}
//出队
bool DeQueue(SqQueue& Q, QElemType& e)
{
if (Q.flag == 0)
{
cout << "队列为空" << endl;
return false;
}
else {
e = Q.base[Q.front];
Q.front = (Q.front + 1) % MAXQSIZE;
if (Q.front == Q.rear)
Q.flag = 0; // 空队
return true;
}
}
//求长度
int QueueLength(SqQueue Q){
return (Q.rear - Q.front + MAXQSIZE) % MAXQSIZE;
}
//取队头元素
QElemType GetHead(SqQueue Q)
{
if (Q.front != Q.rear)
return Q.base[Q.front];
}
//遍历元素
void TraverseQueue(SqQueue Q) {
int i = Q.front;
while (i != Q.rear) {
cout << Q.base[i] << " ";
i = (i + 1) % MAXQSIZE;
}
cout << endl;
}
int main() {
SqQueue Q;
QElemType e;
int n, i;
// 初始化队列
InitQueue(Q);
cout << "请输入您要插入元素的个数:";
cin >> n;
// 入队操作
cout << "请输入您要插入的元素:";
for (i = 0; i < n; i++)
{
cin >> e;
EnQueue(Q, e);
}
// 遍历队列
cout << "队列元素:";
TraverseQueue(Q);
cout << "队列长度:" << QueueLength(Q) << endl;
// 出队操作
if (DeQueue(Q, e)) {
cout << "出队元素:" << e << endl;
}
// 遍历队列
cout << "队列元素:";
TraverseQueue(Q);
// 释放队列占用的内存
delete[] Q.base;
return 0;
}
五、链队
前提引入
关于链队,我们可以参考链表也就是我的上一篇博文:链表
我们考虑一下如何入队和出队?
从队尾插入,从队头进行出队也就是删除。
所以说在声明一个链队列的时候,仅仅是声明一个简单的链表可不可以?
当然是不行的。
我问不仅要设置一个头指针用来出队,还要设置一个尾指针用来入队。
现在又要考虑一个问题:尾指针指向尾部,拿头指针是指向头节点还是首元结点?
指向头节点是比较好的,因为我们指向首元结点的时候,首元结点在出队的时候会一直变,头指针一直需要后移,而指向头结点是不需要一直后移的。
我们看一下链队图片
它的每个节点包含的内容和链表是一致的。即数据域和指针域。
1.定义
故应该如下定义:
typedef struct QNode{
QElemType data;
struct Qnode *next;
}QNode, *QueuePtr;
而我上面也提到了要附设一个头指针和尾指针来方便我们进行操作,而他们应该是什么类型的呢?
在链表中我们就提到过,指针应该是指向什么类型,就是什么类型,那么头指针和尾指针指向的是?
结点。对喽。
故应该如下定义:
typedef struct {
QueuePtr front; //队头指针
QueuePtr rear; //队尾指针
}LinkQueue;
我们假设给一个LinkQuene Q
那么我们该如何取队头元素呢?
很简单
e=Q.front->data;
2.操作实现
前提引入
链队有以下情况
我们可以看一下如何入队,Q.rear后移,而Q.front->next指向x所在的结点,
而如何出队呢?
从队头出队,只需要让Q.front->next从指向x到指向y。
①初始化
我一直在说,初始化就是对结构中的元素赋初值,分配空间,所以非常简单:
首先我们要给头指针和尾指针分配空间:
Q.front=Q.rear=new QNode;
而对于数据域,我们先不为其赋值,因为我们不知道。
然后再为指针赋值,也就是:
Q.front->next=NULL;
完整程序:
bool InitQueue (LinkQueue &Q){
Q.front=Q.rear=new QNode;
if(!Q.front) return false;
Q.front->next=NULL;
return true;
}
②销毁
我们要将所有节点给delete,该如何做?
在链表中,我们重新定义了一个指针,来代替头指针,来进行交替后移,在这里也是如此:
QueuePtr p=Q.front;
Q.front=Q.front->next;
delete p;
但是有必要嘛?
我认为是没有必要的,因为我们最开始是定义了两个指针,我们只需要两个指针交替后移就行了,完整程序如下:
bool DestroyQueue (LinkQueue &Q){
while(Q.front){
Q.rear=Q.front->next;
delete Q.front;
Q.front=Q.rear; }
return true;
}
③判断是否为空
很简单,直接给出程序:
bool QueueEmpty (LinkQueue Q){
return (Q.front==Q.rear);
}
④求队头元素
队头元素是什么?
Q.front->next指向结点的数据,也就是
e=Q.front->next->data;
在取队头元素的时候,切记链队一定不能为空,所以我们要先判断:
完整程序:
bool GetHead (LinkQueue Q, QElemType &e){
if(Q.front==Q.rear) return false;
e=Q.front->next->data;
return true;v
}
⑤入队
入队是从哪里入?
队尾。
那么我们现在有一个x,我们要先申请一块空间:
QueuePtr p=new QNode;
然后把这个结点里的指针域和数据域进行赋值,在赋值前我们要判断申请空间是否成功:
if(!p) return false;
p->data=e;
p->next=NULL;
为什么此时p->next=NULL呢?
因为我们是从尾部插入(可以参考链表的尾插法),它的下一个一定为空。
然后我们需要把这个结点连接到链队中。
Q.rear->next=p;
Q.rear=p;
完整程序:
bool EnQueue(LinkQueue &Q,QElemType e){
QueuePtr p=new QNode;
if(!p) exit(OVERFLOW);
p->data=e; p->next=NULL;
Q.rear->next=p;
Q.rear=p;
return true;
}
思考:如果想要实现调用一次入队函数,使n个元素入队,该怎么办?
两种解决方式:
- 在主函数内使用循环
- 在函数体中传一个n,嵌套在循环中
⑥出队
如图,出队只能从队头出,此时只能把x出队。
为什么要另设一个p?
因为我们要delete
现在开始出队:
我们可以直接让Q.front->next指向y结点,也就是:
Q.front->next=p->next;
但是在进行操作前,我们一定要先判断是否为空队。故完整程序如下:
bool DeQueue (LinkQueue &Q,QElemType &e){
if(Q.front==Q.rear) return false;
QueuePtr p=Q.front->next;
e=p->data;
Q.front->next=p->next;
if(Q.rear==p) Q.rear=Q.front;
delete p;
return true;
}
会有一个疑问:为什会有 if(Q.rear==p) Q.rear=Q.front;这条语句呢?
我们要考虑一下:
如果此时把y出队的时候,尾指针是不是会悬空?
悬空就会造成程序错误,故要进行一些操作。
⑦遍历
遍历也是非常简单,但我们也要注意我们此时的头指针和尾指针都不能移动,所以我们要重新第一个指针来移动去访问各个结点:
void TraversalQueue(LinkQueue Q) {
QueuePtr p = Q.front->next;
while (p) {
cout << p->data << " ";
p = p->next;
}
cout << endl;
}
完整程序
#include <iostream>
using namespace std;
typedef int QElemType;
typedef struct QNode {
QElemType data;
struct QNode* next;
}QNode, * QueuePtr;
typedef struct {
QueuePtr front; //队头指针
QueuePtr rear; //队尾指针
}LinkQueue;
//初始化
bool InitQueue(LinkQueue& Q) {
Q.front = Q.rear = new QNode;
if (!Q.front) return false;
Q.front->next = NULL;
return true;
}
//销毁
bool DestroyQueue(LinkQueue& Q) {
while (Q.front) {
Q.rear = Q.front->next;
delete Q.front;
Q.front = Q.rear;
}
return true;
}
//判空
bool QueueEmpty(LinkQueue Q) {
return (Q.front == Q.rear);
}
//取队头元素
bool GetHead(LinkQueue Q, QElemType& e) {
if (Q.front == Q.rear)
return false;
e = Q.front->next->data;
return true;
}
//入队
bool EnQueue(LinkQueue& Q, QElemType e) {
QueuePtr p = new QNode;
if (!p) exit(OVERFLOW);
p->data = e; p->next = NULL;
Q.rear->next = p;
Q.rear = p;
return true;
}
//出队
bool DeQueue (LinkQueue &Q,QElemType &e){
if(Q.front==Q.rear) return false;
QueuePtr p=Q.front->next;
e=p->data;
Q.front->next=p->next;
if(Q.rear==p) Q.rear=Q.front;
delete p;
return true;
}
//遍历
void TraversalQueue(LinkQueue Q) {
QueuePtr p = Q.front->next;
while (p) {
cout << p->data << " ";
p = p->next;
}
cout << endl;
}
int main() {
LinkQueue Q;
QElemType e;
int n, i;
// 初始化队列
if (!InitQueue(Q)) {
cout << "初始化失败!" << endl;
return 1;
}
cout << "请输入您要插入元素的个数:";
cin >> n;
// 入队操作
cout << "请输入元素:";
for (i = 0; i < n; i++)
{
cin >> e;
EnQueue(Q, e);
}
// 遍历队列
cout << "遍历队列: ";
TraversalQueue(Q);
// 出队操作
if (DeQueue(Q, e)) {
cout << "出队元素: " << e << endl;
}
// 遍历队列
cout << "遍历队列 : ";
TraversalQueue(Q);
DestroyQueue(Q);
return 0;
}
总结
以上就是我对队列的理解,至于为什么没有删除和插入这两个操作,在最开始的前提引入中我就提到,队列就像排队,排队过程中如果插队和被删除是一种不文明的行为。
谢谢大家的观看以及支持。