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


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

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

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

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

队列的抽象数据类型

ADT Queue 
Data//元素具有相同数据类型,相邻元素具有前驱后继关系。
Operation
	InitQueue(*Q);//建立一个空队列
	DestroyQueue(*Q);//若Q存在,则销毁
	ClearQueue(*Q);
	QueueEmpty(*Q);
	GetHead(*Q, *e);//把队头元素存入e
	EnQueue(*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+N1=N1

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

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

队列长度

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

综合这两种情况,队列长度的通用公式则为:
( r e a r − f r o n t + N ) % N (rear - front + N) \% N (rearfront+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;
}
### 回答1: 栈和队列都是线性表一种,但它们的插入删除操作有所不同。栈的插入删除只能一端进行,这一端通常称为栈顶,而另一端称为栈底。栈的插入操作称为入栈,删除操作称为出栈。栈的特点是后进先出,也就是最后入栈的元素最先出栈。 队列插入操作也只能一端进行,这一端通常称为队尾,而删除操作则在另一端进行,称为队头。队列插入操作称为入队,删除操作称为出队。队列的特点是先进先出,也就是最先入队的元素最先出队。 ### 回答2: 栈和队列数据结构中的两种常见数据类型,它们在很多算法和程序中都扮演着重要的角色。栈和队列都是线性表结构,其最大的区别在于它们的插入删除操作的实现方式。 首先,栈是一种后进先出(LIFO)的数据结构,即最后一个插入的元素先被删除。由于栈只允许在某一端进行插入删除操作,因此它具有非常高效的特性。栈一般有两种实现方式:顺序栈和链式栈。在顺序栈中,元素以数组的形式在内存中连续存储,栈顶指针指向栈顶元素的位置。在链式栈中,则是利用指针将栈内元素相连,每个元素都包含了下一个元素的指针信息和数据。 其次,队列一种先进先出(FIFO)的数据结构,即最先插入的元素先被删除。由于队列的特性,插入删除操作分别在两端进行,即插入操作在队尾进行,删除操作在队头进行。也因此,队列的实现方式也有两种:顺序队列和链式队列。与栈相似,顺序队列是在内存中连续存储元素的数组,队头和队尾指针分别指向队列的头和尾部。而链式队列则是利用指针将元素相连,并有头指针和尾指针来指向队列的头和尾。 总的来说,栈和队列是两种常用的数据结构,在算法和程序的实现中具有广泛的应用。它们的实现方式和特性有着显著的区别,因此在不同场合和算法中分别选用它们可以获得更高的效率。对于程序设计人员而言,了解栈和队列的实现方式和操作特性是十分重要的。 ### 回答3: 栈和队列是常见的数据结构,在算法和编程中用到极为广泛。它们都是线性表一种线性表是指数据元素之间一一对应的关系,也就是元素之间只有一种线性关系。 栈是一种插入删除只能一端进行的线性表。它主要有两个特点:后进先出和只能在栈顶进行插入删除。后进先出是指后进入栈的元素先弹出,类似于人们在餐厅堆盘子,后盘子先取出。而只能在栈顶进行插入删除是因为栈顶是最后一个插入的元素,也是最先弹出的元素。 栈的应用非常广泛,例如函数的调用和返回就是栈的一个典型应用。每一次进入函数时,都会将当前函数的程序计数器、参数和局部变量等信息压入栈中,当函数返回时再将这些信息弹出栈。而在表达式求值中,栈可以用来保存运算符和操作数,方便后续的计算。 队列一种插入一端进行,删除另一端进行的线性表。它主要有两个特点:先进先出只能在队尾插入,在队头删除先进先出是指先进入队列的元素先弹出,类似于人们排队等候,先来的先服务。而只能在队尾插入,在队头删除是因为队尾是最后一个插入的元素,而队头是最先弹出的元素。 队列的应用也非常广泛,例如操作系统的进程调度就是队列的一个典型应用。在调度时,系统将新来的进程插入到就绪队列的队尾,而在CPU空闲时,将就绪队列的队头进程调度出去执行。而在图论中,队列可以用来实现广度优先搜索算法。 总的来说,栈和队列是算法和编程中的重要数据结构,它们都有各自的特点和应用场景,掌握它们的特点与应用,可以对我们日后的编程和算法设计具有很大的帮助。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值