8.栈实现浏览器的前进后退

简介: 8.栈实现浏览器的前进后退

栈实现浏览器的前进后退

当你一次访问 1、2、3 页面之后,点击浏览器的后退按钮就可以返回到 2 和 1.当后退到 1,点击前进按钮还可以继续查看页面 2、3。但是当你退到 2 页面,点击了新的页面 4,那就无法继续通过前进、后退查看页面 3 了。

「我们如何实现这个功能呢?」

什么是栈

「栈」我们都知道 Java 虚拟机 JVM 就有『本地方法栈』『虚拟机栈』的划分,每个方法执行的时候都会创建一个栈帧用于存放局部变量表、操作数栈、动态链接、方法出口信息。

每一个方法从调用到结束,就对应着一个栈帧在「虚拟机栈」的入栈与出栈的过程。这里其实就是运用了「栈」数据结构的特性:「后进先出、先进后出」。就像一摞叠在一起的盘子,入栈就像我们放盘子,出栈就是我们从上往下一个个取。

「栈是一种「操作受限」的线性表」,只允许在一端插入和删除数据。

是不是觉得这种数据结构有何意义,只有受限的操作,相比「数组」和「链表」感觉没有任何优势。为何还要用这个「操作受限」的「栈」呢?

特定的数据结构用在特定的场景,数组与链表暴露太多的操作接口,操作灵活带来的就是不可控,也就更加容易出错。

「当某个数据集合只涉及在一端插入和删除数据,并且满足后进先出、先进后出的特性,我们就应该首选“栈”这种数据结构」

比如我们的 JVM 栈结构,方法调用则是对应的入栈与出栈。

栈的实现

核心操作就是「入栈」「出栈」,也就是在栈顶插入元素、从栈顶取出元素。

理解了两个核心操作后,我们可以使用数组或者链表来实现。

  • 数组实现的栈,叫做 「顺序栈」
  • 用链表实现,叫做 「链式栈」

这里我通过数组实现一个顺序栈,可用于实际开发中,我拓展了「清空栈」、「拓容」、「构建默认大小与最大限制」。代码我放在 GitHub https://github.com/UniqueDong/algorithms.git上,自己撸一遍,再对比下是否写的正确。

这里不仅仅作为一个 示例,我的例子还考虑了栈默认初始大小以及最大限制,当超过默认大小但是还没有达到最大限制的时候,还需要扩容操作。

import java.util.Arrays;
/**
 * 基于数组实现的栈
 * @param <T>
 */
public class ArrayStack<T> {
    /**
     * 默认大小
     */
    public static final int DEFAULT_SIZE = 128;
    /**
     * 默认最大限制,-1 表示无限制
     */
    private static final int DEFAULT_LIMIT = -1;
    /**
     * 初始化栈大小
     */
    private int size;
    /**
     * 栈最大限制数,-1 表示无限制
     */
    private final int limit;
    /**
     * 指向栈顶元素的下标,默认没有数据 -1
     */
    private int index;
    /**
     * 保存数据
     */
    private Object[] stack;
    /**
     * 默认构造方法,创建一个 128 大小,无限制数量的栈
     */
    public ArrayStack() {
        this(DEFAULT_SIZE, DEFAULT_LIMIT);
    }
    // 指定大小与最大限制的栈
    public ArrayStack(int size, int limit) {
        this.index = -1;
        if (limit > DEFAULT_LIMIT && size > limit) {
            this.size = limit;
        } else {
            this.size = size;
        }
        this.limit = limit;
        this.stack = new Object[size];
    }
    /**
     * push obj to stack of top
     *
     * @param obj push data
     * @return true if push success
     */
    public boolean push(T obj) {
        index++;
        // 当下标达到 size,说明需要拓容了。判断是否需要拓容
        if (index == size) {
            // 若超过限制则返回 false,否则执行拓容
            if (limit != DEFAULT_LIMIT && size >= limit) {
                index--;
                return false;
            } else {
                // 拓容
                expand();
            }
        }
        stack[index] = obj;
        return true;
    }
    /**
     * pop stack of top element
     *
     * @return top of stack element
     */
    public T pop() {
        if (index == -1) {
            return null;
        }
        T result = (T) stack[this.index];
        stack[index--] = null;
        return result;
    }
    /**
     * 清空栈数据
     */
    public void clear() {
        // 判断是否空
        if (index > -1) {
            // 只需要将 index + 1 个元素设置 null,不需要遍历 size
            for (int i = 0; i < index + 1; i++) {
                stack[i] = null;
            }
        }
        index = -1;
    }
    public int size() {
        return this.index + 1;
    }
    @Override
    public String toString() {
        return "ArrayStack{" +
                "size=" + size +
                ", limit=" + limit +
                ", index=" + index +
                ", stack=" + Arrays.toString(stack) +
                '}';
    }
}

我们来分析下「栈」的空间复杂度与实践复杂度。

在「入栈」「出栈」的操作中,存储数据都是只需要一个 最大限制 n 的数组,所以空间复杂度是 O(1)。

存储数据为 n 大小的数组,不是说空间复杂度是 O(n),这里一定要注意。因为 这个 n 是必须的,无法省。当我们说空间复杂度的时候,指的是除原本数据存储空间外,算法还需要额外的那部分存储空间。

不管是链式栈还是顺序栈,出栈与入栈只是设计栈顶个别数据的操作,只是需要拓容的时候会 O(n),但是均摊以后最后还是 O(1)。

所以栈的时间与空间复杂度都是 O(1)。

拓容实现

当容量达到指定默认值大小的时候再入栈数据则需要拓容知道拓容到最大限制大小。

数组拓容可以通过 System.arraycopy(stack, 0, newStack, 0, size); 当空间不足的时候申请原数组两倍大小的数组,然后把原始数据复制到新数组中。

拓容

/**
     * 扩容两倍 ,若是两倍数值超过 limit 则只能拓容到 limit
     */
    private void expand() {
        int newSize = size * 2;
        if (limit != DEFAULT_LIMIT && newSize > limit) {
            newSize = limit;
        }
        Object[] newStack = new Object[newSize];
        System.arraycopy(stack, 0, newStack, 0, size);
        this.stack = newStack;
        this.size = newSize;
    }

栈的应用场景

经典的应用场景就是 「函数调用栈」

操作系统给每个线程分配了一块独立的内存空间,这块内存被组织成“栈”这种结构, 用来存储函数调用时的临时变量。每进入一个函数,就会将临时变量作为一个栈帧入栈,当被调用函数执行完成,返回之后,将这个函数对应的栈帧出栈。

表达式求值

为了方便解释,我将算术表达式简化为只包含加减乘除四则运算,比如:34+13*9+44-12/3  。对于这个四则运算,我们人脑可以很快求解出答案,但是对于计算机来说,理解这个表达式本身就是个挺难的事儿。如果换作你,让你来实现这样一个表达式求值的功能,你会怎么做呢?

实际上编译器就是通过两个栈实现的。一个保存操作数的栈、一个则保存操作运算符的栈。

我们从左向右遍历表达式,当遇到数字,我们就直接压入操作数栈;当遇到运算符,就与运算符栈的栈顶元素进行比较。

如果比运算符栈顶元素的优先级高,就将当前运算符压入栈;如果比运算符栈顶元素的优先级低或者相同,从运算符栈中取栈顶运算符,从操作数栈的栈顶取 2 个操作数,然后进行计算,再把计算完的结果压入操作数栈,继续比较。如下图所示

浏览器后退前进

我们使用两个栈,X 和 Y,我们把首次浏览的页面依次压入栈 X,当点击后退按钮时,再依次从栈 X 中出栈,并将出栈的数据依次放入栈 Y。当我们点击前进按钮时,我们依次从栈 Y 中取出数据,放入栈 X 中。当栈 X 中没有数据时,那就说明没有页面可以继续后退浏览了。当栈 Y 中没有数据,那就说明没有页面可以点击前进按钮浏览了。

比如你顺序查看了 a,b,c 三个页面,我们就依次把 a,b,c 压入栈,这个时候,两个栈的数据就是这个样子:

点击后退,从页面 c 后退到页面 a 之后,我们就依次把 c 和 b 从栈 X 中弹出,并且依次放入到栈 Y。这个时候,两个栈的数据就是这个样子:

这时候想看 b,于是你又点击前进按钮回到 b 页面,我们就把 b 再从栈 Y 中出栈,放入栈 X 中。此时两个栈的数据是这个样子:

这个时候,你通过页面 b 又跳转到新的页面 d 了,页面 c 就无法再通过前进、后退按钮重复查看了,所以需要清空栈 Y。此时两个栈的数据这个样子:

通过来两个栈来操作,快速的实现了前进后退。


相关文章
|
1月前
|
C语言
【数据结构】栈和队列(c语言实现)(附源码)
本文介绍了栈和队列两种数据结构。栈是一种只能在一端进行插入和删除操作的线性表,遵循“先进后出”原则;队列则在一端插入、另一端删除,遵循“先进先出”原则。文章详细讲解了栈和队列的结构定义、方法声明及实现,并提供了完整的代码示例。栈和队列在实际应用中非常广泛,如二叉树的层序遍历和快速排序的非递归实现等。
215 9
|
1月前
|
存储 算法
非递归实现后序遍历时,如何避免栈溢出?
后序遍历的递归实现和非递归实现各有优缺点,在实际应用中需要根据具体的问题需求、二叉树的特点以及性能和空间的限制等因素来选择合适的实现方式。
37 1
|
28天前
|
存储 缓存 算法
在C语言中,数据结构是构建高效程序的基石。本文探讨了数组、链表、栈、队列、树和图等常见数据结构的特点、应用及实现方式
在C语言中,数据结构是构建高效程序的基石。本文探讨了数组、链表、栈、队列、树和图等常见数据结构的特点、应用及实现方式,强调了合理选择数据结构的重要性,并通过案例分析展示了其在实际项目中的应用,旨在帮助读者提升编程能力。
54 5
|
1月前
|
存储 算法 Java
数据结构的栈
栈作为一种简单而高效的数据结构,在计算机科学和软件开发中有着广泛的应用。通过合理地使用栈,可以有效地解决许多与数据存储和操作相关的问题。
|
1月前
|
存储 JavaScript 前端开发
执行上下文和执行栈
执行上下文是JavaScript运行代码时的环境,每个执行上下文都有自己的变量对象、作用域链和this值。执行栈用于管理函数调用,每当调用一个函数,就会在栈中添加一个新的执行上下文。
|
1月前
|
存储
系统调用处理程序在内核栈中保存了哪些上下文信息?
【10月更文挑战第29天】系统调用处理程序在内核栈中保存的这些上下文信息对于保证系统调用的正确执行和用户程序的正常恢复至关重要。通过准确地保存和恢复这些信息,操作系统能够实现用户模式和内核模式之间的无缝切换,为用户程序提供稳定、可靠的系统服务。
52 4
|
2月前
|
算法 程序员 索引
数据结构与算法学习七:栈、数组模拟栈、单链表模拟栈、栈应用实例 实现 综合计算器
栈的基本概念、应用场景以及如何使用数组和单链表模拟栈,并展示了如何利用栈和中缀表达式实现一个综合计算器。
48 1
数据结构与算法学习七:栈、数组模拟栈、单链表模拟栈、栈应用实例 实现 综合计算器
|
1月前
|
算法 安全 NoSQL
2024重生之回溯数据结构与算法系列学习之栈和队列精题汇总(10)【无论是王道考研人还是IKUN都能包会的;不然别给我家鸽鸽丢脸好嘛?】
数据结构王道第3章之IKUN和I原达人之数据结构与算法系列学习栈与队列精题详解、数据结构、C++、排序算法、java、动态规划你个小黑子;这都学不会;能不能不要给我家鸽鸽丢脸啊~除了会黑我家鸽鸽还会干嘛?!!!
|
1月前
|
算法
数据结构之购物车系统(链表和栈)
本文介绍了基于链表和栈的购物车系统的设计与实现。该系统通过命令行界面提供商品管理、购物车查看、结算等功能,支持用户便捷地管理购物清单。核心代码定义了商品、购物车商品节点和购物车的数据结构,并实现了添加、删除商品、查看购物车内容及结算等操作。算法分析显示,系统在处理小规模购物车时表现良好,但在大规模购物车操作下可能存在性能瓶颈。
50 0
|
2月前
初步认识栈和队列
初步认识栈和队列
65 10