栈:如何实现浏览器的前进和后退功能?

栈是什么?
想象是一摞叠在一起的盘子,在放盘子的时候,需要自下而上一个一个放,取盘子的时候需要自上而下一个一个取。
典型的栈结构:先进者后出,后进者先出,是一种操作受限的数据接口,只能在一端进行插入和删除操作。
 
栈主要包含两个操作,主要是入栈和出栈(插入和读取并删除)操作。
栈既可以用数组实现,也可以用链表实现,用数组实现的栈称为顺序栈,用链表实现的栈称为链式栈。
顺序栈代码:
// 基于数组实现的顺序栈
public class ArrayStack {
  private String[] items;  // 数组
  private int count;       // 栈中元素个数
  private int n;           // 栈的大小
 
  // 初始化数组,申请一个大小为 n 的数组空间
  public ArrayStack(int n) {
    this.items = new String[n];
    this.n = n;
    this.count = 0;
  }
 
  // 入栈操作
  public boolean push(String item) {
    // 数组空间不够了,直接返回 false,入栈失败。
    if (count == n) return false;
    // 将 item 放到下标为 count 的位置,并且 count 加一
    items[count] = item;
    ++count;
    return true;
  }
  
  // 出栈操作
  public String pop() {
    // 栈为空,则直接返回 null
    if (count == 0) return null;
    // 返回下标为 count-1 的数组元素,并且栈中元素个数 count 减一
    String tmp = items[count-1];
    --count;
    return tmp;
  }
}
时间复杂度是O(1)
空间复杂度也是O(1)
 
支持动态扩容的顺序栈:
之前我们实现顺序栈使用的底层结构是数组,不支持动态扩容,事实上只要将数组替换成一个可以动态扩容的数组就可以了,比如java中的list
分析一下动态扩容顺序栈的时间复杂度和空间复杂度:
出栈的时间复杂度依然是O(1),因为不涉及到数组扩容与数据搬迁工作
入栈时间复杂度,最好的情况下是O(1)(空间足够时),最坏的情况是O(n)(空间不足时,复制和搬迁数据)。
使用摊还分析法来分析时间复杂度 :
假设栈大小是k,在入栈过程中,当栈容量不足时,下一次入栈操作会触发一次内存申请操作,以及k个数据的搬迁操作,在之后的k-1次入栈操作,都不需要再重新申请内存以及搬迁数据,将搬迁数据均摊到入栈操作,可以得出入栈操作整体的时间复杂度接近O(1)
 
栈在函数中的应用
典型场景---函数调用栈
操作系统给每个线程都分配了一块独立的内存空间,这种内存空间被组织成栈这样的结构,用来存储函数调用时的临时变量。没进入一个函数,就会将临时变量作为一个栈帧入栈,调用函数完成,返回之后,将函数对应的栈帧出栈。
 
栈在表达式求值中的使用:
以一个简单运算为例:3+5*8-6=?
编译器通过两个栈来实现,一个 存储操作数的栈,另一个存储运算符的栈。从左向右遍历表达式,遇到数字压入操作数栈,遇到运算符,与运算符栈的栈顶数据比较。
如过优先级高于运算符栈栈顶元素,就将运算符入栈,如果低于,就将栈顶元素取出,并且从操作数栈中取出两个元素进行运算,将计算结果压入操作数栈。
 
栈在括号匹配中的应用:
用栈保存为匹配的左括号,从左到右一次扫描字符串,当扫描到左括号时,则将其压入栈中;当扫描到右括号时,从栈顶取出一个左括号,如果能匹配上,则继续扫描剩下的字符串。如果扫描过程中,遇到不能配对的右括号,或者栈中没有数据,则说明为非法格式。
 
如何实现浏览器的前进后退功能?
我们使用两个栈X和Y,我们把首次浏览的页面依次压如栈X,当点击后退按钮时,再依次从栈X中出栈,并将出栈的数据一次放入Y栈。当点击前进按钮时,我们依次从栈Y中取出数据,放入栈X中。当栈X中没有数据时,说明没有页面可以继续后退浏览了。当Y栈没有数据,那就说明没有页面可以点击前进浏览了。
 
思考:
1.我们在讲栈的应用时,讲到用函数调用栈来保存临时变量,为什么函数调用要用“栈”来保存临时变量呢?用其他数据结构不行吗?
其实,我们不一定非要用栈来保存临时变量,只不过如果这个函数调用符合后进先出的特性,用栈这种数据结构来实现,是最顺理成章的选择。
从调用函数进入被调用函数,对于数据来说,变化的是什么呢?是作用域。所以根本上,只要能保证每进入一个新的函数,都是一个新的作用域就可以。而要实现这个,用栈就非常方便。在进入被调用函数的时候,分配一段栈空间给这个函数的变量,在函数结束的时候,将栈顶复位,正好回到调用函数的作用域内。
 
2.我们都知道,JVM 内存管理中有个“堆栈”的概念。栈内存用来存储局部变量和方法调用,堆内存用来存储 Java 中的对象。那 JVM 里面的“栈”跟我们这里说的“栈”是不是一回事呢?如果不是,那它为什么又叫作“栈”呢?
内存中的堆栈和数据结构堆栈不是一个概念,可以说内存中的堆栈是真实存在的物理区,数据结构中的堆栈是抽象的数据存储结构。
代码区:存储方法体的二进制代码。高级调度(作业调度)、中级调度(内存调度)、低级调度(进程调度)控制代码区执行代码的切换。
静态数据区:存储全局变量、静态变量、常量,常量包括final修饰的常量和String常量。系统自动分配和回收。
栈区:存储运行方法的形参、局部变量、返回值。由系统自动分配和回收。
栈区:存储运行方法的形参、局部变量、返回值。由系统自动分配和回收。
 
posted @ 2019-08-17 14:48  星辰河流  阅读(1109)  评论(0编辑  收藏  举报