数据结构与算法之美---CH08---栈

0. 开篇问题

  假设你是 Chrome 浏览器的开发工程师,你会如何实现网页前进后退的功能?

1. 什么是栈?

  栈是一种操作受限的线性表,只允许在一端插入和删除数据。
后进者先出,先进者后出,这就是典型的“栈”结构。

2. 为什么需要栈?

  相比数组和链表,栈带来的只有限制,并没有任何优势。那直接使用数组或者链表不就好了吗?为什么还要用这个“操作受限”的“栈”呢?

  1. 数组和链表确实能够替代栈
  2. 特定的数据结构是对特定场景的抽象
  3. 软件开发中,不是暴露越多接口越好,操作灵活会带来不可控。一句话“有限制,才自由”,这是软件开发的特点。

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

3. 如何实现一个栈?

实现一个栈,必须要实现栈的接口。

public interface StackInterface<T> {
    boolean push(T item);
    T pop();
}

3.1 顺序栈

public class ArrayStack<T> implements StackInterface<T> {
    final static int LEN = 10;//栈最大元素个数
    T[] items = (T[]) new Object[LEN];
    int index = 0;
    @Override
    public boolean push(T item) {
        if (index >= LEN) {
            return false;
        }
        items[index++] = item;
        return true;
    }
    @Override
    public T pop() {
        if (index <= 0) {
            return null;
        }
        return items[--index];
    }
}

出栈入栈时间复杂度:O(1)
出栈入栈空间复杂度:O(1)

3.2 链式栈

public class LinkStack<T> implements StackInterface<T> {
    private class Node {
        public Node(T item, Node next) {
            this.item = item;
            this.next = next;
        }

        T item;
        Node next;
    }
    private Node top;
    private int index;

    @Override
    public boolean push(T item) {
        Node newNode = new Node(item, top);
        top = newNode;
        index++;
        return true;
    }

    @Override
    public T pop() {
        if (index <= 0) {
            return null;
        }
        T item = top.item;
        top = top.next;
        index--;
        return item;
    }
}

出栈入栈时间复杂度:O(1)
出栈入栈空间复杂度:O(1)

3.3 支持动态扩容的栈

  链式栈天然支持动态扩容,顺序栈需要在栈容量不足时,做重新申请空间和数据搬移工作。
操作如图:
在这里插入图片描述
代码如下:

public class ArrayStack<T> implements StackInterface<T> {
    int LEN = 2;
    T[] items = (T[]) new Object[LEN];
    int index = 0;

    @Override
    public boolean push(T item) {
        if (index >= LEN) {
        //判断栈满了,就申请2倍空间,然后搬移数据,再push
            T[] tmpItems = (T[]) new Object[LEN+LEN];
            LEN = 2*LEN;
            System.arraycopy(items, 0, tmpItems, 0, items.length);
            items = tmpItems;
        }
        items[index++] = item;
        return true;
    }

    @Override
    public T pop() {
        if (index <= 0) {
            return null;
        }

        return items[--index];
    }
}

出栈入栈时间复杂度:最好O(1),最坏O(n),均摊O(1)
出栈入栈空间复杂度:O(1)

4. 栈的应用

4.1 函数调用栈

  操作系统给每个线程分配了一块独立的内存空间,这块内存被组织成“栈”这种结构,用来存储函数调用时的临时变量。

  1. 每进入一个函数,就会将其中的临时变量作为栈帧入栈
  2. 当被调用函数执行完成return之后,将这个函数对应的栈帧出栈。
    如下代码:
int main() {
   int a = 1; 
   int ret = 0;
   int res = 0;
   ret = add(3, 5);
   res = a + ret;
   printf("%d", res);
   reuturn 0;
}

int add(int x, int y) {
   int sum = 0;
   sum = x + y;
   return sum;
}

调用栈如图:
在这里插入图片描述

4.2 表达式求值

  比如3+5*8-6,这个表达式看起来简单,但是对于计算机来说理解它是个复杂的事情。
  利用两个栈,其中一个用来保存操作数,另一个用来保存运算符,从左向右遍历表达式。

  1. 当遇到数字,我们就直接压入操作数栈;
  2. 当遇到运算符,就与运算符栈的栈顶元素进行比较
    (1) 若比运算符栈顶元素优先级高,就将当前运算符压入栈.
    (2) 若比运算符栈顶元素的优先级低或者相同,从运算符栈中取出栈顶运算符,从操作数栈顶取出2个操作数,然后计算,把计算完的结果压入操作数栈
  3. 继续比较,知道遍历到最后,求出最终结果。

操作过程如下图所示:
在这里插入图片描述

4.3 括号匹配中应用

  1. 用栈保存为匹配的左括号,从左到右一次扫描字符串,当扫描到左括号时,则将其压入栈中;
  2. 当扫描到右括号时,从栈顶取出一个左括号,如果能匹配上,则继续扫描剩下的字符串。
  3. 如果扫描过程中,遇到不能配对的右括号,或者栈中没有数据,则说明为非法格式。
  4. 当所有的括号都扫描完成之后,如果栈为空,则说明字符串为合法格式;否则,说明未匹配的左括号为非法格式。

5. 解开篇答

如何实现浏览器的前进后退功能?
  我们使用两个栈X 和 Y。

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

如下图演示的几种情况:

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

  2. 当你通过浏览器的后退按钮,从页面 c 后退到页面 a 之后,我们就依次把 c 和 b 从栈 X 中弹出,并且依次放入到栈 Y。这个时候,两个栈的数据就是这个样子:
    在这里插入图片描述

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

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

6. 课后思考

6.1 函数调用栈

为什么函数调用要用“栈”来保存临时变量呢?用其他数据结构不行吗?

  1. 不一定非用栈来保存临时变量,只不过函数调用符合后进先出特性,用栈来实现最顺理成章。
  2. 从调用函数进入被调函数,对于数据来说变化的是什么呢?是作用域
  3. 本质上只要能保证每进入一个新的函数,都是一个新的作用域就可以。而实现用栈非常方便,在进入被调函数时,分配一段栈空间给这个函数的变量,在函数结束的时候,将栈顶复位,正好回到调用函数的作用域内。

6.2 JVM堆栈概念

栈内存用来存储局部变量和方法调用,堆内存用来存储 Java 中的对象。那 JVM 里面的“栈”跟我们这里说的“栈”是不是一回事呢?如果不是,那它为什么又叫作“栈”呢?

  1. 内存中的堆栈和数据结构堆栈不是一个概念,可以说内存中的堆栈是真实存在的物理区,数据结构中的堆栈是抽象的数据存储结构。
  2. 内存空间在逻辑上分为三部分:代码区、静态数据区和动态数据区,动态数据区又分为栈区和堆区。
  3. 代码区:存储方法体的二进制代码。高级调度(作业调度)、中级调度(内存调度)、低级调度(进程调度)控制代码区执行代码的切换。
  4. 静态数据区:存储全局变量、静态变量、常量,常量包括final修饰的常量和String常量。系统自动分配和回收。
  5. 栈区:存储运行方法的形参、局部变量、返回值。由系统自动分配和回收。
  6. 堆区:new一个对象的引用或地址存储在栈区,指向该对象存储在堆区中的真实数据。
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值