勿看 计算机操作系统——第三节堆栈的使用
定义
这个小节中我们会先认识占的基本概念
我们会从占的定义和站的基本操作来认识站这种数据结构
也就是要探讨站的逻辑结构
还有我们需要实现的一些基本运算
在后续的小节我们再来探讨用不同的存储结构实现站
那这一页的课件和线性表的第一页课件其实几乎是一模一样的
因为无论我们讨论什么样的数据结构
肯定都需要从数据结构的三要素作为出发点来依次分析和探讨
那我们这个小节中要学习的栈和线性表之间其实有很紧密的联系
那这是我们之前学过的线性表的概念
而占其实也是一种特殊的线性表
栈的定义
只不过对于普通的线性表来说
当我们要插入一个数据元素或者删除一个数据元素的时候
我们可以在任意一个地方插入和删除
但是对于站这种数据结构来说
我们会限制它的插入和删除操作
插入和删除操作只能在线性表的一端进行
也就是说如果我们要插入一个数据元素
那我们只能在表尾的这个位置进行
我们不可以在其他的这些地方进行插入操作
这是占这种数据结构所不允许的
同样的如果要删除一个数据元素的话
那我们同样只能从这边进行一次的删除
不同点,只允许一端进行操作
相当于是石头堆起来一样
栈只允许在一端进行插入和删除
其实占的英文术语也就是stack
它的含义可以很好地表现出他这所说的这种性质
来看一下stack有个含义叫做整齐的一堆
这给了一张图片
一个整齐的小石头堆好
那现在如果你有一个新的石头
想要放到这个石头堆里
你是不是只能放到这个石头堆堆顶的位置啊
而如果你要拿走一个石头的话
你也只能从顶部拿走一个
如果你强行把中间的某一个石头直接抽掉的话
那整个石头堆它就散了对吧
所以其实这个英文术语的含义更能体现
占这种数据结构的一个呃核心的特性
那我们在生活中其实也有很多站
比如说大家帮家里洗碗的时候
它也是一种线性表
因为这些数据元素之间我们从逻辑上看
它也是穿成了一条线
也是有这样一前一后一对一的关系
只不过占这种特殊的线性表
它的插入操作和删除操作是有限制的
只能从其中的异端进行插入和删除好
顶栈地和空栈
那空战很简单
就是这个站里边没有存任何数据元素
其实就对应线性表的空表对吧
然后我们往一个站里面放入元素的时候
肯定是从其中一端一个一个压入的
所以栈顶和栈底这两个术语其实也很直观
栈顶就是指允许进行插入和删除的这一端
而占底指的是不允许插入和删除的这一端
那相应的最上面的这个元素被称为栈顶元素
最下面的这个称为栈顶元素
那刚才的动画中
这几个数据元素的近战顺序分别是a1 a2 a3 a4 a5
依次被压入栈中
那接下来如果让这几个数据元素出战
或者说依次删除这些数据元素的话
那么出站的顺序应该是a5 先出
接下来是a4 a3 a2 a1
刚好是近战顺序的一个逆序
所以站的特点就是后进先出
后进入这个站的数据元素会先出长
那很多地方在描述后进先出这种特性的时候
会用这样的一个英文缩写
li fo就是last in first out一个缩写
因此通过刚才的这些讲解
可以看到
这是一种特殊的线性表
它和普通的线性表相比
其实逻辑结构是一样的
这些数据元素之间有这样一对一一前一后的逻辑关
穿越:线性的基本操作
就是创建销毁
还有增删查这几个基本操作对吧
那站需要实现的基本操作也是一样的
首先是创建和销毁
创建一个栈
也就是初始化一个站就是要分配相应的内存空间
而销毁一个站就是要释放这个站所占用的内存空间
那接下来是增删两个操作
我们用push和pop来表示进站和出站
也就分别对应增加一个数据元素和删除一个数据元素这两个基本操作
所谓进栈就是要把数据元素x放到战s的栈顶
而出战就是要把栈s它的顶点元素给删除
并且用x这个变量给带回去
所以这才加了一个引用符号好再往后的操作是读取栈顶元素
get top会用变量x返回这个栈顶的元素
那这个基本操作和上面这个基本操作的区别是
上面出战的这个基本操作
除了返回栈顶元素之外
还会把栈顶元素给删除
但是get up这个基本操作它只会读取栈顶元素
用x来返回
但是并不会把站点元素给删除
那get up这个基本操作其实就是对应了查在普通的线性表当中
我们需要实现按位去查找这样的功能
但是对于站的使用场景来说
大部分情况下
当我们要访问一个数据元素的时候
通常只需要访问栈顶的这个元素
所以在查找数据元素的时候
通常我们只需要找到这个栈顶的元素就可以好
再往后还有比较常用的这个基本操作
就是判断这个站是否是一个空战
如果是空的话
返回true
否则返回false
喜欢出的题目
这里我们需要注意点的是,
是的,您理解得非常正确。在栈的操作中,确实需要保持 𝑎a 是最后一个出栈的元素。这是因为 𝑎a 是第一个进栈的元素,根据栈的后进先出(LIFO)原则,𝑎a 必须在其他所有元素都出栈之后才能出栈。
在这张图中,给出了两个合法的出栈顺序示例:
- 𝑒,𝑑,𝑐,𝑏,𝑎e,d,c,b,a
- 𝑏,𝑒,𝑑,𝑐,𝑎b,e,d,c,a
这两个顺序都符合栈的操作规则,并且确保了 𝑎a 是最后一个出栈的元素。
此外,图中还提到了卡塔兰数的计算方法,用来确定不同进出栈序列的数量。对于 𝑛=5n=5 的情况,卡塔兰数 𝐶5C5 的值是 42,这意味着有 42 种不同的合法出栈顺序。
小结
般都是选择题
给你abcd几个选项
然后让你判断某种出战的顺序是否合法
那这是占这种数据结构最喜欢考察的一种形式啊
总之核心就是我们要知道占这种数据结构
我们的插入和删除操作只能在栈顶进行
它是一种操作受限的线性表
而由于插入和删除只能在栈顶进行
因此它就拥有一个特性叫做后进先出
后进入栈的数据元素反而会先出战好的
那以上就是这个小节的全部内容
顺序栈
定义
顺序站相关的基本操作
其实和我们之前学过的顺序表
链表那些是很类似的
比较重要的是如何初始化
然后怎么实现进栈
也就是怎么增加一个数据元素
还要怎么实现出栈
也就删除一个数据元素
另外还需要探讨如何获取栈顶元素
还要如何判断一个栈空
如何判断栈满这几个问题好
初始化顺序栈
初始化操作
struct结构体定义
那顺序站它既然是用顺序存储的方式实现的
那它的代码的定义实现方式
其实就和我们的顺序表是非常类似的
定义了一个struct结构体
让我们用type define把它重命名为sq stack
然后这个结构体里面包含了一个静态的数组data
用于存放栈中的各个元素
那这个地方sq就是sequence的缩写
也就是顺序的意思
所以接下来我们就可以在自己的函数里边
用这样的方式声明一个顺序站栈
那在执行了这句代码之后
就会在内存当中分配这样的一整片的
连续的空间
这整片空间的大小应该是max size
也就是占的最大容量
再乘以每一个数据元素它的大小
另外还需要在内存当中分配啊
四个字节的大小用于存放top这个变量
那这个变量它其实是栈顶指针-----指向此时这个栈的栈顶元素
其实这个就和数组下标一样的情况
一般来说这个栈顶指针它记录的是数组的下标
也就是从零开始的好
那假设现在我们的站中
已经压入了这样的几个数据元素
那此时这个栈顶指针的值就应该是四
也就是指向栈顶元素的这个位置
这个是数组的下标好
那在分配了存储空间之后
栈顶指针要让它指向此时栈顶元素的位置
所以刚开始的时候
top指针指向零这个位置其实是不合理的
因为data 0这个位置此时还没有存放数据元素
所以刚开始我们可以让top的值指向-1
因此按照这样的逻辑
我们要判断一个栈是否为空的时候
只需要判断它的top指针是否等于-1就可以好
那到这一步
增删改查
增
增加一个数据元素的操作
那对于栈这种数据结构的插入操作
我们通常把它称为进栈
首先第一步需要判断这个战此时是否已满
因为顺序站是用静态数组的方式实现的
它有一个最大容量的上限。就是判断这个栈的顶点指针是否到了它的上限减一
后面进行第二步操作
在右边这种情况下
此时栈是不满的
因此可以继续往后执行
接下来我们是不是想往这个data 0这个位置
给插入一个新的数据元素
因此我们可以把top指针先让它加一
也就是从-1变为零
然后接下来把此次传入的这个数据元素x
把它存到data数组里边
时top指针所指向的这个位置
也就是放到这个地方
然后给调用者返回一个true
表示此次入站操作成功好
每次移动位置都要让top指针加上1
那是不是依然需要把top指针让它往后移一位
然后再把这一次的数据元素给放到相应的位置
总之由于我们的top指针
它永远都是指向此时的这个栈顶元素的位置
因此当一个新元素入站的时候
我们肯定都需要先让top指针加一
然后再往top指针指向的位置放入新的元素
错误写法
那这句代码和左边的这种写法是等价的
加加top的意思就是说先让top的指针值加一
然后再来使用这个top的值
而如果你没注意写成了top加加的话
那么就相当于你先使用了top的值
然后再让top的值加一
放在右边这个例子当中
假设我们此时想要插入一个新的数据元素
c那c这个数据元素它合法的啊
我们期待的插入位置应该是插入到这个地方
也就data 2这个位置好
此时top的值是一
那如果你使用的是top加加这样的写法的话
那么就相当于你先把此次要传入的这个
数据元素c先把它放到了啊data一当中
也就相当于你让数据元素c
把以前的这个数据元素b给覆盖了
然后接下来才进行top加一的操作
也就让top指针指向这个位置
所以这种执行就是一种错误的结果
对不起,我之前的回答有误。在栈的操作中,确实是应该先递增指针(top++),然后再将元素入栈。这样可以确保新元素被放置在正确的位置,并且top”始终指向栈顶元素。
让我来更正一下之前的解释:
当你想要将一个新元素压入栈时,你应该先递增top”,然后再将元素放入data[top]”。这样可以确保新元素被放置在正确的位置,并且top”始终指向栈顶元素。
例如,假设当前栈中有元素 'a', 'b', 'c',且top”指向 'd' 的位置(即top”的值是 3)。当你想要将新元素 'd' 压入栈时,你应该这样做:
先递增top”(即 top” 变为 4)。
然后将 'd' 放入 data[top]”(即 data[4]”)。
这样,top”就始终指向栈顶元素,而不是下一个可用位置。这是栈的基本操作方式之一,确保了元素能够按照正确的顺序进出栈。
再次为我之前的错误回答向您道歉,感谢您的指正和理解。
如果是顺序栈已经满了的话,
我们的顺序占它的容量是十
此时已经全部存满了
那top指针指向栈顶元素
也就是指向这个位置
它的值应该是九为什么呢
因为我们从0开始算起的
出栈操作的话
出栈操作其实就对应了
增删改查的删
也就是要删除栈顶的一个元素
并且用变量x来返回
那这个地方x这个变量加了引用符号
也就是说这个出站操作的调用者
他首先会在自己的函数里面定义一个变量
叫做x这个变量存放在内存中的某一个位置
那由于这加了引用符号
所以在出站操作里边操作的这个变量
x和函数的调用者
定义的这个变量x对应的是同一份数据
而不是它的复制品
引用符号的要求(会直接)
-
变量 x 和引用符号:
- 在函数调用中,如果变量 x 前面有引用符号(例如 C++ 中的 &),那么这个变量是通过引用传递给函数的。
- 引用传递意味着函数内部对引用的操作会直接影响调用者作用域中的原始变量。
先出栈再减一
判断栈是否是空的
判断栈是否为空:
- 在执行出栈操作之前,首先需要判断栈是否为空。如果栈为空(即 top 等于 -1 或 0,取决于你的实现),则不能执行出栈操作,因为这会导致错误。
出栈操作:
- 假设栈不为空,可以进行出栈操作。在右侧的例子中,栈不为空,因此可以继续执行出栈操作。
- 将栈顶元素(即
data[top]
)赋值给变量 x,这样就将栈顶元素的值复制到了调用者提供的变量 x 中。- 然后将 top 的值减一(即
top--
),这表示栈顶指针下移,逻辑上删除了栈顶元素。逻辑删除与物理删除:
- 在删除操作中,虽然我们将 top 指针下移了一位,这在逻辑上删除了栈顶元素,但实际上该元素的数据仍然残留在内存中,直到被新的数据覆盖
正确的结果:
- 为了得到正确的结果,应该使用
x = data[top]; top--;
或者x = data[top--];
(后缀递减),这样会先使用 top 的当前值来赋值给 x,然后再将 top 减一。
读栈操作
出站操作会让top指针减减往下移位
但是读站点元素
这个操作
他只是把此时top指针指向的这些数据元素
用x给返回
但并不会让top指针减
另一种方式设置
那刚才我们提到所有的这些代码
都是让top指针指向此时的站点元素
用了这样的一个方案
那其实我们还可以用另外一种方式来设计
我们可以让top指针刚开始指向零这个位置
那相应的判断栈是否为空
就是看top是否为零
那这儿的这种实现方式
我们是让top指针指向了下一个
我们可以插入元素的位置
因此如果接下来有一些数据元素入栈的话
那么top指针应该是指向这个位置
好那如果我们这么设计的话
就是相当于我们的top指针开始自动的跳到了下一个我们需要存的位置
我们是不是需要先把x放到此时
ttop指针指向的这个位置
然后再让top指针加一对吧
这和之前的那种方式刚好是相反的
在这种情况下
我们就应该用top加加
而不是加加top那类似的
如果此时要按站点元素出战的话
那么我们应该先让top的值先减一
然后再把top指向的数据元素给传回去
因此就应该用减减top这样的操作
所以大家在做题的时候一定要注意审题
这个top指针它到底是要让它指向栈顶元素
还是要让它指向栈顶元素后面的一个位置
两种方式实现代码是不一样的
好那按照右边的这种设计方式
如果这个站已经存满了的话
那么top指针它的值应该是等于max size
也就是等于十
因此判断占满的条件也会不一样
那可以看到
由于顺序栈是用了一个静态数组
来存放数据元素
因此当这个静态数组被存满的时候
它的容量是不可以改变的
那这是顺序站的一个缺点
如何解决这个问题呢
可以用链式存储的方式来实现站
或者我们也可以在刚开始的时候
给这个站分配一个比较大片的连续的存储空间
但是刚开始就分配大片的存储空间
这会导致内存资源的浪费
那其实可以用共享站的方式来
提高这一整片内存空间的利用率
共享站的意思就是说
两个站共享同一片存储空间
我们会设置两个栈顶指针
分别是0号站和1号站的站点指针
然后0号站的栈顶指针刚开始-1
1号站的占领指针
刚开始是max size
接下来如果要往0号站放入数据元素的话
那么就是从下往上依次递增的
而如果要往1号站放入数据元素的话
那么这个栈的栈顶又是从上往下依次递增的
那这样的话在逻辑上我们这儿实现了两个站
但是物理上他们又是共享着同一片的存储空间
这就会提高内存资源的利用率
好那这个共享站它也是会被存满的
判断共享站是否满了的条件
就是你判断一下top 0
他再加一是否等于top一的值好
那共享站相关的代码
有兴趣的同学也可以自己去实现一下
这一小节中
我们学习了如何用顺序存储的方式实现站
那用这种方式实现的站就称为顺序站
我们要定义一个静态数组来存放数据元素
另外还需要在struct结构体里面
定义一个栈顶指针
我们可以让这个栈顶指针指向当前的栈顶元素
也可以让栈顶指针指向
接下来可以插入数据元素的这个位置
两种设计方式所对应的初始化操作会各不相同
另外再增加一个数据元素
和删除一个数据元素的时候
代码实现也会有所不同
这个是比较容易错的地方
所有的这些基本操作都可以在o一的时间
复杂度内完成
那怎么销毁一个顺序站呢
首先是要在逻辑上把这个站给清空
接下来再回收这个站所占用的内存资源
在逻辑上清空一个站
其实只需要让top指针指向初始化的那个位置
就可以了
在这个小节的代码当中
我们是使用了变量声明的这种方式
来分配相应的内存空间
并没有使用malloc函数
所以给这个站分配的内存空间
也会在你的这个函数结束之后
由系统自动的回收
所以其实回收内存资源这个事情
你并不需要管好的
那以上就是这一小节的全部内容
小结
栈的链式存储实现
在这个小节中我们会学习如何用链式存储的方式来实现栈
那用链式存储的方式实现了站叫做链栈
同样的在确定了存储结构之后
我们也需要探讨基于这种存储结构
怎么实现相应的这些重要的基本操作好
头插法来建立单链表
所谓用头插法建立一个单链表
就是指当我们要插入一个数据元素的时候
我们都是把它插入到这个头节点之后的位置
就像这个样子
们对这个单链表进行插入操作
的时候都是在这一端进行插入的
那这其实不就是站的近战操作吗
如果我们规定只能在单链表的这一端进行插入操作的话
那这就是进栈操作
那类似的
如果我们规定当我们对这个单链表进行删除操作的时候
我们同样只能在这一端进行删除操作
就像这个样子
那这种有限制的删除操作不就是我们站里边的出栈操作吗
本质
它本质上也是一个单链表
只不过我们会规定只能在这个单链表的这一端进行插入和删除操作
也就是把链头的这一端
把它看作是我们的栈顶的一端
所以大家会看到对于链战的定义
其实和单链表的定义几乎没有任何区别
只是这些名字稍微改了一下而已
那和单链表类似
当我们用链式存储的方式来实现恋战的时候
我们是不是也可以实现带头节点的版本和不带头节点的版本
当然两种设计方式对于栈是否为空的这个判断会不一样
那进站和出站操作其实就是对应了单链表当中的
插入数据元素和删除数据元素的操作
只不过插入和删除我们只能在这个表头的位置进行
那如何在表头的位置插入和删除
这个我们在单链表那个小节当中已经讲得非常详细了
这就不再赘述
大家可以结合之前单链表相关的那些知识
来把恋战相关的这些基本操作给实现一遍
那我们的课本当中呃
推荐实现恋战的时候
就是使用不带头节点的这种情况
但是不管带头节点还是不带头结点
大家都需要会写
队列
队列的定义
那队列其实它也是一种操作受限的线性表
之前我们学习的站是只允许在线性表的某一端进行插入和删除
而队列这种数据结构是只允许在其中的一端进行插入
在另一端进行删除
对队列的插入操作一般把它称为入队
而对队列的删除操作一般把它称为出队
其实相比于之前学习的战队列
这个术语一看就知道它是什么意思
我们的生活中就有许多队列
比如说大家去食堂打饭的时候
其实就是排成了一个队列
如果此时有一个人他也想打饭的话
那他只能插入到这个队列的对位
队头、队尾和空队列其实很好理解对吧
如果一个队列里边此时没有任何数据元素的话
那么这个队列就是一个空队列
我们可以往一个队列中插入数据元素
那么允许插入数据元素的这一端
我们就把它称为队位
此时队列中最靠近队尾的这个元素就是队尾元素
那相应的可以进行删除操作的这一端
我们就把它称为队头
那相应的这个元素就应该是对头元素好
再次强调队列的特点叫先进先出
很多时候在考试中喜欢用这种英文缩写来表示先进先出
f i f o也就是first in first out的缩写好
那这顺道回忆一下栈的特点是叫后进先出对吧
l i f o也就是last in first out好
那这就是队列这种数据结构
其实本质上它也是一种线性表
只不过对队列的插入和删除操作是有限制的
只能在一端进行插入
另一端进行删除
那我们需要实现的对队列的基本操作其实和线性表也是一样的
就是创销增删改查这些东西
队列出栈入栈
创建一个队列和销毁一个队列
其实就是给这个队列分配相应的内存空间和回收相应的内存空间
其实和我们之前学过的内容几乎是一样的
只不过对队列的插入操作
我们通常把它称为入队
而对队列的删除操作
通常把它称为出对
并且新的数据元素x它只能从队尾的这个位置入出队
对这个操作会把对头元素删除
同时用x给返回
和它很像的是读对头元素这个操作
但是这个操作只会把对头元素的值用x这个变量给返回
但是并不会删除对头元素
这个其实和站的出站还有独占顶元素是相对应的
另外判断一个队列是否为空
也是一个很常用的操作由于插入操作只能在队尾
删除操作只能在队头进行
所以这就导致了队列拥有先进先出的特点
好那对队列的这些基本操作
我们之后还会结合不同的存储结构来进行更进一步的讲解
那以上就是这个小节的全部内容
队列的顺序实现
那之前我们说过队列,它其实也是一种特殊的线性表,只不过是一种操作受限的线性表
因此基于之前学习的知识
不难想到
如果要用顺序存储的方式实现队列的话
那我们可以用静态数组来存储
队列当中的数据元素
同时由于队列的操作受限
我们只能从对头删除元素
只能从队尾插入元素
因此我们还需要设置两个变量rear 和front
用来标记队列的对头和队尾好
那定义了队列的结构体之后
我们就可以用变量声明的方式
来声明这样的一个队列
执行这句代码
们就可以用变量声明的方式
来声明这样的一个队列
执行这句代码
会导致系统在背后给我们分配
这样的一整篇连续空间
这片空间的大小是可以存放十个数据元素
也就是十个type那如果此时队列中有这
样的一些数据元素
然后下面是队头
上面是队尾的话
我们可以规定
让队头指针指向这个队头元素
让队尾指针指向队尾元素的后一个位置我们应该插入数据元素的这个位置
因此在右边这种情况下
front的值应该是零
而real的值应该是五好
如何判断队列尾空
那既然队尾指针是要指向,接下来应该插入数据元素的那个位置
因此按照这样的设计逻辑
我们就可以在初始化的时候
让对尾指针和对头指针同时指向零
因为对尾指针指向的这个位置
应该是接下来应该插入数据元素的位置
那我们可以用对尾指针和对头指针
它们所指向的位置是否相等
来判断这个队列此时是否为空好
那完成了队列的初始化工作之后
接下来就可以进行一系列后续的操作
当rea到了10,就会回来了
环状循环队列
运算或者取余运算是什么意思
a对b进行取余运算的结果
其实就相当于a除以b
然后它们的余数是多少
其实就是我们小学时候学的
最简单的那些除法
比如23对七取余应该是等于多少呢
那这就相当于23÷7
然后三七二十一3-1=2
也就是余数为二
因此这个区域操作它的结果就应该是二
那在有的地方也会用a mode b来表示
这个取余运算
很显然
任何一个整数x它除以七
最终得到的这个余数
只有可能是0123456
这样的几个可能性
这个余数不可能大于等于七对吧
所以这种模运算或者说取余的运算
其实是把无限的整数域
把它映射到了有限的整数集合上
因此我们这儿利用了取余运算
让尾指针的变化是从下到上
然后再回到最下面
然后再往上
这样循环往复的变化
其实就相当于把我们这这样的一个
现状的存储空间
在逻辑上
把它变成了这样的一个环状的存储空间
操作 7:入队元素 60
-
rear
的下一个位置是(4 + 1) % 5 = 0
,检查是否与front
相等。 -
此时
front
是 1,rear
可以移动到 0。 -
将
60
放入rear
的位置(索引 0)。
复制
队列: [60, 20, 30, 40, 50] front = 1, rear = 0
注意这里的话,逻辑上是环状的
那刚才说过
这儿的第二句代码
会导致对尾指针指向下一个
应该插入数据元素的位置
基础不好的同学可以暂停捋一捋
由于这个队列的存储空间
在逻辑上看似乎是一个环状
是一个循环
所以我们把用这种方式实现的队列
称为循环队列
作 9:出队
-
从
front
位置(索引 2)移除元素30
。 -
front
移动到下一个位置(索引 3)。
复制
队列: [60, None, None, 40, 50] front = 3, rear = 0
操作 10:入队元素 70
-
rear
的下一个位置是(0 + 1) % 5 = 1
。 -
将
70
放入rear
的位置(索引 1)。
复制
队列: [60, 70, None, 40, 50] front = 3, rear = 1
最终状态
复制
队列: [60, 70, None, 40, 50] front = 3, rear = 1
这个术语是有可能在选择题当中出现的好
那由于此时还有空闲空间
因此我们还可以继续往这个呃队列当中
插入新的数据元素
同时也需要让这个队尾指针不断地后移
当这个队列还剩最后一个存储空间的时候
我们就认为此时队列已经满了
可能有同学会觉得奇怪
这不是还有一个空闲的空间可以利用吗
往这儿插入一个新的数据元素
同时让rear指针指向后一个位置
这样不是也可以吗
但是需要注意的是
我们在初始化队列的时候
我们是让front指针和rear指针
指向了同一个位置
同时我们也是通过这两个指针是否指向同
一个位置来判断这个队列是否为空的
如果像刚才说的这样
往这个位置也插入了数据元素
同时让rear指针和front指针
指向同一个位置的话
那刚才说的逻辑
这两个指针指向同一个位置
是否就意味着这个队列为空了呢
这显然不对对吧
所以没办法
我们必须牺牲这样的一个存储单元
因此在新元素入队之前
我们可以检查对尾指针的后一个位置
它是否等于对头指针所指向的这个位置
如果满足这个条件
说明队列已经存满了
就直接return false
表示插入失败
看看AL的回答
初始状态
复制
队列: [None, None, None, None, None] front = -1, rear = -1
关键矛盾点
问题:为什么队列还剩一个空闲位置时,就认为队列已满?
矛盾:若允许最后一个位置插入元素,会导致front == rear
,但此时队列可能“满”或“空”,无法区分!
分步推演
步骤 1:插入 4 个元素(队列“逻辑满”)
假设依次插入
10, 20, 30, 40
,此时:复制
队列: [10, 20, 30, 40, None] front = 0, rear = 3
判满条件:
(rear + 1) % size == front
→(3 + 1) % 5 = 4
,不等于front
(0),队列未满。
步骤 2:插入第 5 个元素
50
此时
rear
移动到 4:复制
队列: [10, 20, 30, 40, 50] front = 0, rear = 4
判满条件:
(4 + 1) % 5 = 0
,等于front
(0)→ 队列已满!但此时数组中所有位置都被占用,没有空闲空间。
步骤 3:如果允许插入第 6 个元素
60
若强行插入
60
,rear
会循环到0
:复制
队列: [60, 20, 30, 40, 50] front = 0, rear = 0
判空条件:
front == rear
→ 系统会误判队列为空!矛盾:队列实际是满的,但
front
和rear
重合,导致逻辑混乱。
出队操作
是删除一个数据元素
那我们只能让对头元素出对
那第一步首先判断一下这个队列是否为空
如果队列空的话
直接就return一个force
表示出对操作失败
如果队列不空
那么就把对头指针指向的数据元素
把它赋给变量x
用这个变量让数据元素返回
接下来让front指针往后移一位
当然我们这儿也要对max size取模
这样才可以让front指针啊
转着圈圈移动好
那接下来类似的
只要这个队列不空
那么就可以继续
让这个队列的队头元素依次的出
对
每一次出队的
都是front指针所指向的元素
并且对头指针会每次往后移一位
那当对头指针和队尾指针
再次指向同一个位置的时候
此时就说明这个队列已经被取空了
此时就不能再进行输对操作
那这是出对操作的实现
也就是增删改查的删
那增删改查的查怎么实现呢啊在队列当中
我们通常只需要查询
或者说只需要读取对头的那个元素
所以它的实现很简单
把此时对头指针指向的这个元素赋给x
用x返回就可以了
其实就是把出对操作的这一句给删掉
不要让队头指针往后移就可以好
那刚才我们设计的这种方案当中
我们要判断一个循环队列是否已满的条件
是
是要看一下这个对尾指针的后面一个位置
是否和对头指针指向的是同一个地方
满足这些条件
那么就说明此时队列已满
而如果对头指针和队尾指针
指向同一个地方的话
那么就说明此时队列已经空了
我们可以很方便地
用对尾指针和对头指针的值
计算出这个队列当中
当前有多少个数据元素
计算方法就是这样
比如在左边这个例子当中
此时对尾指针的值是二
对头指针的值是三
那按照这儿的算法
整个队列的数据元素个数就应该是二
加上max size是十
然后再减掉三
在对max size进行取余
也就等于九对十取余计算结果是九
也就是说此时队列中有九个数据元素
其他的情况同学们可以自己验证一下
学过数论的同学
可以用模运算的性质来证明这个式子
不会证明的同学也需要把这些记住
也就是增删改查那些操作
先来看如何在队列中增加一个数据元素
也就是如何入对
那我们只能从对尾的方向让新元素入对好
那由于这个队列是用静态数组实现的
它的容量有限
所以当我们在插入之前
是不是需要先判断一下
这个队列是否已经存满
如果没有存满
才可以往队列当中放入新的数据元素
那如何判断队列已满
这个我们一会儿再来讨论
我们先来看一下如何插入数据元素
第一步
把这一次传入的参数x
也就是此次要插入的数据元素
把它放到队尾指针所指向的这个啊位置
也就是这第二步再把对尾指针加一
也就是往后移一位
这就完成了一次简单的入队操作好
那再往后如果还按这样的逻辑啊
让其他的数据元素依次入队的话
那么瑞尔指针会依次往后移
当整个静态数组都被存满之后
rear指针应该是十这样的值好
那我们是不是可以认为
当对为指针的值等于这个max size的时候
就认为队列它是满的好
来看一下
如果接下来依次让几个对头元素出对的话
那么我们的对头指针会依次往后移
同时这些静态数组
前面这些区域是不是就已经空前了
所以接下来如果还有新元素要入队
那我们可以把它插入到前面这些位置
因此当real也就是对尾指针等于max size的时候
其实这个队列并没有存满
那怎么让rear指针重新指挥到这儿呢
其实很简单
我们只需要用一个取余的操作
就可以完成这个事情
来看一下
假设此时对尾指针的值是九
也就是指向了这个数组的最后一个位置
接下来新元素应该是插入到队
尾指针所指的这个位置
再往后的一句代码
会让rear指针的值等于real加一
再对max size取模
也就是9+1
在对十取模
这个运算的结果是零
因此执行完第二句之后
那刚才我们设计的这种判断
对满和对空的方式
其实是以牺牲了一片存储空间作为代价的
其实自己写代码做自己的项目的话
使用这种方式就已经完全ok了
但是我们的出题老师不会这么想
有的题目会要求我们说
不能浪费这一片存储空间
但是之前我们又说了
如果继续往这存入数据元素
同时让这个对尾指针往后移位的话
那么对满和对空所表现出来的状态
其实就是一样的
我们就没办法用代码逻辑来判断
这个队列到底是满的还是空的
那怎么解决这个问题呢
其实可以在我们的队列结构当中
定义一个变量size
这个变量来记录队列当中
此时存放了几个数据元素
也就是说刚开始我们要把size的值设为零
之后
每一个新元素入队成功
队列的链式实现
链式实现
何用链式存储的方式来实现队列
那其实我们学过单链表之后
再来学习这个小节是十分轻松的
因为队列和单链表相比
无非就是进行插入和删除操作的时候啊
它只能分别在对位和对头进行
而单链表的插入和删除
是可以在任何一个位置进行的
所以队列其实是单链表的一个阉割版
那和单链表类似
实现队列的时候
我们也可以实现带头节点的版本和
不带头节点的版本好
下面的这个struct
它里边其实就是包含了两个指针
队列的头指针和尾指针
不知道大家还记不记得
我们在学习单链表的时候
我们要标记
就可以了一个单链表
其实只需要保存这个单链表的头指针l
如果只有一个指针的话
因为后续的节点其实都可以
根据这个头指针依次往后找到
但是如果我们只标记一个
头结点的指针的话
那么当我们想要在这个单链表的尾部
进行插入操作的时候
我们只能从这头节点依次往后
找到最后一个节点
然后再进行插入操作
这样的时间复杂度应该是on这个数量级
好处
在队列这种数据结构当中
由于我们每次插入元素
都只能在表尾的这个位置插入
因此我们可以就是像这个地方这样
用一个专门的尾指针指向最后一个节点
这样的话再进行插入操作的时候
就不需要从头往后寻找
带头节点
如果要给这个队列进行出队的操作
是不是只需要从这个对头节点
然后找到第一个数据节点
把这个节点给删除就可以了
因此当我们在声明一个队列的时候
既需要保存它对手的指针
也需要保存它对尾的指针好
和单链表类似
我们也可以实现不带头节点的这种队列
那这两种实现方式
这个对头指针的指向是不一样的
那像这种用链式存储实现的队列
可以把它称为链队列好
接下来看怎么初始化
首先看带头节点的版本
首先第一句代码其实就是
声明了队列相对应的这样的一个结构体
它里边包含两个指针
然后接下来对这些队列执行初始化操作
这句代码my lock申请一个头结点
并且让front和rear
这两个指针同时指向头节点
然后让这个头节点的next指针指向n
也就是这样
所以对于带头节点的队列
要判断它是否为空
我们可以根据front和rear
是否指向同一个节点来进行判断
当然我们是不是也可以判断
这个头节点的next指针是否指向null
个头节点的next指针是否指向null
那如果满足这个条件的话
那么队列也是空的好
那这是带头节点的情况
如果不带头节点的话
那我们初始化的时候
需要让这个real和front都指向now
所以判断队列是否为空
就只需要confront指针是否等于那就可以
当然也可以判断real指针是否等于
那好
那在初始化结束之后就可以执行入队
也就是插入操作
首先来看带头节点的这种队列
怎么实现一个新的数据元素要入队
那么这个数据元素肯定是
要被包含在一个节点当中
因此这句代码会用malloc申请一个新的节点
然后把数据元素x放到这个新节点当中
接下来由于队列的入队
或者说队列的这个插入操作
是在表尾的位置进行的
因此新插入的这个节点
肯定是队列当中的最后一个节点
所以我们需要把新节点的next指针域
把它设为n好
接下来
由于rear指针指向的是当前的标本节点
而我们新插入的这个新节点
应该连到当前的这个表尾节点之后
所以我们需要把rear指向的这个节点
它的指针域让它指向新节点s
最后还需要让表尾指针指向
这个新的表尾节点
好
再往后如果有第二个节点要入队的话
那同样用这段代码逻辑就可以处理
也就是说对于带头节点的这种队列
当我们在插入第一个元素
和后续的元素的时候
其实使用的这些处理的代码
逻辑都是一样的
而如果不带头节点的话
在插入第一个数据元素
或者说第一个数据元素入队的时候
就需要进行特殊的处理
因为刚开始rear和front
它们都是指向当的
所以在插入第一个数据元素的时候
需要对这两个指针都进行修改
首先是要申请一个新的节点
然后往这个节点中写入数据元素x
那由于每一次入队的这个新节点
s都是队列当中的最后一个节点
因此需要让它的next指针域指向n好
接下来需要进行一个判断
此时如果这个队列为空的话
那么就意味着这个节点
是队列当中的第一个节点
此时需要修改front和rear指向
让他们都指向这个第一个节点
如果接下来还有其他的元素要入队的话
那么其实就是对rear指针指向的节点
进行一个后插操作
当然了
不要忘记修改rear指针的指向
每一次插入新节点之后
都需要让rear指针指向新的标为节点
那这是入队操作
接下来看出对操作
先来看带头节点的情况
第一步首先要判断这个队列是否为空
如果没空的话
返回一个false
表示出对操作失败
接下来让p指针指向
这次要删除的这个节点
对于带头节点的队列来说
就是要删除这个头节点的后面一个节点好
接下来用变量x
把此次要删除的数据元素给带回去
所以这个地方变量x加了引用符号
再往后就是要修改这个头节点的后向指针
接下来需要注意
如果此次删除的节点p
它刚好就是当前的表尾节点
也就是说
这次删除的是队列当中的
最后一个节点的话
那么需要进行特殊的处理
像下面这种情况
p节点并不是队列当中的
最后一个数据节点
因此我们直接把p节点给释放掉就可以了
而如果此次出对的这个节点
p
它刚好就是当前队列当中的最后一个节点
这种情况下我们还需要修改表尾指针
让它指向这个头节点
让real和front指向同一个位置
意味着这个队列再一次变成了空队列的
这种状态
好对于不带头节点的队列
他的出对操作也是类似的
每次出对的是front指针指向的这个节点
那由于没有头节点
因此每一个对头元素出对之后
都需要修改这个front指针的指向
另外在最后一个节点出队之后
也需要把front和real都指向n
也就是把它们恢复成空对的这种状态
那这些不难理解
只需要细心一点
写代码肯定也没问题好
最后来看一下队列满的情况
我们在上小节当中学习了
用顺序存储方式实现了这种循环队列
那由于给这种循环队列分配的空间
都是提前预分配的
用静态数组的方式分配的
因此它的这个存储空间是有限的
不可拓展的
但是对于链式存储的队列
它的容量扩展是很方便的
只要内存资源还足够
那我们就可以继续给这个队列扩容
所以大家会发现对于顺序存储的方式
实现了这种队列
我们花了很大的篇幅来判断
就是这个队列是否已满
但是在链式存储的队列当中
我们其实可以不用关心这个问题
一般来说是不会满的
好的
那这个小节的内容十分简单
结合单链表示十分类似的
只不过我们再增加一个数据元素的时候
也就是入队操作
我们只能在队尾进行入队
而在删除一个数据元素的时候
也就是执行出对操作的时候
只能在对头进行
如果题目中要求大家实现一个队列的话
那首先是要关注它是不是要求你带头节点
还是不带头节点
另外当第一个元素入对
和最后一个元素出对的时候
也许需要一些比较特殊的处理
需要特别注意修改
rear指针
也就是对尾指针
那其他方面和单列表没有太大的区别
所以这部分的内容
大家在之后自己复习整理的时候
可以和单链表的内容进行对比
学习好
那最后来思考这样一个问题
如果你要统计这个队列它有多长的话
队列的长度你要怎么计算呢
是不是只能从对头这个节点开始
依次往后遍历
然后统计一下总共有多少个节点
显然需要用on的时间
那如果你的这个队列
它的这个队列长度
这个信息是你经常频繁的需要访问到的
那其实是不是
你可以在这加一个int型的变量
叫less
用来记录当前这个队列到底有多少个元素
是不是也可以好
所以就是大家在学习这些数据结构的时候
千万不要把它教条化
数据结构并不是让你考试用的东西
它是一种解决问题的工具
所以你的问题需要什么东西
其实你就可以因地制宜的
就是很灵活地添加一些你觉得需要的东西
不管是做题的时候还是未来开发的时候
大家都可以根据自己的需求来改造
这个数据结构
我们在课程里学习的只是一些
比较容易错的
比较核心的一些基本操作
不需要把思维局限在我们课程里
学的这些东西
好的
那以上就是这个小节的全部内容
反正我看不懂,
在链式存储中实现队列时,主要涉及以下几个关键点:
---
### **1. 队列结构定义**
队列由 **头指针(front)** 和 **尾指针(rear)** 构成,每个节点包含数据域和指向下一节点的指针。
```c
typedef struct LinkNode {
int data;
struct LinkNode *next;
} LinkNode;
typedef struct {
LinkNode *front, *rear;
} LinkQueue;
```
---
### **2. 初始化队列**
- **带头结点**:头指针和尾指针均指向头结点,头结点的`next`为空。
```c
void InitQueue(LinkQueue &Q) {
Q.front = Q.rear = (LinkNode *)malloc(sizeof(LinkNode));
Q.front->next = NULL;
}
```
- **不带头结点**:头尾指针初始化为`NULL`。
```c
void InitQueue(LinkQueue &Q) {
Q.front = Q.rear = NULL;
}
```
---
### **3. 判断队列空**
- **带头结点**:`front == rear`。
- **不带头结点**:`front == NULL`(或`rear == NULL`)。
---
### **4. 入队操作**
- **带头结点**:直接在尾部插入新节点,无需特殊处理第一个元素。
```c
bool EnQueue(LinkQueue &Q, int x) {
LinkNode *s = (LinkNode *)malloc(sizeof(LinkNode));
s->data = x;
s->next = NULL;
Q.rear->next = s; // 新节点链入尾部
Q.rear = s; // 更新尾指针
return true;
}
```
- **不带头结点**:需处理队列为空的情况。
```c
bool EnQueue(LinkQueue &Q, int x) {
LinkNode *s = (LinkNode *)malloc(sizeof(LinkNode));
s->data = x;
s->next = NULL;
if (Q.front == NULL) { // 队列为空
Q.front = Q.rear = s; // 头尾均指向新节点
} else {
Q.rear->next = s; // 链入尾部
Q.rear = s; // 更新尾指针
}
return true;
}
```
---
### **5. 出队操作**
- **带头结点**:删除头结点的下一节点,注意处理最后一个元素。
```c
bool DeQueue(LinkQueue &Q, int &x) {
if (Q.front == Q.rear) return false; // 空队
LinkNode *p = Q.front->next; // 待删除节点
x = p->data;
Q.front->next = p->next; // 修改头结点的next
if (Q.rear == p) { // 若删除的是尾节点
Q.rear = Q.front; // 队列置空
}
free(p);
return true;
}
```
- **不带头结点**:直接删除头节点,需处理队列变空的情况。
```c
bool DeQueue(LinkQueue &Q, int &x) {
if (Q.front == NULL) return false; // 空队
LinkNode *p = Q.front; // 待删除节点
x = p->data;
Q.front = p->next; // 更新头指针
if (Q.rear == p) { // 若删除最后一个节点
Q.rear = Q.front = NULL; // 队列置空
}
free(p);
return true;
}
```
---
### **6. 队列长度计算**
- 若频繁访问长度,可在结构体中添加`length`字段,入队时`length++`,出队时`length--`,时间复杂度为`O(1)`。
- 否则需遍历队列,时间复杂度为`O(n)`。
---
### **关键注意事项**
- **边界条件**:插入第一个元素或删除最后一个元素时,需正确调整头尾指针。
- **内存管理**:释放出队节点的内存,避免泄漏。
- **空指针判断**:操作前检查队列是否为空,避免访问非法内存。
通过对比带头结点和不带头结点的实现,可以更深入理解链式队列的指针操作逻辑。实际应用中,带头结点的实现通常更简洁,减少了边界条件的判断。
双端队列
分情况,一端插入和删除
各位同学大家好
在这个小节中
我们要学习
曾经在真题中考察过的一个概念
考点双端队列
那双端队列其实也是一种操作受限的
线性表
来看一下
我们之前学习的站是允许从一端插入
然后也只允许从这一端进行删除
它是这样的一种啊线性表
而队列是允许从一端插入
然后另一端进行删除
这个是它对插入和删除操作的一个限制
而双端队列是允许从两端插入
也允许从两端删除
它是这样的一种操作的限制
所以其实本质上这几种数据结构
它们都是插入和删除操作受限的线性表
只不过对插入和删除操作的这个限制
是不一样的
那如果我们对双端队列的插入和删除
只从一端进行
那双端队列是不是就退化成了战
所以只要站能够实现的功能
其实双端队列肯定可以实现好
那双端队列
它是可以从两边进行插入和删除
那由此还可以引申出另外的两种变种啊
分别是输入受限的双端队列
和输出受限的双端队列
那看这个图很容易看明白
输入受限的双端队列
就是我们只允许从一边进行插入
但删除操作可以从两边进行删除
而输出受限的话
就是啊删除操作只能在一边进行
但是插入可以从两边进行
那双端队列这个考点
他喜欢考察的
就是让大家判断一个输出序列是否合法
那这种类型的题目
是不是在占那个小节的课后练习当中
遇到过很多个
好来看一下
假设我们输入了1234
这样的四个数据元素
这样的输入顺序
有可能得到哪些合法的输出序列呢
那这四个元素如果进行排列组合的话
他们总共有可能会有24种
就是输出的顺序
我们来检查一下这些输出顺序
哪些是合法的
哪些是非法的
首先来看如果使用战的话
那么有可能产生哪些合法的输出序列
那为什么要讨论战呢
因为刚才我们说过双端队列
如果我们只从其中的某一端
进行输入和输出
那他就退化成了一个站对吧
所以只要站里边能够出现的
这种合法的输出序列
那在双端队列里边肯定也可以出现好
首先看一下
按照这样的顺序输入各个元素
那么1234这样的出战顺序
有没有可能实现呢
显然是可能的
如果我们先让一入站
然后再让他出战
二入站
紧接着出战
三入站
紧接着出战
四入站
紧接着出战
那这样的话输出的顺序刚好就是1234
所以这个输出序列是合法的
再来看下一个有没有可能输出2413呢
那如果第一个输出的元素是二
那么是不是就意味着一
这个元素肯定是在二之前就已经输入了
因为这是题目规定的输入顺序嘛
我们只能让一先入账
然后二再入站好
那接下来二出站之后
再往后是要输出四这个元素
那既然要输出四
接下来是不是还需要先把三给压入栈中
那三和四都入站之后
站里的元素情况就是这样的
接下来四处站
那再往后出战的元素肯定是三
而不可能是一对吧
所以2413这种输出序列肯定是非法的
他不可能用栈来实现好
再看下一个第一个输出的元素是三
那么按照刚才的思路
在三输出之前
肯定已经提前先压入了一和二这两个元素
好三出站之后
接下来要出战的是二
那么我们可以继续执行一个出战操作
再往后是四要出战
所以接下来我们可以让四先入站
然后再出战
然后最后还剩一个一出战
那这样的话就可以得到3241
这样的一个输出序列
这个序列是合法的
再来看最后一个例子
如果四这个元素是最先出战的
那么就意味着123这几个元素
肯定已经提前入站了
那四出站之后
这里边的这几个元素
它们的顺序是不可以改变的
接下来如果要继续出战
那只有可能按照321这样的顺序出战
因此4321这样的序列是合法的
那相应的
其他的这些序列是不是都不可能发生
因为刚才我们已经说了
剩下的几个元素
它们的顺序是不可以颠倒的好
那剩下的这些例子我们就不再一一举例
大家可以暂停来自己验证一下
这儿我们用绿色标出的是合法的出战序列
然后用红色标出的是不可能出现的
非法的啊
出战序列
对于这样的分析其实还是相对简单的
比如说再来看一个这个吧
呃3412
按照刚才我们说的那个逻辑
三这个元素他是最先出战的
那么在三出战之前
是不是意味着
站里边肯定已经有一二这两个元素了
然后接下来加入三
然后三再出战
对吧好
再往后无论你怎么操作一和二这两个元素
它们的出战顺序肯定都是二先出战
一在二的之后才可以出战
所以你看这个一在二之前
这个也是一在二之前
这个也是一在二之前
这些序列就都是非法的
那还记不记得我们之前提过一个东西
叫卡特兰数
用这样的算法
可以算出n个元素的输入序列
总共有可能出现多少种合法的出战序列
那我们这有四个元素
用卡特兰数算出来的合法出战序列
应该是14种
刚好和这是吻合的
那这我们只是列举了
有四个元素的这种例子
但是尽管只有四个元素
它可以产生的这些序列就已经有这么多种
所以大家在考试的时候
其实根本不需要担心说
它有没有可能让你列出
所有的合法出战序列
基本不可能考这样的题目
因为需要验证的情况太多了
而且这个难度本身不是很大
所以这种题目即便考察
也区分不出这个同学们之间的水平差距
不过卡特兰数的这个计算公式
还是可以记一下
不需要证明
但是大家要会使用好
那这我们枚举了所有的
在战当中有可能出现的合法输出序列
接下来看一下输入受限的双端队列
那刚才我们在战当中已经验证过的
这些合法的输出序列
在这里边肯定也是合法的
我们只需要使用这一边的插入和删除
就可以实现和这一模一样的功能
所以我们只需要验证在战当中
不合法的这些序列就可以了
那首先看一下1423这样的输出序列
有没有可能发生
那我们只能从这一端插入
首先插入一
那最先被输出的是一
所以一可以从右边
或者也可以从左边进行输出
接下来要输出的是四这个元素
那是不是和之前我们分析的一样
如果要输出四
那么就意味着四这个元素肯定需要输入
而输入四之前就需要先输入二和三
那由于这个双端队列只能从一端进行输入
因此234在这个队列当中的呃
排列顺序应该是这样的好
那接下来需要输出的是四
那我们可以从这边进行删除
所以四可以正常的输出
再往后是要输出二
那由于左边也可以删除
所以二也是可以正常输出的
那最后是三三可以从右边输出
也可以从左边输出
总之1423
这是一个合法的输出序列
好再看一个例子
3142
如果刚开始要输出的是三
这个元素
那么是不是就意味着123这几个元素
在三输出之前
肯定是已经输入到这个队列当中的
好
接下来三可以从右边进行输出
再往后是一可以从左边进行输出
再往后要输出四
那四此时还没有输入
可以从右边放进去好
接下来四可以从右边输出
二可以从左边输出
所以3142这样的序列也是合法的
再下一个例子
第一个要输出的是四
那么是不是就意味着1234这些元素
在四输出之前肯定就已经输入了好
接下来第一个输出四四可以从右边出去
再往后是不是可以先输出一或者先输出三
但是不可能先输出二对吧
所以四后面跟着二
那这两个序列肯定就是非法的
因为二倍加在一和三的中间
而我们只能从两边进行输出
或者说进行删除
所以这两个序列就是非法的好
那对于输入受限的双端队列来说
只有这两个序列是不可能产生的
然后其他的这些序列都是合法的
其中我们画了下划线的
这些序列是在站里边非法
但是在这边合法的输出序列好
最后我们再看一下输出受限的双端队列
同样的
我们只需要验证在站里边非法的这些序列
首先来看1423有没有可能出现
第一个要输出的是一
那么一可以先输入
然后紧接着输出
这个没问题
接下来要输出的是423这样的一个序列
在输出四之前
肯定需要先把二和三都放到这个队列里边
那注意由于输出受限的双端队列
它只能从这边输出
所以如果接下来我们要输出
423这样的顺序的话
那么这几个元素它在队列当中的排列
肯定是423这样的排列
因为它们只能从右边出去
所以接下来的分析方法就是看一下
我们能不能从这两边交替的进行插入
然后搞出这样的一个顺序
那接下来是二要入队对吧好
再往后是三要入队
如果我们要让三排在二的左边的话
那么我们可以从这边进行插入
再往后是四进行插入
那这样的话在输出的时候
是不是就可以得到
我们刚才所期待的这个423输出序列好
所以这个序列是合法的好下一个例子
首先第一个要输出的是三
那么在三输出之前
是不是123都得先入队
也就意味着在三这个元素出对之前
其实123这几个元素
它们在队列当中的相对位置
肯定就需要先确定下来
那怎么排列呢
看一下在三之后
一是先输出
然后接下来是二再输出
那刚才我们说了
这个队列它只能从右边出去
所以在三之后
如果是一先输出
二紧随其后的话
那么我们在进行123
这几个元素的输入的时候
肯定需要把二放在一的左边对吧
所以我们可以先插入一
然后从左边插入二
这样的话二就在一的左边对吧
好
再往后是三要出对
那么三要出对
是不是
就意味着三肯定得在最右边才可以出对
所以三是需要从右边插入
然后再从右边删除
接下来一可以出对
再往后是四要出对
那我们可以从右边插入四
然后再从右边删除四
最后是二出对
所以这样的一个序列它也是合法的
好来看最后一个例子
第一个要输出的是四
那么是不是就意味着在四输出之前
123需要先放到这个队列里边
并且它们之间的这个顺序
是需要提前在队列里边确定好的
由于只能从右边输出
所以如果要输出4132的话
是不是意味着这些元素在呃队列里边
应该是4132
按照这样的次序来排列对吧
那来看一下我们交替着从两边插入
能否插出这样的顺序呢
首先插入一
接下来要插入的是二对吧
那么二这个元素无论是从左边插入
还是从右边插入
它肯定是和一相邻的
也就是说我们这个地方假设的这种序列
就是二和一之间还有一个三
这种序列是查不出来的
所以这种输出序列是不可能得到的
同样的道理
4231是不是也一样
如果要先输出四
然后输出231
那么呃一这个元素先入对
接下来二入队的时候
肯定也是需要和一相邻对吧
和刚才是一样的
所以这个序列也是非法的好
所以在输出受限的双端队列里边啊
有这样的两个非法的输出序列
然后其他的都是合法的
同样的这儿有下划线的
这些序列是站里边不合法
但是这边合法的输出序列
那这种题目一般是在选择题当中
给你几个序列
然后验证它们是否合法
那不知道大家有没有发现
我们在对这些序列进行验证的时候
其实很重要的一点
就是如果你在输出序列当中
看到某一个序号的元素
那么在这个元素输出之前
意味着他之前的所有的那些元素
肯定都已经输入到这个队列里边了
这六是三这个元素先输出
在三输出之前是不是一和二的呃
相对次序肯定已经确定了
所以我们可以看它这个呃
给的这个序列当中
一和二他们到底是怎么排列的
在这种输出受限的队列当中
由于只能从右边出
所以如果接下来他给你的这个输出序列
是一在二之前
那么肯定是一要在右
然后二要在左对吧
如果是二在一之前
那么就是二在右
一在左
接下来你要验证的就是
怎么用这两边的插入操作
拼凑出你期待的这样的一个顺序
而对于输入受限的这种队列
由于它只能从一端进行输入
所以在序号较大的这个元素出
对之前其他的序号较小的那些元素
是不是已经可以确定
他们在这个队列里边的相对位置
所以接下来就只需要验证
能不能根据左右两边的删除操作
来拼凑出后续的这些输出序列
好的那这一小节中我们学习了双端队列
还有啊输入受限和输出受限的双端队列
并且对考试当中
常考的这种题型进行了练习
对于输出序列的合法性判断
其实战士啊一般不太容易出错的
但是双端队列的判断就需要细心一些
这个大家也需要自己在进行一些练习
找到那种做题的感觉
那大家可以自己尝试一下
如果有12345
有五个元素呃
依次输入
那你可以把他们的这个排列顺序打乱一下
看一下有没有可能产生相应的这种输出序
列
来训练自己分析的逻辑
那双端队列是队列的一个变种
我们之前还学过一个站的变种
就是共享战
共享战
可能也是大家比较容易遗忘的一个点
那这也提醒大家回忆一下好
那以上就是这个小节的全部内容
解析问的放在这里
以下是整理后的CSDN文章格式(含解析):
---
# 数据结构考点精讲 | 双端队列全解析,真题解题技巧一网打尽!
**摘要**:本文深度解析双端队列核心考点,通过真题案例详解输出序列合法性判断技巧,附带卡特兰数公式与变种队列对比分析。备考数据结构必看干货!
---
## 一、核心概念解析
### 1.1 双端队列定义
```markdown
- 本质:操作受限的线性表
- 特性:
▶ 两端均可进行插入(enqueue)和删除(dequeue)
▶ 特殊退化形态:
• 单端操作 → 退化为栈(LIFO)
• 一端入另一段出 → 队列(FIFO)
```
### 1.2 双端队列变种
| 类型 | 插入限制 | 删除限制 | 示意图 |
|---------------------|---------------|---------------|------------------|
| 输入受限双端队列 | 仅限一端 | 两端均可 | ←插入← ▢ →删除→ |
| 输出受限双端队列 | 两端均可 | 仅限一端 | ←插入→ ▢ ←删除← |
---
## 二、真题实战:输出序列合法性判断
### 2.1 基础案例(输入序列:1-2-3-4)
```python
# 合法序列示例
合法栈序列 = ["1234", "3241", "4321"] # 卡特兰数计算14种合法序列
非法栈序列 = ["2413", "3412"] # 违反后进先出原则
# 双端队列扩展验证
输入受限合法 = ["1423", "3142"] # 突破栈限制的合法序列
输出受限合法 = ["1423", "3142"] # 特殊操作组合实现的序列
```
### 2.2 判断方法论
**黄金法则**:输出元素X时,必须满足:
1. X之前输入的元素已全部入结构
2. 元素排列符合操作限制(通过两端操作可重构目标序列)
**验证步骤**:
1. 标记当前待输出元素位置
2. 模拟插入/删除操作路径
3. 检查能否通过受限操作达成目标序列
---
## 三、高阶考点突破
### 3.1 卡特兰数公式
```math
C_n = \frac{1}{n+1}\binom{2n}{n} = \frac{(2n)!}{(n+1)!n!}
```
- n个元素的合法出栈序列总数
- 当n=4时,C₄=14种合法序列
### 3.2 对比记忆表
| 结构 | 插入端 | 删除端 | 典型应用场景 |
|------------------|--------|--------|--------------------|
| 栈 | 1 | 1 | 函数调用栈 |
| 队列 | 1 | 1 | 消息队列 |
| 双端队列 | 2 | 2 | 撤销重做功能 |
| 输入受限双端队列 | 1 | 2 | 特定调度算法 |
| 输出受限双端队列 | 2 | 1 | 数据缓冲机制 |
---
## 四、真题演练场
**题目**:输入序列为ABCDE,判断下列输出序列的合法性:
1. EDCBA(栈/双端队列)
2. ACBED(输入受限双端队列)
3. BAEDC(输出受限双端队列)
**参考答案**:
1. ✅ 合法(栈的逆序输出)
2. ❌ 非法(B出现在C之后违反输入受限操作规则)
3. ✅ 合法(通过交替插入实现)
---
## 五、知识扩展:共享栈
```markdown
- 设计思想:两个栈共享同一存储空间
- 优势:提高内存利用率
- 关键指针:
▶ top1:栈1栈顶指针(正向增长)
▶ top2:栈2栈顶指针(逆向增长)
- 溢出条件:top1 + 1 = top2
```
---
**下期预告**:《栈与队列的梦幻联动:用队列实现栈的三种绝妙方案》 🚀
**互动环节**:在评论区留下你遇到的真题难题,点赞前3名的问题将获得详细解答!💡
---
**版权声明**:本文为博主原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。
**推荐阅读**:
[▶ 栈与队列:五分钟速通所有考点](https://blog.csdn.net/xxx)
[▶ 算法面试必备:TOP100数据结构真题精解](https://blog.csdn.net/xxx)
---
**。
栈在括号匹配中应用
、
各位同学大家好
在之前的小节中
那从这个小节开始
这个小节中我们来看一下括号匹配问题
这是占这种数据结构的一个经典应用
那什么是括号匹配问题呢
这我在我的id上写了一小段代码
大家会发现我们写的代码当中
这些小括号
大括号还有中括号
它们都是成双成对出现的
所以你看这个地方
如果我们只有一个左括号
而没有写与它对应的右括号
那这个i d e它是可以检测出来的
我们的编译器也会进行这样括号匹配的检查
因此这就是括号匹配的问题
我们必须保证我们在代码当中出现的这些括号
它们都是成双成对的出现的
除了左括号和右括号在数量上必须匹配之外
那形状上是不是也必须匹配
比如我这儿写的
是一个左边的小括号
而这如果写一个呃右边的中括号
那这样的代码也是有错误的好
那我们先用手算的方式捋一下这个括号匹配的过程
那这几个括号是不是很简单
大家可以下意识地看出中间的这两个是一对
然后这两个是一对
这两个是一对
最外侧的两个是一对
那这是我们人脑的反应
如果要用计算机来匹配这些括号的话
那只能一个一个依次扫描对吧
也就是从左到右扫描
那不难发现这样的一个规律
越往后出现的左括号会越先被匹配
比如当我们从左往右扫描扫描
到这个右括号的时候
寻找与他匹配的左括号
应该是找到这个对吧
而这个左括号是最后出现的左括号
那扫描到再往后的一个右括号的时候
下一个被匹配的就是这个组和号对吧
那算法的这种特性是不是就和占的后进先出其实是异曲同工的
我们是不是可以把这些左括号依次压入栈中
然后越往后压入站的左括号越先被弹出
战越先被匹配对吧
来看另一个例子
从左往右依次扫描这些括号
那前面几个都是左括号
当扫描到第一个右括号的时候
是不是需要检查最近出现的
一个左括号是哪一个好发现是这个与它匹配
那就可以继续往后扫描后一个还是有括号
那是不是同样的
我们依然是往前找
最近出现的
并且没有被匹配的括号是这个好
所以他们俩进行了配对好
那再往后的过程都是类似的
总之每当出现一个右括号的时候
就需要消耗一个左括号
那这的消耗其实是不是就对应了出站的一个操作
当我们遇到左括号的时候
就把它压入栈中
当我们遇到右括号的时候
就把栈顶的那个左括号给弹出
然后检查他们俩是否匹配
所以这就是用栈来
实现括号匹配的一个呃大体的思路
那我们结合动画看几个实际的例子啊
依次扫描这些括号
那第一个是碰到了一个左括号
需要把它压入栈底
接下来的两个还是左括号
依然是入站
然后再往后是遇到了一个右括号
此时需要弹出栈顶元素
然后和当前扫描到的右括号进行配对
发现他们俩都是小括号
因此他们是可以配对成功的
好继续往后扫描
依然是一个右括号
那同样的需要弹出此时栈顶的这个括号进行配对
再往后是一个左括号
需要压入栈中
然后之后是一个右括号于
是需要把栈顶的这个左括号弹出
然后检查他们俩是否匹配
如果匹配的话
那继续往后扫描
接下来还是一个右括号
因此就把此时这个栈顶的左括号弹出
检查它们是否匹配
显然这所有的括号都是可以两两配对的好再来看下一个例子
遇到左括号的时候
入站遇到右括号需要弹出
此时栈顶的左括号进行匹配的检查
发现能够匹配
那么继续往后扫描
接下来一个同样是右括号
因此需要弹出此时栈顶的左括号进行配对的检查
但是由于右括号是中括号
左括号是小括号
因此这一
对左右括号是匹配不上的
那再往后的这些括号是不是就不用扫描了
扫描到这儿我们就可以知道啊
这些括号肯定是匹配失败的
是非法的
再下一个例子
扫描到左括号就一次入站
扫描到右括号就出站
然后进行配对
好再往后扫描到的应该是这个右括号对吧
当我们遇到右括号的时候
需要消耗
但是此时这个站已经空了
所以是不是就说明并不存在和这个右括号相匹配的左括号
也就是出现了一个单身的右括号
那写代码的时候需要注意这种情况
扫描到右括号
但战已经空
那说明括号匹配失败
那后续的这些括号是不是就都不用检查了
再下一个例子
遇到左括号就把它压入栈中
然后遇到右括号的话
就把栈顶的左括号弹出进行匹配的检查
好扫描到最后一个优括号的时候弹出
此时栈顶的左括号
发现他们也是匹配的
但是当我们扫描完所有的括号之后
发现这个站里边还留有一个左括号对吧
那这是不是就说明并不存在和这个左括号相匹配的右括号
也就出现了单身的左括号
那这种情况也是不匹配的情况
因此写代码的时候
扫描王所有的括号
还需要检查这个栈是否为空
如果占飞空
那么就说明匹配失败好那接下来我们就来捋一捋这个算法的详细过程
我们需要从左往右扫描各个括号对吧好
那假设此时还有没有被处理的括号
并且我们把当前扫描到的括号记作a
那接下来是不是需要判断这个a他到底是左括号还是右括号
如果是左括号的话
就需要把它压到栈顶
然后再接着扫描下一个括号就可以好
如果这个a它不是左括号
而是右括号的话
那我们是不是需要弹出栈顶的左括号来检查是否匹配
而由于占有可能为空
所以需要进行一个占空的判断
如果这直接已经为空了
那么就说明啊当前扫描到的这个右括号
它是一个单身的状态
这就意味着配对失败对吧
就可以直接解除这个算法
而如果占不为空的话
那我们可以弹出栈顶的这个左括号
我们用b来表示
那接下来是不是需要判断b和a是否匹配
就是小的和小的匹配
中的和中的匹配
大的和大的匹配对吧
好
如果发现匹配成功的话
那是不是就可以接着往后处理下一个括号了
而如果匹配失败的话
同样的我们就可以结束这个算法
好用这样的方式是不是可以扫描处理完所有的这些括号好
所有的这些括号都扫描处理完了之后
我们需要判断栈是否为空
如果此时占非空的话
那么就说明有一些左括号是没有与他匹配的
右括号的
也就说明左括号单身
那这种情况下就是匹配失败
而如果占空的话
就说明所有的括号都是两两配对的啊
那此时就是匹配成功好
那如果用代码实现的话
就是这样的啊
这个函数里边传入了两个参数
这个参数是一个字符型的数组
里边就是存储了啊
什么左括号
右括号这一串的字符
然后第二个参数length是表示我的这个数组它的长度有多少好
另外我们这在这儿定义了一个顺序站
这个站里边它的数据元素是不是都是差类型的
然后还有一个栈顶指针top
考试的时候
大家是可以直接使用和数据结构相对应的这些基本操作的
但是个人建议是要简要地说明一下这些接口分别是什么
就是用这种注释的方式啊
把各个接口进行一个说明
那这样的话即便你的这个基本操作接口的名字
你命名的不太规范
但是只要你用中文把它注释讲明白了
那改卷老师肯定也不会算你的错
所以这是给大家的一个小建议
好回到算法本身
首先是定义一个站
并且初始化这个站
接下来用一个for循环
从左往右依次扫描这些字符
如果此次扫描到的字符它是某一种左括号的话
那么我们就把这个字符给他push
给他压入栈中
而如果此次扫描到的是右括号
那么就首先需要检查啊战是否为空
如果占为空的话
那么就说明右括号单身匹配失败
而如果占不空的话
我们就用啊pop也就是出战的操作
把栈顶元素弹出
然后用top m这个变量来存储
接下来是不是要检查此次扫描到的这个右括号
和当前栈顶的左括号是否匹配
如果此次扫描到的是右小括号
而栈顶的这个元素它不是左小括号的话
那么就说明配对失败
就可以直接return false算法就到此结束
那中括号和大括号也是一样的好
所以用这样的方式处理完所有的括号之后
是不是还需要检查最后这个站是否为空
如果占此时为空
那么就说明匹配成功
而如果最后这个站是非空的
也就是这个基本操作
它的返回值是false的话
那就说明有单身的左括号
这样也是匹配失败好那这个地方我们是用顺序站
也就是用一个静态数组来存储啊
站里边的各个数据元素
之前我们说过这种顺序占的容量是固定不变的
所以如果给的这个括号串它很长很长的话
那么就有可能出现占的溢出
因此在实际开发的过程中
如果要实现这个代码的话
那其实可以用恋战的方式来实现
不过在考试中
我们用顺序这样的这种方式实现
相对来说会更简单一些
所以考试的时候用这种方式也是没问题的
如果在这个
代码当中并不是直接定义了一个账
而是在里边定义了一个这样的数组
还有一个top指针
那么大家可以尝试着把这些基本操作给去掉
把相应的这些逻辑换成啊对数组
还有top指针直接的判断和操作好
特别是基础不太好的同学好
概括来讲算法思路就是啊从左到右依次扫描所有的字符
遇到左括号入栈
遇到右括号的话
弹出栈顶的左括号
检查是否匹配
如果所有的括号都检查完了
但是最终
战士飞空的话
那么说明有左括号单身
然后如果扫描到右括号
但是此时占已经空的话
那说明有右括号单身
而如果此时弹出的这个站顶左括号和当前的右括号不匹配的话
也会出现匹配失败的情况
其实只要理解这个算法的大体思想之后啊
具体的代码应该是大家能够在考场上临场把它写出来的东西好的
那以上就是这个小节的全部内容
栈的应用
波兰数学家的
几个表达式
左优先原则
难
右优先原则
要放弃了,太难了
表达式是计算机科学和数学中用于表示计算的一种方式,常见的有中缀表达式、后缀表达式和前缀表达式,以下是具体介绍: - **中缀表达式** - **定义**:运算符在两个操作数中间,是人们最熟悉的算术表达式形式。例如:`(1 + 1) * 3 - 2`。 - **特点**:符合人们的日常计算习惯,易于理解。但由于运算符的优先级和括号的存在,计算机在计算时需要先解析运算符的优先级和括号,以确定计算顺序,增加了计算的复杂性。 - **后缀表达式(逆波兰表达式)** - **定义**:把运算符放到两个操作数的后面,操作数的左右顺序不能颠倒。例如,中缀表达式`a + b`的后缀表达式为`a b +`。 - **特点**:后缀表达式中运算符的顺序与计算顺序一致,不需要考虑运算符优先级和括号,计算机可以按照从左到右的顺序依次扫描操作数和运算符,遇到运算符就对前面最近的两个操作数进行运算,计算过程更加简单直接,在计算机编程中应用广泛。 - **前缀表达式(波兰表达式)** - **定义**:把运算符放到两个操作数的前边。例如,中缀表达式`a + b`的前缀表达式为`+ a b`。 - **特点**:与后缀表达式类似,计算机计算时也不需要考虑运算符优先级和括号。不过,前缀表达式在扫描时是从右往左进行,遇到运算符就对后面最近的两个操作数进行运算。它在某些特定的编程语言或计算场景中会有应用,但相对后缀表达式来说,使用场景较少。 这三种表达式可以相互转换,将中缀表达式转换为后缀或前缀表达式时,需要先确定中缀表达式中各个运算符的运算顺序,然后按照相应的规则进行转换。在计算后缀或前缀表达式的值时,可以利用栈这种数据结构来辅助实现。
用栈表示后缀的时候
在计算机科学中,栈是一种具有“后进先出”特性的数据结构,在处理后缀表达式时非常有用。以下结合案例,详细解释用栈处理后缀表达式的运行过程。 1. **案例**:计算后缀表达式`3 4 + 2 * 1 -`的值。 2. **运行步骤** - 初始化一个空栈。 - 从左到右扫描后缀表达式: - 扫描到`3`,`3`是操作数,将其压入栈中,此时栈内元素:`3`。 - 扫描到`4`,`4`是操作数,将其压入栈中,此时栈内元素:`3`,`4`。 - 扫描到`+`,`+`是运算符,从栈顶弹出两个元素,即`4`和`3`,执行加法运算`4 + 3 = 7`,然后将结果`7`压入栈中,此时栈内元素:`7`。 - 扫描到`2`,`2`是操作数,将其压入栈中,此时栈内元素:`7`,`2`。 - 扫描到`*`,`*`是运算符,从栈顶弹出两个元素,即`2`和`7`,执行乘法运算`2 * 7 = 14`,然后将结果`14`压入栈中,此时栈内元素:`14`。 - 扫描到`1`,`1`是操作数,将其压入栈中,此时栈内元素:`14`,`1`。 - 扫描到`-`,`-`是运算符,从栈顶弹出两个元素,即`1`和`14`,执行减法运算`14 - 1 = 13`,然后将结果`13`压入栈中,此时栈内元素:`13`。 - 扫描结束,栈中仅剩一个元素`13`,这个元素就是后缀表达式`3 4 + 2 * 1 -`的计算结果。 通过这个案例可以看到,利用栈“后进先出”的特性,能够方便地处理后缀表达式。在扫描后缀表达式的过程中,遇到操作数就压入栈,遇到运算符就从栈顶弹出相应数量的操作数进行运算,并将运算结果重新压入栈,最终栈顶元素就是整个后缀表达式的计算结果。
下
当然,让我们通过一个具体的例子来理解中缀表达式转后缀表达式以及中缀表达式的求值过程。
假设我们有一个中缀表达式:A + B * (C - D) - E / F
中缀表达式转后缀表达式
我们按照上述步骤进行转换:
从左至右扫描中缀表达式。
遇到A,操作数,直接输出:A
遇到+,栈为空,直接入栈:+
遇到B,操作数,直接输出:A B
遇到*,栈顶为+,优先级高于+,入栈:+ *
遇到(,直接入栈:+ * (
遇到C,操作数,直接输出:A B C
遇到-,栈顶为(,直接入栈:+ * ( -
遇到D,操作数,直接输出:A B C D
遇到),依次弹出栈顶运算符并输出,直到左括号:A B C D -,弹出左括号,栈中剩余:+ *
遇到-,栈顶为*,优先级低于*,弹出*并输出:A B C D - *,然后将-入栈:+ -
遇到E,操作数,直接输出:A B C D - * E
遇到/,栈顶为-,优先级高于-,入栈:+ - /
遇到F,操作数,直接输出:A B C D - * E F
扫描结束,弹出栈中剩余的运算符:A B C D - * E F / - +
最终得到的后缀表达式为:A B C D - * E F / - +
中缀表达式的求值
使用两个栈进行求值:
扫描中缀表达式,操作数直接入操作数栈,运算符按照中缀转后缀的规则处理。
遇到A,入操作数栈:A
遇到+,入运算符栈
遇到B,入操作数栈:A B
遇到*,入运算符栈
遇到(,入运算符栈
遇到C,入操作数栈:A B C
遇到-,入运算符栈
遇到D,入操作数栈:A B C D
遇到),弹出-,计算C - D,结果入操作数栈:A B (C-D)
弹出*,计算B * (C-D),结果入操作数栈:A (B*(C-D))
遇到-,入运算符栈
遇到E,入操作数栈:A (B*(C-D)) E
遇到/,入运算符栈
遇到F,入操作数栈:A (B*(C-D)) E F
扫描结束,处理剩余运算符
每次弹出运算符时,从操作数栈弹出两个操作数进行计算,结果再压回操作数栈。
弹出/,计算E / F,结果入操作数栈:A (B*(C-D)) (E/F)
弹出-,计算(B*(C-D)) - (E/F),结果入操作数栈:A ((B*(C-D))-(E/F))
弹出+,计算A + ((B*(C-D))-(E/F)),结果入操作数栈
扫描结束后,操作数栈中的元素即为表达式的值。
最终操作数栈中的元素就是整个表达式的计算结果。
通过这个例子,我们可以看到中缀表达式转后缀表达式以及中缀表达式的求值过程是如何进行的。这个例子展示了如何使用栈来处理运算符的优先级和括号,以及如何计算表达式的值。
栈在递归中实验
各位同学大家好
在这一小节中我们会学习占这种数据结构在递归中的应用
那大家都学过c语言
应该都知道递归算法其实就是递归函数调用它自身的一个过程
所以递归的过程背后其实就是函数调用的过程
所以我们先来看一下普通的这种函数调用
它背后发生了一些什么事
来看一下这样的一个简单的程序
这一个卖行数定义了一些局部变量
然后执行前面这些代码
在这个地方调用了funk一这个函数
然后在这个函数执行到这一句的时候
又会调用funk 2这个函数
然后等funk 2执行结束之后
它又会返回funk 1
然后接着执行后面的这些代码
最后等funk一执行结束之后
又会返回啊main函数
然后执行后续的这些代码
所以大家会看到函数调用它有一个特性
最后被调用的函数是最先执行结束的
那这个特性是不是就是l i f o就和站的后进先出是异曲同工的
那事实上在我们的任何一段代码
任何一个程序运行之前
其实系统都会给我们开辟这样的一个函数调用站
用这个站来保存各个函数在调用过程当中所必须保存的一些信息
包含这样的三个方面
我们知道啊
我们的程序它的入口其实就是这个慢函数
所以刚开始运行main函数的时候
其实会把main函数相关的一些必要的信息给压入栈中
比如说麦函数里边的这些局部变量abc
它们的值就是存放在这个位置好
接下来my函数会调用funk一这个函数
也就是说接下来我们的程序要执行的是funk一所对应的这些代码
那等这些代码执行完了之后
接下来又应该执行哪句代码呢
显然是这一句对吧
但是这是我们肉眼看到的
那计算机解决这个问题的方式
就是它会在调用funk一这个函数的时候
把funk一执行结束之后
应该执行的这句代码的存储地址给他压到栈中
那除此之外
函数调用的这两个参数也会放到这个站里边
也就是a b这两个参数
所以为什么大家在函数调用的时候
如果在这个函数里边修改a的值或者b的值
那么它修改的其实是内存当中这两个ab的值
但是麦函数里边的ab这两个变量其实对应的是内存当中的这两份数据
因此在funk一里边修改ab的值
影响不到慢行数里边的ab值好
那除了被调用函数的实参之外
这个函数里面定义的局部变量x也是放在这个站里边的
注意啊
这个站其实就是内存里的某一片区域
某一片存储空间好
接下来funk一会调用funk 2
那同样的我们应该记录下来
funk 2执行结束之后
我们应该回到哪一句
继续往后执行
就把这一句代码的存放地址也给放到这个站里边
同时和刚才一样啊
这里边还需要存放实仓
还有局部变量这些信息好
接下来等funk 2执行结束之后
是不是就可以从这个站点的信息得知
再往后应该执行的是这一句代码对吧
因为这我们已经把下一句该执行的代码
它的地址给记录下来了
那这个函数执行完了之后
就可以把和它相关的这些信息给弹出站
也就是说释放了这一片内存空间
好接下来是不是就从这句代码继续往后执行
那再往后也是一样的
funk一执行结束了之后
又可以通过站点记录的信息知道再往后应该执行的是这句代码
所以我们就可以把funk一相应的这些信息给删除
然后接着从这句往后执行
所以这其实就是函数调用背后发生的一些事情
其实背后是需要用一个赞来支持的
那从我们的视角来看
程序是从麦函数开始的
但其实你的程序在经过编译之后
你的编译器其实会在main函数之前还给你加其他的一些代码
然后那些代码执行完了之后才会调用main函数
执行main函数相关的这些东西
所以你会看到我们这个地方在这个占地其实画了一些点点点
就是说在main之前
其实还需要把某一些我们不知道的信息压入占比
那科班出身的同学肯定用ide自己调试过程序
我这儿写了这样的三个简单的函数
然后在这一句代码这儿下了一个断点
那用调试模式运行这个程序的时候
它就会被卡住
然后这个时候你可以在你的ide里边看到
此时你的这个程序
它的函数调用栈是什么样的一个情况
那我这用的ide是c line啊
你用其他的id什么vs之类的都可以
肯定都有这种调试
看变量的功能
那可以看到当代码运行到这一句的时候
我的这个函数调用栈栈顶其实是funk 2
也就是这个函数相关的一些信息
这里边有参数x的值
也有局部变量m和n
那你也可以用鼠标点一下这个站里边的其他这些元素
比如说funk 1
你会看到这个funk一里边它包含ab这两个参数
ab
然后还有x这个局部变量
那main函数也是一样的
那除了main函数之外
其实在这个占比他还压住了其他的一些信息
只不过这些信息并不需要我们普通程序员关心好
总之大家用ide来debug的时候
其实是可以通过这样的方式来观察你的函数
调用栈里边保存了哪些东西
这些信息的
当然了
他这并没有显示返回地址好
那知道了函数调用背后的这些原理之后
我们再来看一下啊
这个小节要着重探讨的站在递归当中的应用
那我们的很多问题可以用递归算法来解决
而这类问题的特点是
我们可以把原始问题转换成属性相同
但是规模更小的问题
比如计算一个正整数的阶乘n的阶乘
我们是不是可以把它转换成n乘以n减一的阶乘
你看这样的话
我们的问题规模从n变成了n减一
它的规模变小了
同时属性是相同的
都是要计算阶乘
还有像斐波那契数列啊的求职
就是我们可以把问题规模n拆分成计算问题规模n减一和n减二
这样的两个部分也是一样的
有相同的特性
那实现一个递归算法需要两个比较重要的东西
一个是递归表达式
一个是边界条件
也就是递归的出口好
那我们在这儿要着重探讨的并不是如何设计一个递归算法
而是这个递归算法它背后和站有什么联系
那在这儿实现了一个呃计算阶乘的递归函数
传入了一个参数时
也就是要计算十的阶乘
然后这个地方啊阶乘函数又会递归的调用它自身
只不过问题规模会逐渐的收敛
逐渐的缩小
那既然涉及到函数调用
是不是和刚才我们说的那个过程一样
只要函数调用
其实背后就需要站的支持
而在递归调用的时候
这个函数调用占有的地方会把它称为递归工作站
但是本质上和我们刚才说的函数调用占比是同一个东西
也就是在这个站里边
你需要加入一些我们不知道是什么鬼的占比信息
然后和main函数相关的这些什么
就是它里面的局部变量啊
这些信息好
接下来main函数要调用这个函数对吧
然后传入的实参值是十
那在这个函数调用结束之后
他是不是应该接着执行192行
这句代码
就是需要把这个函数执行的结果把它赋给x这个变量好
那此时这个函数的实参n等于十
那由于n不等于零
也不等于一
所以它会执行这段代码
那这段代码又会递归的调用
它自身只不过n的值会减一
也就是变成九
那么在下一层调用结束之后
他是不是还得回来
接着执行187这句代码
因为需要把这个函数的返回值和n进行一个相乘
然后再return好
那接下来这个函数的实参是九
再往后是不是原理是类似的
我们就不再展开分析
那问题规模会逐渐收敛
直到n的值为一
也就是啊这个实参传入的是一的时候
由于满足这个条件
所以就可以直接return return 1
这也就意味着它的上一层函数的啊
这个运算得到的值就是被返回的一好
那最深层的这一次调用返回之后啊
按照我们之前所说的
它可以把与它相关的这些信息给弹出战
然后回到上一层继续执行187这句代码好
那经过检查
你会发现这个站里边记录了这一层的参数
n n的值是二
而刚才的return返回值是一对吧
所以这层函数的这个return值是不是就会return一个2x1
那往后是不是原理都是类似的
就可以逐层的返回
直到第一层的调用
第一层的调用里边n的值是十吗
那通过之前的那一系列的计算
这儿的这次调用返回值是不是应该是九的阶乘啊
那最后的这一层函数就可以计算它的n也就是十乘以这串东西
然后把这个值给return返回给main函数
然后接着执行main函数的192行代码
那这一行代码会把呃十的阶乘的值赋给变量x好
所以在递归调用
当中每当进入一层更深的递归的时候
就会把这一层递归调用所需要的信息压到栈顶
然后每执行完一层递归调用
就会从栈顶弹出相关的信息
那刚才我们这儿传入的参数是十
如果我们传入的参数大一点
比如说传入个10万
那么是不是就意味着这个站里边会被押入10万条信息啊
所以呃用递归算法它有一个缺点
就是如果我们的这个递归层数太多的话
那么有可能会导致占已出
因为我们的内存资源是有限的嘛
包括系统给我们开辟的这个函数调用站
它肯定也是有一个存储的上限的
那通过这个例子
大家就应该更能够理解我们在绪论里面谈到的啊
递归带来的空间复杂度升高
这个问题递归层数越多
那相应的空间复杂度也会越高
那同样的大家也可以在自己的i d e上啊下一个断点
然后观察一下
执行到某一句代码的时候
你的函数调用栈里边包含了哪些信息
那由于递归算法背后其实就是用一个栈来实现的
因此如果需要把递归算法改造成非递归的实现方式的话
那我们完全可以自己定义一个站
然后用类似的思想来实现它
那类似的题目
大家会在课后习题当中遇到
这儿我们只需要理解递归算法和我们的占有什么本质联系就可以了
好那再来看用递归算法实现的非布纳奇数列的求值
我们main函数里边传入的参数是四好
接下来由于这两个条件不满足
所以它会在调用f b3 和f i b2
那首先被调用的是fb 3
那这就会进入更深一层的递归
此时还没到达这个递归的边界条件
所以又会继续往更深层调用
会调用fb 2和fb 1
那首先被调用的是fb 2
那执行fb 2的时候又会递归的调用fb一和fb 0
首先被调用的是fb 1
那当这个n等于一的时候
由于满足这个边界条件
所以可以return
然后就可以弹出这个栈顶元素好
那接下来在fb 2这一层是不是还需要调用fb 0
那同样的到达边界条件之后就可以返回到此为止
是不是fib 2相关的这两个调用都已经有返回值了
所以fb 2也可以接着往上一层返回
其实刚才的过程就是呃四调用三
三调用二
然后二先调用一
一返回之后
二又调用零零
在返回之后二返回三
然后接下来是不是三还需要再调用一
然后一再返回之后
三再返回四对吧
就用这样的方式逐层的把这个递归调用给完成
还用这样的方式就计算出了f i b4 的值
那你会看到在这个计算的过程当中
f i b2 的值其实是被计算了两次
然后f i b e f i b0 同样的也是被重复了多次
所以用递归实现的算法啊
他也有可能在某些时候会包含很多次的重复运算
因此这也是递归算法不太高效的其中一个原因
好那这个小节其实是需要大致的了解函数调用栈它的一个原理
在我们看不见的背后
系统是如何通过一个站来帮你实现你的函数调用这个过程呢
好的那以上就是这个小节的全部内容
各位同学大家好
在这个小节中
我们会简单地介绍几个队列的应用
在之后的章节中
我们会学习一种数据结构
叫做数
那科班出身的同学肯定知道树是什么
而跨考的同学如果这个部分听不懂
没有关系
我们会在树的章节当中
还会更详细的讲解啊
这个地方要提到的这些内容
我们这儿只是简单快速的把它带一遍
那不难发现
数这种数据结构其实它是分层的
比如这是第一层
这是第二层
然后这是第三层
这是第四层
所以所谓的层次遍历
就是指你一层一层的来便利
这些数里边的各个节点
那要实现这种层次便利
就需要一个队列的辅助
那来看一下层次便利的大致思想
我们需要先新建一个队列
然后从这个根节点出发
按层次来遍历各个节点
那首先被便利的应该是根节点
也就是1号节点
在便利到1号节点的时候
我们就需要把这个节点的左右
两个孩子节点都放到队列的对位
那左孩子是2号
右孩子是3号好
那遍历完了1号节点之后
他就可以出
对我们就可以从队列中删除啊
1号节点好
那接下来应该检查的就是对头的这个节点
也就是2号节点
那同样的
我们也需要把2号节点的左右
两个孩子加入队列的对位
所以左孩子是四
右孩子是五
那我们就把它们都连到这队列里边好
那处理完2号节点之后
同样的2号节点就可以出对了
接下来需要处理的是3号节点
和刚才一样
3号节点的左孩子和右孩子
都需要加入队列
那左孩子是六
右孩子是七
所以我们把6号和7号节点
都放到队列的队尾
接下来3号孩子出队好
那接下来应该遍历到4号节点
但是由于4号节点没有左右孩子
所以不需要啊在队尾当中插入任何元素
就可以直接让4号节点出对
接下来处理的是5号节点
那5号节点的左右孩子分别是八和九
都需要把它连到队列的对位
那同样的删除5号节点
再往后是6号节点
那6号节点也没有左右孩子
那也可以让6号节点直接出对
最后是7号节点
那7号节点的左右孩子分别是十和11
这两个节点
处理完7号之后就可以让他出对
那之后的处理是不是都一样的
按层次便利的这种顺序
我们每处理一个节点的时候
都需要把这个节点的左右
孩子放到队列的对位
而我们每一次便利处理的
应该是对头的那个元素
所以我们用一个队列
就可以辅助的完成对数的层次便利好
再次提醒这个部分的内容
我们会在之后的章节当中进行
更详细的学习
也会结合代码来实现
刚才我们所说的这个过程
这地方能有个大致的印象就可以好
那除了对数的便利之外
其实对图这种数据结构的广度优先遍历
也需要队列这种数据结构的支持
同样的
这个部分的内容
会在图那个章节当中进行更详细的学习
这地方我们只是简单的说一下
那其实实现的思想和刚才数的便利
是很类似的
我们假设我们从这个1号节点出发
来
按广度优先的方式
便利这个图里边的各个节点
那首先就是需要新建一个队列
刚开始要遍历的是1号节点
那当我们遍历一个节点的时候
就需要检查和这个节点相邻的其他节点
有没有被遍历过
那现在2号和3号
这两个节点都没有被遍历过
所以可以把它们放到这个队列的对位
那和刚才类似
处理完1号节点之后就可以让他出对
接下来要处理的是2号节点
那类似的
我们要检查和2号相邻的节点中
有没有还没有被处理过的节点
那显然1号节点已经被处理过
所以不需要把1号节点放到队列的对位
而4号是没有处理过的
所以我们需要把4号放到他的对位
接下来2号就可以出队
再往后便利到3号节点
那同样的
和3号节点相邻的这些节点当中啊
1号已经被处理过
然后五六是没有被编辑过
没有被处理过的
所以把五六放到这个队列的对位
然后3号就便利结束
接下来要处理4号节点
显然和4号相邻的节点都已经被处理过
所以4号节点可以直接处对
接下来处理5号
那么和5号相邻并且没有被便利
没有被处理的应该是7号和8号节点
那同样的我们把它加入到这个队列的对位
那后续的便利中
就不会再加入任何一个节点
所以直到整个队列为空的时候
我们就完成了对这个图的广度优先遍历
同样的给大家的提示就是呃
这个部分的内容你听不懂没有关系
甚至你不知道广度优先遍历是什么
也没有关系
我们在图的章节当中还会更详细的学习好
再来看队列在操作系统中的应用
那我们的操作系统
需要管理系统当中的一些硬件资源
比如说cpu还有某些l设备之类的
但是这些系统资源往往都是有限的
而我们的系统当中
同时会有多个进程并发的运行
这些进程都会争抢的来使用这些系统资源
那操作系统是如何分配系统资源的呢
一个很常用的策略就是先来先服务的策略
就是哪个进程先申请系统资源
我就先把这个资源分配给那个进程
那先来先服务的思想
是不是和队列的先进先出是一样的
因此要实现先来先服务的这种管理策略
其实一般都是需要用一个队列来辅助完成
比如说我们的cpu资源其实是有限的
但是你的系统中
可能会同时启动了多个进程
那操作系统会让这些进程
排成一个所谓的就绪队列
然后每一次选择对头元素
让他上cpu执行一个比较短的时间片
然后迅速的下cpu
接下来让下一个进程上cpu运行
运行一小段时间之后
又让他下cpu
这样循环往复
那这样的话
你所有的这些进程
是不是都可以轮流地得到cpu的服务
所以为什么你的电脑只有一个cpu
但是可以有那么多的程序同时运行呢
这背后其实离不开
操作系统的一些管理策略的支持
而这种先来先服务的策略
通常都需要队列这种数据结构的支持
再来看下一个例子
那大家应该去学校打印店打印过东西
应该有这样的体会
就是学校打印店很多时候都是多台电脑
连同一台打印机的
也就是多个同学可以同时使用一台打印机
来打印他们想要的东西
那你会发现当打印机还在工作的时候
其实你就已经可以按下打印按钮了
这是因为系统开辟了一片缓冲区
用来存放
此时打印机暂时还不能处理的数据
比如说此时有一个同学
他在打印自己的论文
那这个论文很长
那当这个论文在打印的过程中
其他人其实就可以陆续地提交
自己的这个打印请求
系统会在背后把这些打印数据
按照先来后到的这样的原则
把它们组织成一个队列
然后接下来按照队列先进先出的原则
从这个队列中依次取出打印的数据
然后让打印机打印输出
所以在这个场景中用一个队列
把这些暂时不能被处理的任务给组织起来
这样的话就可以从一定程度上缓解
我们主机和打印机速度不匹配的问题
因为打印机是一个慢速设备
但是我们的电脑主机是很快速的设备
我们可以在短时间内发出大量的打印任务
而打印机又处理不了
怎么办呢
就把这些需要打印的数据放到缓冲区里
那比较合理的方式
就是可以用队列把这些数据给组织起来好
那这个小节就暂且只简单的介绍
这样的几个队列的应用
关于树的层次
遍历图的广度优先搜索
还有队列在操作系统当中的应用
大家会随着之后的学习有更深入的理解好
那以上就是这个小节的全部内容