作者: 天命_风流 | 来源:发表于2020-03-13 23:35 被阅读0次

什么是栈

栈是一个非常基础的数据结构,它的特点是对保存的数据进行“后进先出”的操作。你可以想象你在刷盘子:先刷好的盘子放在最下面,最后刷完的盘子放在最上面,如果你需要使用盘子,一定会选择最上面的盘子,或者你要下面的盘子,就一定要先将上面的盘子移走。
你可以用下面的图理解栈:


栈.jpg

从栈的操作上来看,栈是一种“操作受限”的线性表,它只允许从栈顶插入和删除数据。
你是否会有和我最初一样的疑惑:我们已经有功能齐全的数组和链表,为什么还要使用这样一个操作被限制的数据结构呢?
确实,数组或者链表可以在功能上替代栈,但是特定的数据结构是对特定场景的抽象,当某个数据集合只设计在一端插入和删除数据,并满足后进先出的特性,我们就应当首选“栈”这种数据结构。而且,数组和链表暴露了太多的操作接口,虽然灵活自由,但是使用容易失控,也就更容易出错。

栈的实现

栈的实现可以由数组或者链表完成,由数组完成的栈被称为“顺序栈”,由链表完成的栈被称为“链式栈”,个人认为使用数组作为栈比较简单,下面给出专栏老师的代码:

// 基于数组实现的顺序栈
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;
  }
}

很容易发现,栈的push操作和pop操作都是O(1)的时间和空间复杂度。

支持动态扩容的顺序栈

如以前我们所讲的,数组的问题在于只能保存固定数据大小的内容,我们当然可以对栈进行扩容,其大致思路是这样的:
当栈未满,正常进行push操作,如果栈满,push操作就会申请一块更大的内存(假定为两倍),将栈内的数据搬运到新的栈中,然后再进行push操作,其过程大致如下:

支持动态扩容的顺序栈.jpg
在上面的过程中,你会发现push操作在不同的情况下会有不同的时间复杂度,要想分析它的时间复杂度,就要使用到之前学习过的摊还分析法

为了分析的方便,我们需要事先做一些假设和定义:

  • 栈空间不够时,我们重新申请一个是原来大小两倍的数组;
  • 为了简化分析,假设只有入栈操作没有出栈操作;
  • 定义不涉及内存搬移的入栈操作为 simple-push 操作,时间复杂度为 O(1)。

如果当前栈大小为 K,并且已满,当再有新的数据要入栈时,就需要重新申请 2 倍大小的内存,并且做 K 个数据的搬移操作,然后再入栈。但是,接下来的 K-1 次入栈操作,我们都不需要再重新申请内存和搬移数据,所以这 K-1 次入栈操作都只需要一个 simple-push 操作就可以完成。为了让你更加直观地理解这个过程,我画了一张图。


摊还分析动态栈的时间复杂度.jpg

经过分析,可以将扩容的时间均摊到之后的入栈操作中,均摊后他们的复杂度为O(1).

栈的应用

栈在函数调用中的应用

在操作系统中,操作系统会为每个线程分配一块独立的内存空间,这块内存就是栈的结构,它用于存储函数调用时的临时变量。每调用一个函数,就会将临时变量作为一个栈帧入栈,调用完成,就将函数对应的栈帧出栈。

栈在表达式求值中的应用

对于计算机来说,了解一个算数表达式是一件很困难的事,那么如何通过栈实现这个功能呢?举个例子,我们希望计算机实现计算 3+5*8-6 :
我们使用两个栈,一个栈保存操作数,一个栈保存运算符,我们从左到右依次遍历表达式,当遇到数字,就将数字入栈;遇到运算符,则需要和运算符栈的栈顶进行比较:
如果当前运算符比栈顶的运算符的优先级高,则直接入栈;如果比栈顶元素优先级低或者相同,就从运算符栈中取出运算符,从操作数栈中取出 2 个操作数,然后进行计算,然后把计算结果放入数字栈,然后再进行比较。
下面图解这一过程:


用栈做算术.jpg

用栈匹配括号

可以参考leetcode的第20题
解题思路:
我们从左往右依次扫描字符串,使用栈来保存尚未被匹配的左括号,当扫描到左括号就将其入栈,遇到右括号的时候,将栈顶出栈,如果栈顶元素可以匹配左括号,继续扫描。
在扫描过程中遇到不匹配的右括号,或栈中没有数据,或扫描完成后栈仍有数据,则说明格式非法。
下面给出解题代码:

class Solution:
    def isValid(self, s: str) -> bool:
        dic = {')':'(' ,']':'[' ,'}':'{'}
        stack = []
        for i in s:#依次扫描表达式
            if stack and i in dic.keys():#如果扫描到右括号
                ls = stack.pop()#栈顶出栈
                if ls == dic[i]:#如果左右括号匹配,就继续
                    pass
                else:#不匹配:表达式不合法
                    return False
            else:
                #左括号入栈
                stack.append(i)
        return not bool(stack) #如果stack空,则合法,如果不为空,则不合法

思考:你能否使用栈完成浏览器中的“前进”和“后退”功能呢?


以上就是关于栈的所有内容

注:本文章的主要内容来自我对极客时间app的《数据结构与算法之美》专栏的总结,我大量引用了其中的代码和截图,如果想要了解具体内容,可以前往极客时间

相关文章

  • Java实现栈

    数组栈:压栈、出栈、返回栈顶元素 链式栈:压栈、出栈、返回栈顶元素

  • 数据结构之 栈

    栈结构 链式栈 一.栈结构体 1构建空栈 2栈置空 3判断栈空 4获取栈顶 5入栈 6出栈 7便利栈 二.链式栈 ...

  • 栈和队列

    1、栈 栈是一种先进先出的数据结构。栈顶进栈,栈顶出栈。 数据结构 栈的初始化 进栈 出栈 栈的最小值 2、队列 ...

  • 递归累加数组

    入栈 5入栈 4入栈 3入栈 2入栈 1出栈 [1 0]出栈 [2 1 0]出栈 [3 2 1 0]出栈 [4 3...

  • 栈的逻辑结构和存储结构

    main()进栈s(1)进栈s(0)进栈 s(0)出栈s(1)出栈main()出栈 顺序栈 一个数组 + 指向栈顶...

  • 单调栈 2020-06-12(未经允许,禁止转载)

    1.单调栈 指栈内元素保持单调性的栈结构,分为单调增栈(栈底到栈顶元素递增)和单调减栈(栈底到栈顶元素递减) 2....

  • 链栈的操作

    链栈的定义 链栈的操作 初始化 判断栈空 入栈 出栈

  • 函数调用栈平衡

    栈平衡 栈平衡:函数调用前后的栈顶指针指向的位置不变 内平栈 外平栈 内平栈: 指的是在函数调用返回之前使栈保持...

  • 栈的简单Java实现

    栈栈的特点是先进后出,出栈、入栈都是在栈顶操作。

  • 汇编学习-入栈和出栈

    栈有两个基本的操作:入栈和出栈。入栈就是将一个新的元素放到栈顶,出栈就是从栈顶取出一个元素。栈顶的元素总是最后入栈...

网友评论

      本文标题:

      本文链接:https://www.haomeiwen.com/subject/fooyshtx.html