当前位置: 代码迷 >> 综合 >> m数据结构 day6 队列:先进先出,只能在一端插入,而在另一端删除的线性表
  详细解决方案

m数据结构 day6 队列:先进先出,只能在一端插入,而在另一端删除的线性表

热度:91   发布时间:2024-02-04 20:02:06.0

文章目录

  • queue的应用举例(用了先进先出的排队思想的都算)
  • 队列的抽象数据类型
  • 头尾相接的顺序存储结构:循环队列
    • 空队列和满队列中,front和rear重合
      • 办法一:加一个标志变量flag
      • 办法二:改变满队列的定义(用更多)
        • 队列是否满的判断条件
        • 队列长度
        • 循环队列顺序存储结构相关代码
  • 队列的链式存储结构:链队列

队列是一种FIFO(first in first out)线性表,它和栈一样,本质是线性表,但是不同的是他只允许在一端插入,另一端输出。

允许插入的一端叫做队尾,允许删除的一端叫做队头。总是删除队头元素,插入总是插入到队尾。这无疑是一种非常符合人类生活习惯的数据结构,所以很好学。
在这里插入图片描述
虽然符合人类生活习惯,但是在程序设计中也一样用的极其频繁,下面是一些例子

queue的应用举例(用了先进先出的排队思想的都算)

  • 电脑的鼠标操作
    在这里插入图片描述
    我有!原来如此!
  • 客服电话排队
    在这里插入图片描述
  • 键盘输入
    各种被敲击到的键会进入到一个队列
  • 记事本软件等的输出
    把字符流按照队列顺序显示在记事本中,比如你敲击的是god,显示的就也是god,而不是dog,哈哈哈,先进先出

队列的抽象数据类型

ADT Queue 
Data//元素具有相同数据类型,相邻元素具有前驱后继关系。
OperationInitQueue(*Q);//建立一个空队列DestroyQueue(*Q);//若Q存在,则销毁ClearQueue(*Q);QueueEmpty(*Q);GetHead(*Q, *e);//把队头元素存入eEnQueue(*Q, e);//把新元素插入到Q中DeQueue(*Q, *e);//删除Q的队头元素,用e返回其值QueueLength(*Q);
endADT

头尾相接的顺序存储结构:循环队列

队列也是线性表,所以和栈一样,也有顺序存储结构和链式存储结构两种。

顺序存储结构还是要用数组实现,但是并不是像栈那样简单地使用,而是要做成一个循环的队列。因为栈只在一端插入或删除,所以直接用数组,把数组为0那端作为栈底就妥了。

但是如果简单地用数组直接实现队列,则数组索引较大的一端是队尾,这样便于插入新元素时直接插在数组下一个空位,则数组索引为0那边是队头,继续思考你就会发现问题了。队尾增加元素确实还比较方便的,时间复杂度是O(1)。但是队头元素的删除呢?如果我们假设只能把数据存在数组的前n个位置,则每次删除队头元素,就需要把所有后续元素往前移动一个位置,时间复杂度是O(n),如下图。
在这里插入图片描述

为了不移动后续所有元素,我们取消元素必须一直存在前N个位置的假设,而是用一个头指针front指向队头元素,和一个尾指针rear指向队尾元素的下一个内存位置。这样的话,删除队头元素只需要改变front指针,时间复杂度也变成O(1)了。插入到队尾只需要改一下rear指针,当然时间复杂度还是O(1)。

如果rear指向队尾元素,则队列长度为1时,front和rear指向同一个位置,重合了,这样不便于处理,我们不希望front和rear指向同一个位置
在这里插入图片描述

你以为这样子的队列就丝滑完美了吗?不,还有问题。你想,不断删除队头结点会使得front指针不断后移,而rear指针不变。这时候用来实现队列的数组的前端有很多空位。如果这时候再往队列中不断添加新元素,知道队尾达到了数组的长度,这时候rear指针不断增大,直到指向数组后面那个内存位置,如上图右边,就不能再增加新元素了(这叫做假溢出),否则就会内存越界,可是明明数组的前端还有很多空位可用啊。

为了解决假溢出,我们不要让rear指针指向数组后方内存,本身这样做也是很危险的,一般只有迫不得已才这样做,而让rear指针指向数组的头部,索引为0处,这样形成一个环,头尾相接,则可以利用数组前端空位。也永远不会出现指针指向不明的问题。

所以,经过上面一番问题的出现和解决,我们设计出了顺序存储结构的队列:循环队列。我们也理解了为什么队列用顺序结构存储时必须是循环的,而栈不是。

空队列和满队列中,front和rear重合

让rear指针指向队尾元素的下一个位置,还是会有两种情况下,front会和rear相等:

  • 空队列:front == rear == someValue(我之前以为是NULL,后来发现并不是!因为使用数组实现,所以这里的front和rear指针实际是数组下标,是整数,所以不会是NULL)
  • 满队列: front == rear == someValue
    在这里插入图片描述
    由于空队列时俩指针也不是NULL,而是某个整数(数组下标),那怎么解决这个问题呢?有两个办法,都是用多用点空间的办法来换取两指针重合的情况判断。

办法一:加一个标志变量flag

设置一个标志变量flag,当标志为0且俩指针重合,则是空队列。这用了额外的一个字节,因为我们一般不会只分配一个比特,虽然表示这个标志位只需要一个比特。

  • 空队列:front == rear && flag == 0
  • 满队列:front == rear && flag == 1

办法二:改变满队列的定义(用更多)

满队列会遇到front和rear重合是因为我们把数组的空间全用完了。如果我们省出一个位置不用,即还剩一个位置为空时我们就认为队列已经满了,不能再插入新元素了,那么两个指针在满队列下也不会重合了。

这样一来,就只有空队列会让两个指针重合了

无需利用外部变量,直接利用队列本身就可以实现,所以这种办法用的更多。后面我们也只实现这种办法。

如下图,左右都是新方案的满队列。用一个数组位置来换取指针重合的唯一性。

在这里插入图片描述

队列是否满的判断条件

从上图可以看到,rear可能比front大,也可能比front小。满队列时,二者的差值可能是1,也可能是N-1(N是数组长度)。

可以确定的是,如果rear比front大且队列为满,则一定是上图左边这种满队列情况,一大就大一整圈。front一定指向第一个位置(front = 0),而rear一定指向最后一个位置(rear = N - 1)
r e a r = f r o n t + N ? 1 = N ? 1 rear = front+N - 1 = N - 1

如果rear比front小且队列为满,则一定是上图右边这样,rear比front小1
r e a r = f r o n t ? 1 rear = front - 1

综合这两种情况可以发现,队列满一定有
( r e a r + 1 ) % N = f r o n t (rear + 1) \% N = front

队列长度

另外,不考虑队列是否为满,如果 r e a r > f r o n t rear>front ,则队列长度为 r e a r ? f r o n t rear - front ,如上图左子图。
如果 r e a r < f r o n t rear<front ,则队列长度f分为两段,一段为 N ? f r o n t N - front ,另一端为 r e a r rear ,如上图右子图。所以队列长度是
N ? f r o n t + r e a r N - front + rear

综合这两种情况,队列长度的通用公式则为:
( r e a r ? f r o n t + N ) % N (rear - front + N) \% N

总结来说,如果队列中数据元素的类型占用空间比较大,比如是某个类的对象,这个类有很多私有数据成员,则适合用第一种办法,如果队列中存储的数据的类型是int等类型,占用内存和1字节差不多的,对内存又不是特别严苛,就可以使用第二种办法。

循环队列顺序存储结构相关代码

队列结点的结构体

Typedef struct
{ElemType data[SIZE];int front;//无论数据类型是什么,指针类型都是整型int rear;
}SqQueue;

初始化

Status InitQueue(SqQueue * Q)
{Q->front = Q->rear = 0;return OK;//使用数组,没有要求自己分配数组的内存,所以不需要malloc
}

求队列长度

int QueueLength(SqQueue * Q)
{return (Q->rear - Q->front + SIZE) % SIZE;
}

入队列操作

Status EnQueue(SqQueue * Q, ElemType e)
{if ((Q->rear + 1) % SIZE == Q->front)return ERROR;//已满Q->data[Q->rear] = e;if (Q->rear == SIZE - 1)Q->rear = 0;else++(Q->rear);return OK;	
}

改变尾指针的那段代码可以简化为如下:

Status EnQueue(SqQueue * Q, ElemType e)
{if ((Q->rear + 1) % SIZE == Q->front)return ERROR;//已满Q->data[Q->rear] = e;Q->rear = (Q->rear + 1) % SIZE;return OK;	
}

出队列操作

Status DeQueue(SqQueue * Q, ElemType * e)
{if (Q->rear == Q->front)return ERROR;//空*e = Q->data[Q->front];Q->front = (Q->front + 1) % SIZE;return OK;
}

到此为止,我们学会了队列的顺序存储结构,用数组实现的循环队列,可以通过上面几个基本函数看到循环队列的时间复杂度还是很不错,入队,出队,返回队长度,初始化的时间复杂度全都是O(1)。

但是他再好,也是顺序结构,即要底层要用数组实现,那么只要用数组就一定会有数组的缺点:需要提前设置数组长度,长度固定后不能改。这使得循环队列的灵活性差了一些,存的项最多就N-1个(有一个空着,为了使得满队列时两指针不重合,只有空队列才重合)。

要让长度灵活,当然还是要选择链表,动态实时地分配,随用随分配内存,十分方便。

队列的链式存储结构:链队列

本质上就是线性表的单链表,只是他只能尾进头出罢了。

front指针指向头结点(链表喜欢设置一个头结点以使得空队列和普通队列处理统一),rear指针指向最后一个结点(和循环队列不同)。
在这里插入图片描述
在这里插入图片描述

队列的结点

typedef struct QNode
{ElemType data;struct QNode * next;
}QNode, *QueuePtr;//这里写两个则struct不能是匿名的,且第一个和结构名一样.QueuePtr即QNode *

队列的链表结构

typedef struct
{QueuePtr front, rear;//只有队头队尾指针
}LinkQueue;

入队操作

Status EnQueue(LinkQueue * Q, ElemType e)
{//不用判断是否已满,因为是链表(QueuePtr) q = (QueuePtr)malloc(sizeof(ONode));\if (!q)return ERROR;//分配失败,或者写exit(OVERFLOW);q->data = e;q->next = NULL;Q->rear->next = q;Q->rear = q;return OK;
}

出队操作

Status DeQueue(LinkQueue * Q, ElemType * e)
{QNode * q;if (Q->front == Q->rear)return ERROR;//空*e = Q->front->next->data;q = Q->front->next;Q->front = Q->front->next;free(q);return OK;
}
  相关解决方案