这是我参与「第五届青训营 」伴学笔记创作活动的第 16 天
栈与队列
栈
栈的定义
先进后出
比如浏览器的后撤健 Word里的撤销(undo)
允许插入和删除的一端称为栈顶 (top), 另一端称为栈底 (bottom), 不 含任何数据元素的栈称为空栈。栈又称为后进先出 (Last In First Out) 的线性表,简 称LIFO结构。
先它是一个线性表,也就是说,栈元素具有线性关系,即前驱后继关系。只不 过它是一种特殊的线性表而已。定义中说是在线性表的表尾进行插入和删除操作,
这里表尾是指栈顶,而不是栈底。
特殊之处就在于限制了这个线性表的插入和删除位置,它始终只在栈顶进 行。这也就使得:栈底是固定的,最先进栈的只能在栈底。
进栈出栈变化形式
问:这个最先进栈的元素,是不是就只能是最后出栈呢?
栈对线性表的插入和删除的位置进行了限制,并 没有对元素进出的时间进行限制,也就是说,在不是所有元素都进栈的情况下,事先 进
去的元素也可以出栈,只要保证是栈顶元素出栈就可以。
但没有312的出栈次序
栈的抽象数据类型
由于栈本身就是一个线性表,那么上一章我们讨论了线性表的顺序存储和链式存 储,对于栈来说,也是同样适用的。
栈的顺序存储结构及实现
栈的顺序存储结构(顺序栈)
线性表是用数组来实现的
下标为0的一端作为栈底比较好,因为首元素都存在栈底,变化最 小,所以让它作栈底。
若存储栈的长度为StackSize,则栈顶位置top必须小于StackSize
当栈存在一个元素时,top等于0,因此通常把空栈 的判定条件定为top等于一 1。
结构
现在有一个栈,StackSize是5,则栈普通情况、空栈和栈满的情况示意图
进栈操作
出栈操作
两栈共享空间
对于两个相 同类型的栈,我们却可以做到最大限度地利用其事先开辟的存储空间来进行操作。
关键思路:它们是在数组的两端,向中间靠拢。topi和top2是栈1和栈2 的栈顶指针,可以想象,只要它们俩不见面,两个栈就可以一
直使用。
栈1为空时,就是top1等于一 1时;而当top2等于 n时,即是栈2为空时,那什么时候栈满呢?
若栈2是空栈,栈1的top1等于n-1时,就是栈1满了。反 之,当栈1为空栈时,top2等于0时,为栈2满。
两个栈见面之时,也就是两个指针之间相差1时,即top1 + 1 == top2为栈 满。
结构代码
插入
对于两栈共享空间的push方法,我们除了要插入元素值参数外,还需要有一个判 断是栈1还是栈2的栈号参数stackNumbero
使用这样的数据结构,通常都是当两个栈的空间需求有相反关系时,也 就是一个栈增长时另一个栈在缩短的情况。
栈的链式存储结构及实现
栈的链式存储结构 (链栈)
栈只是栈顶来做插入和删除操作,栈顶放在链表的头部还是尾部呢?
由 于单链表有头指针,而栈顶指针也是必须的,那干吗不让它俩合二为一呢,所以比较 好的办法是把栈顶放在单链表的头部
通常对于链栈来说,是不需要头 结点的。
对于空栈来说,链表原定义是头指针指向空,那么链栈的空其实就是top=NULL
结构代码
进栈操作
出栈操作
总结
链栈的进栈push和出栈pop操作都很简单,没有任何循环操作,时间复杂度均 为 O(1)。
对比一下顺序栈与链栈,它们在时间复杂度上是一样的,均为0(1)。对于空间性 能,顺序栈需要事先确定一个固定的长度,可能会存在内
存空间浪费的问题,但它的 优势是存取时定位很方便,而链栈则要求每个元素都有指针域,这同时也增加了一些 内存开销,但对于栈的
长度无限制。
所以它们的区别和线性表中讨论的一样,如果栈 的使用过程中元素变化不可预料,有时很小,有时非常大,那么最好是用
链栈,反 之, 如果它的变化在可控范围内,建议使用顺序栈会更好一些。
栈的作用
引入简化了程序设计的问题,划分了不同关注层次,使得思考范围缩小,更 加聚焦于我们要解决的问题核心。反之,像数组等,因为要
分散精力去考虑数组的下 标增减等细节问题,反而掩盖了问题的本质。
栈的应用——递归
斐波那契数列实现
常规迭代
递归
对比了两种实现斐波那契的代码。迭代和递归的区别是:迭代使用的是循环结 构,递归使用的是选择结构。递归能使程序的结构更清晰、更简洁、更容易让人理 解,从而减少读懂代码的时间。但是大量的递归调用会建立函数的副本,会耗费大量 的时间和内存。迭代则不需要反复调用函数和占用额外的内存。因此我们应该视不同 情况选择不同的代码实现方式。
递归的定义
在高级语言中,调用自己和其他函数并没有本质的不同。我们把一个直接调用自 己或通过一系列的调用语句间接地调用自己的函数,称
做递归函数。
每个递归定义 必须至少有一个条件,满足时递归不再进行,即不再引用自身而是返回值退出。
递归过程退回的顺序 是它前行顺序的逆序。在退回过程中,可能要执行某些动作,包括恢复在前行过程中 存储起来的某些数据。
这种存储某些数据,并在后面又以存储的逆序恢复这些数据,以提供之后使用的 需求,显然很符合栈这样的数据结构,
栈的应用一四则运算表达式求值
后缀(逆波兰)表示法定义
后缀表示法
规则:从左到右遍历表达式的每个数字和符号,遇到是数字就进栈,遇到是符 号,就将处于栈顶两个数字出栈,进行运算,运算结果进
栈,一直到最终获得结果
中缀表达式转后缀表达式
总结
具体步骤看PDF
队
例子
你们在用电脑时有没有经历过,机器有时会处于疑似死机的状态,鼠标点什么似 乎都没用,双击任何快捷方式都不动弹。就当你失去耐心,打算reset时。突然它像 酒醒了一样,把你刚才点击的所有操作全部都按顺序执行了一遍。这其实是因为操作 系统中的多个程序因需要通过一个通道输出,而按先后次序排队等待造成的。
定义
队列是一种先进先出 (First In First Out) 的线性表,简称FIFO。
允许插入的一 端称为队尾,允许删除的一端称为队头。假设队列是q= ( a1, a2,……,an),那么 a1就是队头元素,而an是队尾元素。
这样我们就可以删除时,总是从a,开始,而插 入时,列在最后。
应用
队列在程序设计中用得非常频繁。前面我们已经举了两个例子,再比如用键盘进 行各种字母或数字的输入,到显示器上如记事本软件上
的输出,其实就是队列的典型 应用
队列的抽象数据类型
同样是线性表,队列也有类似线性表的各种操作,不同的就是插入数据只能在队 尾进行,删除数据只能在队头进行。
循环队列
队列顺序存储的不足
所谓的 入队列操作,其实就是在队尾追加一个元素,不需要移动任何元素,因此时间复杂度为O(1)
与栈不同的是,队列元素的出列是在队头,即下标为0的位置,那也就意味着, 队列中的所有元素都得向前移动,以保证队列的队头,也
就是下标为0的位置不为 空,此时时间复杂度为O(n)
队头不需 要一定在下标为0的位置
为了避免当只有一个元素时,队头和队尾重合使处理变得麻烦,所以引入两个指 针,front指针指向队头元素,rear指针指向队尾元素的
下一个位置,这样当front等 于rear时,此队列不是还剩一个元素,而是空队列。
循环队列定义
因数组末尾元素已经占用,再向后加,就会产生数组越界的错误,可实际上,我 们的队列在下标为0和1的地方还是空闲的。我们把这种现象叫做“假溢出"
所以解决假溢出的办法就是后面满了,就再从头开始,也就是头尾相接的循环。
我们把队列的这种头尾相接的顺序存储结构称为循环队列。
队列的最大尺寸为Queuesize
通用的计算队列长度公式为:
实现代码
求队列长度
入队列操作
出队列操作
队列的链式存储结构及实现(链队列)
队列的链式存储结构,其实就是线性表的单链表,只不过它只能尾进头出而已, 我们把它简称为链队列。
链队列结构
队列的链式存储结构——入队操作
队列的链式存储结构——出队操作
总的来说,在可以确定队列长度最夹值的情况下,建议用循环队列,如果你无法 预估队列的长度时,则用链队列。
总结
串
定义
串(string )是由零个或多个字符组成的有限序列,又名叫字符 串。
一般记为s = "a1a2……an";
s是串的名称 注意单引号不属于串的内容。 a(l<i< n) 可以是字母、数字或其他字符,
i就是该字符在串中的位置 串中的字符数目n称 为串的长度 ”有限”是指长度n是一个有限的数值。
零个字符的串称为 空串 (null string), 它的长度为零,可以直接用两双引号""""表示,也可以用希腊 字母"打不来"来表示。
所谓的序列,说明串的相邻字符之间具有前驱和后继的关系。
*空格串: *是只包含空格的串。注意它与空串的区别,空格串是有内容有长度的, 而且可以不止一个空格。
*子串与主串: *串中任意个数的连续字符组成的子序列称为该串的子串,相应地, 包含子串的串称为主串。
子串在主串中的位置就是子串的第一个字符在主串中的序号
串的比较
相等
不相等比较
串的抽象数据类型
操作Index的实现算法
串的存储结构
串的存储结构与线性表相同,分为两种:
- 串的顺序存储结构
- 串的链式存储结构
串的顺序存储结构
串的顺序存储结构是用一组地址连续的存储单元来存储串中的字符序列的。
按照 预定义的大小,为每个定义的串变量分配一个固定长度的存储区。一般是用定长数组 来定义。
比如在计算机中存在一个自由存储区,叫做“堆“ 这个堆可由C语言的 动态分配函数malloc ()和free ()来管理。
串的链式存储结构
对于串的链式存储结构,与线性表是相似的,但由于串结构的特殊性,结构中的 每个元素数据是一个字符,如果也简单的应用链表存储串值,一个结点对应一个字 符,就会存在很大的空间浪费。因此,一个结点可以存放一个字符,也可以考虑存放 多个字符,最后一个结点若是未被占满时,可以用或其他非串值字符补全,如 图5-5-3所示。
当然,这里一个结点存多少个字符才合适就变得很重要,这会直接影响着串处理 的效率,需要根据实际情况做出选择。
但串的链式存储结构除了在连接串与串操作时有一定方便之外,总的来说不如顺 序存储灵活,性能也不如顺序存储结构好。
朴素的模式匹配算法
串的定位操作通常称做串的模式匹配
从下面的主串S=“goodgoogle”中,找到T="google”这个子串的位置。
用数组实现算法Index
前面我们已经用串的其他操作实现了模式匹配的算法Index。现在考虑不用串的其 他操作,而是只用基本的数组来实现同样的算法。注意我们假设主串S和要匹配的子 串T的长度存在S[0]与T[0]中。实现代码如下:
KMP模式匹配算法
DEKnuth、J.H.Morris和V.R.Pratt (其 中Knuth和Pratt共同研究,Morris独立研究)发表一个模式匹配算法,可以大大避 免重复遍历的情
况,我们把它称之为克努特一莫里斯一普拉特算法,简称KMP算 法。
KMP模式匹配算法原理
我们可以得出规律,j值的多少取决 于当前字符之前的串的前后缀的相似度。
next数组值推导
- 情况一
- 情况二
- 情况三
- 情况四
KMP模式匹配算法实现
这段代码的目的就是为了计算出当前要匹配的串T的next数组
KMP算法仅当模式与主串之间存在许多“部分匹配”的情况下 才体现出它的优势,否则两者差异并不明显。
KMP模式匹配算法改进
KMP还是有缺陷的。比如,如果我们的主串S=”aaaabcde”,子串 T=”aaaaaax“其next数组值分别为012345,在开始时,当i=5、j=5时,我们发现 “b”与“a”不相等,如图5-7-6的①,因lit j=next[5]=4,如图中的②,此时“b”与 第4位置的“a"依然不等,j=next[4]=3,如图中的③,后依次是④⑤,直到 j=next[l]=0时,根据算法,此时if J++,得到i=6、j=l,如图中的⑥
中的②③④⑤步骤,其实是多余的判断。由于T串的第二、三、 四、五位置的字符都与首位的“a”相等,那么可以用首位next[l]的值去取代与它相 等的字符后续next[j]的值,
假设取代的数组为nextval,增加了加粗部分,代码如下:
nextval数组值推导
总结
改进过的KMP算法,它是在计算出next值的同时,如果a位字符与它next 值指向的b位字符相等,则该a位的nextval就指向b位的
nextval值,如果不等,则 该a位的nextval值就是它自己a位的next的值。