微软手机做网站服务器吗,网站备案需要哪些材料,山东威海网站开发,php 开源 建站概述
浏览器的前进、后退功能#xff0c;你肯定很熟悉吧#xff1f;
当依次访问完一串页面 a-b-c 之后#xff0c;点击浏览器的后退按钮#xff0c;就可以查看之前浏览过的页面 b 和 a。当后退到页面 a#xff0c;点击前进按钮#xff0c;就可以重新查看页面 b 和 c。但…概述
浏览器的前进、后退功能你肯定很熟悉吧
当依次访问完一串页面 a-b-c 之后点击浏览器的后退按钮就可以查看之前浏览过的页面 b 和 a。当后退到页面 a点击前进按钮就可以重新查看页面 b 和 c。但是如果你后退到页面 b 后点击新的页面 d那就无法再通过前进、后退功能查看页面 c 了。
假设你是浏览器的开发工程师你会如何实现这个功能呢
这就要用到本章讲的 “栈” 这种数据结构了。 如何理解 “栈”
关于 “栈”有一个非常贴切的例子就是一摞叠在一起的盘子。我们平时放盘子时都是从下往上一个一个的放取的时候也是从上往下一个一个地依次取不能从中间任意抽出。后进者先出先进者后出这就是典型的 “栈” 结构。
从栈的操作特性上来看栈是一种 “操作受限” 的线性表只允许在一端插入和删除数据。
第一次接触这种数据类型时我对它存在的意义产生了很大的疑惑。因为我觉得相比数组和链表栈给我的只有限制并没有任何优势。那我直接使用数组或链表不就好了吗为什么还要用这个 “操作受限” 的 “栈” 呢
事实上从功能上来说数组或链表确实可以替代栈但你要知道特定的数据结构是对特定场景的抽象而且数组或链表暴露了太多的接口操作上的确灵活但使用时就比较不可控自然也就容易出错。
当某个数据集合只涉及在一端插入和删除数据并且满足后进先出、先进后出的特性这是应该首选 “栈” 这种数据结构。
如何实现一个 “栈”
从刚才栈的定义里我们可以看出栈主要包含两个操作入栈和出栈也就是在栈顶插入一个数据和从栈顶删除一个数据。理解了栈的定义后我们来看下如何用代码实现一个栈。
实际上栈既可以用数组来实现也可以用链表来实现。
用数组实现的栈我们叫做顺序栈。用链表实现的栈我们叫做链式栈。
这里实现一个基于数组的顺序栈。
这段代码使用 Java 来实现但不涉及任何高级语法并且用了中文做了详细的注释。
public class ArrayStack {private String[] items; // 数组private int count; // 栈中元素个数private int n; // 栈大小public ArrayStack(int n) {this.items new String[n];this.count 0;this.n n;}// 入栈操作public boolean push(String item) {if (count n) {// 数组空间不够了直接返回false入栈失败return false;}items[count] item;count;return true;}// 出栈操作public String pop() {if (count 0) {// 栈为空直接返回nullreturn null;}// 返回下标为count-1的数组元素并且栈中元素个数减1String temp items[count - 1];count--;return temp;}
}了解了定义和基本操作那它的操作时间、框架复杂度时多少呢
不管是顺序栈还是链式栈存储数据只需要一个大小为 n 的数组就够了。在入栈和出栈的过程中只需要一两个临时变量存储空间所以空间复杂度时 O ( 1 ) O(1) O(1)。
注意这里存储数据需要一个大小为 n 的数组并不是说空间复杂度就是 O ( n ) O(n) O(n)。因为这 n 个空间是必须的无法省掉。所以我们说空间复杂度的时候是除了原本的数据存储空间外算法运行还需要额外的存储空间。
框架复杂度分析是不是很简单时间复杂度也不难。不管是顺序栈还是链式栈入栈、出栈只涉及栈顶个人数据的操作所以时间复杂度都是 O ( 1 ) O(1) O(1)。
支持动态扩容的顺序栈
刚才那个基于数组实现的栈是一个固定大小的栈也就是说在初始化栈时需要实现制定栈的大小。当栈满之后就无法再往栈里添加数据了。尽管链式栈的大小不受限但要存储 next 指针内存消耗相对较多。那我们如何基于数组实现一个可以支持动态扩容的栈呢
还记得在数组那篇文章是如何来支持一个支持动态扩容的数组吗当数组空间不够时我们就重新申请一块更大的内存将原来数组中的数据统统拷贝过去。这样就实现了一个支持动态扩容的数组。
所以如果要实现一个支持动态扩容的栈我们只需要底层依赖一个支持动态扩容的数组就可以了。当栈满了之后我们就申请一个更大的数组将原来的数据搬移到新数组中。 实际上支持动态扩容的顺序栈我们平时开发中并不常用到。讲这个的目的主要还是希望带你练习一下前面将的复杂度分析方法。
你不用死记硬背入栈、出栈的时间复杂度你需要掌握的是分析方法。能够自己分析才算是真正掌握了。现在就带你一起分析一下支持动态扩容的顺序栈的入栈、出栈的时间复杂度。
对于出栈操作来说我们不会涉及内存的重新申请和数据搬移所以出栈的时间复杂度还是 O ( 1 ) O(1) O(1)。但是对于入栈来说当占用有空闲空间时入栈操作的时间复杂度是 O ( 1 ) O(1) O(1)。但当空间不够时就需要申请内存和数据搬移所以时间复杂度编程了 O ( n ) O(n) O(n)。
也就是说对于入栈操作最好情况时间复杂度是 O ( 1 ) O(1) O(1)最坏情况时间复杂度是 O ( n ) O(n) O(n)。那平均情况下的时间复杂度又是多少呢还记得我们在复杂度那篇文章中讲的摊还分析法吗这个入栈操作的平均时间复杂度可以用摊还分析法来分析。正好也借此再回顾一下摊还分析法。
为了分析的方便我们需要预先做一些假设和定义
栈空间不够时我们重新申请一个原来大小两倍的数组为了简化分析假设只有入栈操作没有出栈操作定义不涉及内存搬移的入栈操作为 simple-push时间复杂度为 O ( 1 ) O(1) O(1)。
如果当前栈大小为 k并且已满当再有新的数据要入栈时就需要重新申请 2 倍大小的内存并且做 k 个数据的搬移操作然后再入栈。
我们将 k 个数据的搬移操作均摊到前面 k 次的 simple-push 操作。均摊后每个入栈只需要一次 simple-push 操作和 一次搬移操作。也就是说均摊后入栈操作的均摊时间复杂度就为 O ( 1 ) O(1) O(1)。 通过这个例子的实战分析也印制了前面讲的均摊时间复杂度一般都等于最好情况时间复杂度。因为在大部分情况下入栈的操作时间复杂度都是 O ( 1 ) O(1) O(1)只有在个别时刻才会退化为 O ( n ) O(n) O(n)所以把好是多的入栈操作均摊到其他入栈操作上平均情况下的耗时就接近 O ( 1 ) O(1) O(1)。
栈在函数调用中的应用
接下来在看栈的另一个常见的应用场景编译器如何利用栈来实现表达式求值。
为了方便解释我们将算术表达式简化为只包含加减乘除四则运算比如3413*944-12/3。对于这个四则运算人脑可以很快求接触答案但是对于计算机来说理解这个表达式本身就是个挺难得事儿。如果换作你让你来实现这样一个表达式求值的功能你会怎么做
实际上编译器就是通过两个栈来实现的。其中一个是保存操作数的栈另一个是保存运算符的栈。我们从左向右遍历表达式
当遇到数字我们就直接压入操作数栈当遇到运算符就与运算符栈的栈顶元素进行比较。 如果运算符 比 运算符栈顶元素的优先级高就将当前的运算符压入栈如果运算符 比 运算符栈顶元素的优先级低或者相同从运算符中取栈顶运算符从操作数栈的栈顶取 2 个操作数然后进行计算再把计算的记过压入操作数栈继续比较。
我们将 35*8-6 这个表达式的计算过程画了一张图你可以 结合图来理解上面的计算过程。
栈在括号匹配中的应用
除了用栈来实现表达式求值还可以借助栈来检查表达式中的括号匹配。
假设表达式中只包含三种括号圆括号 ()、花括号 {} 和方括号 []并且它们可以任意嵌套。比如 {[]()[{}]} 或 [{()}([])] 等都为合法格式而 {[}()] 或 [({)] 问哦不合法格式。那我现在给你一个包含三种括号的表达式字符串如何检查它们是否合法呢
这里也可以使用栈来解决。用栈来保存未匹配的左括号从左到右一次扫描字符串。当扫描到左括号时将其压入栈中当扫描到有括号时从栈顶取出一个左括号。如果能够匹配比如 ( 跟 ) 匹配[ 跟 ] 匹配{ 跟 } 匹配则继续扫描剩下的字符串。如果扫描过程中遇到不能匹配的右括号或者栈中没有数据则说明为非法格式。
当所有的括号都扫描完成之后如果栈为空则说明字符串为合法字符串否则说明有未匹配的左括号为非法格式。
如何实现浏览器的前进、后退功能
其实用两个栈就可以完美解决。
我们使用两个栈X 和 Y我们把首次浏览的页面压入栈 X当点击后退按钮时再一次从栈 X 中出栈并将出栈的数据依次放入栈 Y。当我们点击前进按钮时依次从栈 Y 中取出数据放入栈 X。当 X 中没有数据时那就说明没有页面可以后退浏览了。当栈 Y 中没有数据那就说明没有页面可以点击前进按钮浏览了。
比如你顺序查看了 abc 三个页面我们依次把 abc 压入栈 X这个时候两个栈的数据就是这个样子的。 当你通过后退按钮从页面 c 退到页面 a 之后我们就一次把 c 和 b 从栈 X 中取出并依次放入栈 Y。这个时候数据就是这样的。 这个时候如果你又想看页面 b于是你点击了前进按钮回到页面 b我们就再把 b 从栈 Y 出栈放入栈 X。 这个时候你通过页面 b 跳转到新的页面 d页面 c 就无法再通过前进、后退按钮重复查看了所以需要清空栈 Y。 小结
栈是一种操作受限的数据结构只支持入栈和出栈操作。后劲先出是它最大的特点。栈既可以通过数组实现也可以通过链表来实现。不管基于数组还是链表入栈、出栈的时间复杂度都为 O ( 1 ) O(1) O(1)。此外还讲了一种支持动态扩容的顺序栈你需要掌握其均摊时间复杂度的分析方法。