思考

首先思考一个问题,浏览器的前进、后退功能是如何实现的呢?

理解栈

  • 结构:先进的后出,后进的先出。类似于洗好的盘子,叠一摞,下次用的时候只能从最上面那个盘子开始拿。
  • 操作特性:操作受限线性表
  • 什么时候用:当某个数据集合只涉及在一端插入和删除数据,并且满足后进先出的特性,就应该首选这种结构。

如何实现栈

栈既可以用数组实现,也可以用链表来实现。用数组实现的栈,叫作顺序栈,用链表实现的栈,叫作链栈。

  • 示例:顺序栈

      class Stack():
    
          def __init__(self,size):
              """初始化"""
              self.size = size
              self.num = 0
              self.stack = []
    
          def getSize(self):
              """获取栈的长度"""
              return self.num
    
          def print_all(self):
              """输出栈元素"""
              for s in self.stack:
                  print s
          
          def append_stack(self,value):
              """入栈"""
              if self.num >= self.size:
                  print("the stack is full")
                  return
              else:
                  self.stack.append(value)
                  self.num += 1
         
          def pop_stack(self):
              """ 出栈"""
              if self.num is None:
                  print("the stack is empty")
                  return
              else:
                  self.stack.remove(self.stack[-1])
    

复杂度分析

空间复杂度

无论是顺序栈还是链栈,存储数据只需要一个大小为n的数组。在入栈和出栈过程中,只需要一两个临时变量存储空间,所以空间复杂度为O(1)

注:存储数据需要一个大小为n的数组,并不是指空间复杂度就为O(n)。因为,这 n 个空间是必须的,无法省掉。
我们说空间复杂度的时候,是指除了原本的数据存储空间外,算法运行还需要的额外的存储空间。

时间复杂度

不管顺序栈还是链栈,入栈、出栈只涉及栈顶个别数据的操作,所以复杂度为O(1)

支持动态扩容的顺序栈的入栈、出栈时间复杂度分析

对于出栈操作来说,不会涉及内存的重新申请和数据的搬移,所以出栈的时间复杂度仍然是 O(1)。但是,对于入栈操作来说,情况就不一样了。当栈中有空闲空间时,入栈操作的时间复杂度为 O(1)。但当空间不够时,就需要重新申请内存和数据搬移,所以时间复杂度就变成了 O(n)

也就是说,对于入栈操作来说,最好情况时间复杂度是 O(1),最坏情况时间复杂度是 O(n)。而平均时间复杂度,由摊还分析法分析可知为 O(1)

栈的应用

在函数调用中的应用

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

栈在表达式求值中的应用

实现一个表达式求值的功能,编译器就是通过两个栈来实现的。
其中一个保存操作数的栈,另一个是保存运算符的栈。
从左向右遍历表达式,当遇到数字,就直接压入操作数栈;当遇到运算符,就与运算符栈的栈顶元素进行比较。
如果当前运算符优先级高,就将当前运算符压入栈;如果运算符栈顶元素优先级高,就从运算符栈中取栈顶运算符,从操作数栈的栈顶取 2 个操作数,然后进行计算,再把计算完的结果压入操作数栈,继续比较。

栗子

3+5*8-6表达式的计算过程如下:

表达式

栈在括号匹配中的应用

假设表达式中只包含三种括号,圆括号 ()方括号 []花括号{},并且它们可以任意嵌套。比如,{[{}]}[{()}([])] 等都为合法格式,而{[}()][({)] 为不合法的格式。

问题

给定一个包含三种括号的表达式字符串,如何检查它是否合法呢?

方法

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

示例
left_brackets = '{[(<'
right_brackets = '}])>'
matching_brackets = {'}': '{', ']': '[', ')': '(', '>': '<'}

def judgment_brackets_matching(rows):
    stack = []
    label = True
    for row in rows:
        if row in left_brackets:
            stack.append(row)
        elif row in right_brackets:
            if len(stack) < 1:
                label = False
                break
            elif matching_brackets[row] == stack[-1]:
                stack.pop()
            else:
                label = False
                break
        else:
            continue
    if stack:
        label = False
    return label

解答开篇

用栈实现浏览器的前进、后退功能

方法

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

示例

顺序查看了a,b,c三个页面,将其依次压入栈,栈中数据情况为:

X: a->b->c
Y: None

点击后退按钮,从c页面推到a页面,栈中数据情况为:

X: None
Y: c->b->a

想再次查看b页面,点击前进按钮到b页面,此时栈中数据情况为:

X: a->b
Y: c

假设,此时在b页面跳转到新页面d,页面c就无法通过前进或后退按钮重复查看了,因而需要清空Y栈:

X: a->b->d
Y: None
-------------完-------------