栈(Stack) 是一种经典的数据结构,它具有“后进先出”(Last-In-First-Out,LIFO)的特性。栈通常有两个基本操作:压栈(Push)和弹栈(Pop)。压栈操作将数据元素添加到栈顶,弹栈操作将栈顶的元素弹出。

除了基本操作,栈还有其他几个重要的概念:

  1. 栈顶(Top):栈中最后一个压入的元素。

  2. 栈底(Bottom):栈中最先被压入的元素。

  3. 栈空(Empty):栈中没有任何元素。

  4. 栈满(Full):栈中已经没有剩余的空间可以容纳新的元素。

栈的应用非常广泛,常见的应用场景包括:

  1. 程序的函数调用堆栈:每个函数的参数和返回值都被存储在一个栈帧(Stack Frame)中,调用一个函数时就将其栈帧压入栈中,函数返回时再将其栈帧弹出。

  2. 表达式求值:将中缀表达式转换为后缀表达式,再使用栈来求解后缀表达式。

  3. 编辑器的撤销和重做操作:使用两个栈,一个栈用于存储历史编辑操作,另一个栈用于存储已撤销的操作。

  4. 浏览器的前进和后退操作:使用两个栈,一个栈用于存储已访问过的页面,另一个栈用于存储已经访问过的页面的前一个页面。

栈的实现方式有多种,常见的实现方式包括数组实现和链表实现。数组实现的栈具有随机访问的特性,但插入和删除操作的时间复杂度较高;链表实现的栈具有插入和删除操作的时间复杂度较低,但访问元素的时间复杂度较高。

Python中可以使用列表(List)来实现栈的功能,因为列表可以在末尾添加和删除元素,可以很方便地实现栈的压入和弹出操作。

以下是使用列表实现栈的基本操作:

  1. 创建一个空的栈:
stack = []
  1. 将元素压入栈中:
stack.append(element)
  1. 从栈中弹出一个元素:
element = stack.pop()
  1. 获取栈顶元素:
element = stack[-1]
  1. 检查栈是否为空:
if not stack:
    print("stack is empty")

如果不嫌麻烦,可以把栈的实现可以封装成一个类,方便使用。以下是一个简单的栈类的实现:

class Stack:
    def __init__(self):
        self.stack = []
    
    def push(self, element):
        self.stack.append(element)
    
    def pop(self):
        if not self.stack:
            return None
        return self.stack.pop()
    
    def peek(self):
        if not self.stack:
            return None
        return self.stack[-1]
    
    def is_empty(self):
        return not self.stack

使用这个类,可以很方便地创建栈对象,并使用栈的各种方法:

stack = Stack()
stack.push(1)
stack.push(2)
stack.push(3)
print(stack.pop())  # 3
print(stack.peek())  # 2
print(stack.is_empty())  # False

155. 最小栈

设计一个支持 push ,pop ,top 操作,并能在常数时间内检索到最小元素的栈。

实现 MinStack 类:

  • MinStack() 初始化堆栈对象。
  • void push(int val) 将元素val推入堆栈。
  • void pop() 删除堆栈顶部的元素。
  • int top() 获取堆栈顶部的元素。
  • int getMin() 获取堆栈中的最小元素。

 

示例 1:

输入: ["MinStack","push","push","push","getMin","pop","top","getMin"] [[],[-2],[0],[-3],[],[],[],[]]

输出: [null,null,null,null,-3,null,0,-2]

解释: MinStack minStack = new MinStack(); minStack.push(-2); minStack.push(0); minStack.push(-3); minStack.getMin(); --> 返回 -3. minStack.pop(); minStack.top(); --> 返回 0. minStack.getMin(); --> 返回 -2.

 

提示:

  • $-2^{31} <= val <= 2^{31} - 1$
  • poptop 和 getMin 操作总是在 非空栈 上调用
  • pushpoptop, and getMin最多被调用 $3 * 10^4$ 次

这个代码很简单,但是要注意题目的要求 “常数时间获得最小值”。 所以用一个辅助最小栈。

class MinStack:

    def __init__(self):
        self.vals = []
        self.minStack = [float('inf')]

    def push(self, val: int) -> None:
        self.vals.append(val)
        self.minStack.append(self.minStack[-1] if self.minStack[-1] < val else val)

    def pop(self) -> None:
        self.vals.pop()
        self.minStack.pop()

    def top(self) -> int:
        return self.vals[-1] 

    def getMin(self) -> int:
        return self.minStack[-1]

# Your MinStack object will be instantiated and called as such:
# obj = MinStack()
# obj.push(val)
# obj.pop()
# param_3 = obj.top()
# param_4 = obj.getMin()

20. 有效的括号

给定一个只包括 '('')''{''}''['']' 的字符串 s ,判断字符串是否有效。

有效字符串需满足:

  1. 左括号必须用相同类型的右括号闭合。
  2. 左括号必须以正确的顺序闭合。
  3. 每个右括号都有一个对应的相同类型的左括号。

 

示例 1:

输入: s = "()" 输出: true

示例 2:

输入: s = "()[]{}" 输出: true

示例 3:

输入: s = "(]" 输出: false

 

提示:

  • 1 <= s.length <= 104
  • s 仅由括号 '()[]{}' 组成
class Solution:
    def isValid(self, s: str) -> bool:
        brackets = {'(':')','[':']','{':'}'}
        stack = []
        for x in s:
            if stack and stack[-1] in brackets and brackets[stack[-1]] == x:
                stack.pop()
            else:
                stack.append(x)
        return not len(stack)

首先,定义了一个字典 brackets,用于存储括号的匹配关系。然后创建一个空栈 stack,遍历输入字符串 s 中的每一个字符:

  1. 如果栈不为空,并且栈顶的括号和当前字符可以匹配,则弹出栈顶的括号。

  2. 如果栈为空,或者栈顶的括号和当前字符不能匹配,则将当前字符压入栈中。

最后,如果栈为空,说明所有的括号都匹配了,返回 True;否则返回 False

150. 逆波兰表达式求值

给你一个字符串数组 tokens ,表示一个根据 逆波兰表示法 表示的算术表达式。

请你计算该表达式。返回一个表示表达式值的整数。

注意:

  • 有效的算符为 '+''-''*' 和 '/' 。
  • 每个操作数(运算对象)都可以是一个整数或者另一个表达式。
  • 两个整数之间的除法总是 向零截断 。
  • 表达式中不含除零运算。
  • 输入是一个根据逆波兰表示法表示的算术表达式。
  • 答案及所有中间计算结果可以用 32 位 整数表示。

 

示例 1:

输入: tokens = ["2","1","+","3","*"] 输出: 9 解释: 该算式转化为常见的中缀算术表达式为:$((2 + 1) * 3) = 9$

示例 2:

输入: tokens = ["4","13","5","/","+"] 输出: 6 解释: 该算式转化为常见的中缀算术表达式为:$(4 + (13 / 5)) = 6$

示例 3:

输入: tokens = ["10","6","9","3","+","-11","","/","","17","+","5","+"] 输出: 22 解释: 该算式转化为常见的中缀算术表达式为:

$((10 * (6 / ((9 + 3) * -11))) + 17) + 5 \= ((10 * (6 / (12 * -11))) + 17) + 5 \= ((10 * (6 / -132)) + 17) + 5 \= ((10 * 0) + 17) + 5 \= (0 + 17) + 5 \= 17 + 5 \= 22$

提示:

  • $1 <= tokens.length <= 10^4$
  • tokens[i] 是一个算符("+""-""*" 或 "/"),或是在范围 [-200, 200] 内的一个整数

逆波兰表达式:

逆波兰表达式是一种后缀表达式,所谓后缀就是指算符写在后面。

  • 平常使用的算式则是一种中缀表达式,如 ( 1 + 2 ) * ( 3 + 4 ) 。
  • 该算式的逆波兰表达式写法为 ( ( 1 2 + ) ( 3 4 + ) * ) 。

逆波兰表达式主要有以下两个优点:

  • 去掉括号后表达式无歧义,上式即便写成 1 2 + 3 4 + *也可以依据次序计算出正确结果。
  • 适合用栈操作运算:遇到数字则入栈;遇到算符则取出栈顶两个数字进行计算,并将结果压入栈中
class Solution:
    def evalRPN(self, tokens: [str]) -> int:
        nums = []
        for i in tokens:
            if i.isdigit() or len(i)>1:   # 负数用isdigit()返回false
                nums.append(int(i))
            else:
                n1 = nums.pop()
                n2 = nums.pop()
                if i == '+':
                    nums.append(n2+n1)
                elif i == '-':
                    nums.append(n2-n1)
                elif i == '*':
                    nums.append(n2*n1)
                elif i == '/':
                    nums.append(int(n2/n1))
        return nums[-1]

该算法使用一个栈来跟踪表达式中的数字,在从左到右读取tokens时使用。当遇到一个数字时,它被推送到栈上。当遇到运算符时,栈上的前两个数字被弹出,运算符被应用于它们,然后将结果推回到栈上。

在循环结束时,栈上应该只剩下一个数字,这个数字是表达式的最终结果。这个数字作为函数的输出返回。

代码检查每个token以查看它是运算符还是数字。如果它是数字,则将其转换为整数并推送到栈上。如果它是运算符,则弹出栈上的前两个数字,并应用运算符。然后将结果推回到栈上。

代码处理四个运算符:+,-,*和/。如果运算符是+,则将弹出的两个数字相加,并将结果推回到栈上。如果运算符是-,则将第二个弹出的数字从第一个弹出的数字中减去,并将结果推回到栈上。如果运算符是*,则将弹出的两个数字相乘,并将结果推回到栈上。如果运算符是/,则将第二个弹出的数字除以第一个弹出的数字(使用整数除法),并将结果推回到栈上。

最后,函数返回栈中的最后一个元素,这应该是表达式的最终结果。

71. 简化路径

给你一个字符串 path ,表示指向某一文件或目录的 Unix 风格 绝对路径 (以 '/' 开头),请你将其转化为更加简洁的规范路径。

在 Unix 风格的文件系统中,一个点(.)表示当前目录本身;此外,两个点 (..) 表示将目录切换到上一级(指向父目录);两者都可以是复杂相对路径的组成部分。任意多个连续的斜杠(即,'//')都被视为单个斜杠 '/' 。 对于此问题,任何其他格式的点(例如,'...')均被视为文件/目录名称。

请注意,返回的 规范路径 必须遵循下述格式:

  • 始终以斜杠 '/' 开头。
  • 两个目录名之间必须只有一个斜杠 '/' 。
  • 最后一个目录名(如果存在)不能 以 '/' 结尾。
  • 此外,路径仅包含从根目录到目标文件或目录的路径上的目录(即,不含 '.' 或 '..')。

返回简化后得到的 规范路径 。

 

示例 1:

输入: path = "/home/"
输出: "/home"
解释: 注意,最后一个目录名后面没有斜杠。 

示例 2:

输入: path = "/../" 输出: "/" 解释: 从根目录向上一级是不可行的,因为根目录是你可以到达的最高级。

示例 3:

输入: path = "/home//foo/" 输出: "/home/foo" 解释: 在规范路径中,多个连续斜杠需要用一个斜杠替换。

示例 4:

输入: path = "/a/./b/../../c/" 输出: "/c"

 

提示:

  • 1 <= path.length <= 3000
  • path 由英文字母,数字,'.''/' 或 '_' 组成。
  • path 是一个有效的 Unix 风格绝对路径。
class Solution:
    def simplifyPath(self, path: str) -> str:
        path = path.split('/')
        stack = []
        for i in path:
            if i == '' or i == '.':
                continue
            elif i == '..':
                if stack:
                    stack.pop()
            else:
                stack.append(i)
        return '/' + '/'.join(stack)

用于简化给定的 Unix 路径。它接受一个字符串参数 path,然后使用斜杠 / 分隔路径字符串,并将其存储在一个列表中。然后,代码遍历该列表,并根据以下规则对路径进行简化:

  • 如果当前路径是空字符串或者单点(.),则跳过。
  • 如果当前路径是双点(..),则将栈中的最后一个路径弹出。
  • 如果当前路径不是单点或双点,则将其添加到栈中。

最后,代码使用 join 函数将栈中的路径组合成一个字符串,并在开头添加一个斜杠 /,然后将简化后的路径字符串作为结果返回。

394. 字符串解码

给定一个经过编码的字符串,返回它解码后的字符串。

编码规则为: k[encoded_string],表示其中方括号内部的 encoded_string 正好重复 k 次。注意 k 保证为正整数。

你可以认为输入字符串总是有效的;输入字符串中没有额外的空格,且输入的方括号总是符合格式要求的。

此外,你可以认为原始数据不包含数字,所有的数字只表示重复的次数 k ,例如不会出现像 3a 或 2[4] 的输入。

 

示例 1:

输入: s = "3[a]2[bc]"
输出: "aaabcbc"

示例 2:

输入: s = "3[a2[c]]"
输出: "accaccacc"

示例 3:

输入: s = "2[abc]3[cd]ef"
输出: "abcabccdcdcdef"

示例 4:

输入: s = "abc3[cd]xyz"
输出: "abccdcdcdxyz"

 

提示:

  • 1 <= s.length <= 30

  • s 由小写英文字母、数字和方括号 '[]' 组成

  • s 保证是一个 有效 的输入。

  • s 中所有整数的取值范围为 [1, 300]

class Solution:
    def decodeString(self, s: str) -> str:
        stack = []
        res = ''
        mul = 0
        for c in s:
            if c.isdigit():
                mul = mul*10 + int(c)
                # 注意mul的大小
            elif c == '[':
                stack.append([mul,res])
                res = ''
                mul = 0
            elif c == ']':
                m,s = stack.pop()
                res = s + m*res
            else:
                res += c
        return res
  1. 定义一个栈,一个字符串变量res用来保存当前解码的结果,一个整数变量mul用来保存当前数字字符所表示的倍数;

  2. 遍历字符串s中的每个字符c:

    a. 如果c是数字字符,将其加入mul变量的个位上;

    b. 如果c是左括号字符,将当前的mul和res入栈,并分别将mul和res初始化为0和空字符串;

    c. 如果c是右括号字符,弹出栈顶的mul和res,并将当前的res设置为栈顶的res加上mul个当前的res;

    d. 如果c是其他字符,直接将其加入res中;

  3. 最终返回res即为解码结果。

224. 基本计算器 - 力扣(Leetcode)

给你一个字符串表达式 s ,请你实现一个基本计算器来计算并返回它的值。

注意:不允许使用任何将字符串作为数学表达式计算的内置函数,比如 eval() 。

 

示例 1:

输入: s = "1 + 1"
输出: 2

示例 2:

输入: s = " 2-1 + 2 "
输出: 3

示例 3:

输入: s = "(1+(4+5+2)-3)+(6+8)"
输出: 23

 

提示:

  • $1 <= s.length <= 3 * 10^5$

  • s 由数字、'+''-''('')'、和 ' ' 组成

  • s 表示一个有效的表达式

  • '+' 不能用作一元运算(例如, "+1" 和 "+(2 + 3)" 无效)

  • '-' 可以用作一元运算(即 "-1" 和 "-(2 + 3)" 是有效的)

  • 输入中不存在两个连续的操作符

  • 每个数字和运行的计算将适合于一个有符号的 32位 整数

class Solution:
    def calculate(self, s: str) -> int:
        ans = 0
        num = 0
        sign = 1
        stack = [sign]  # stack[-1]: current env's sign

        for c in s:
            if c.isdigit():
                num = num * 10 + int(c)
            elif c == '(':
                stack.append(sign)
            elif c == ')':
                stack.pop()
            elif c == '+' or c == '-':
                ans += sign * num
                sign = (1 if c == '+' else -1) * stack[-1]
                num = 0

        return ans + sign * num
  1. 初始化 ans 和 num 为 0,sign 为 1,stack 为 [sign]。

  2. 遍历字符串 s 中的每个字符 c:

    • 如果 c 是数字,则更新 num 为 num*10 + int(c)。

    • 如果 c 是左括号 (,则将当前的 sign 压入 stack 中,表示进入一个新的环境。

    • 如果 c 是右括号 ),则将 stack 的栈顶元素弹出,得到当前环境的符号 sign_env。将当前环境的结果 num 与当前环境的符号 sign_env 相乘,加入到 ans 中,表示当前环境的结果已经计算完成,可以退出当前环境了。

    • 如果 c 是加号 + 或减号 -,则将当前环境的结果 num 与当前环境的符号 sign_env 相乘,加入到 ans 中。更新当前环境的符号 sign 为 1 或 -1,具体根据 c 的值来判断。同时,将当前环境的符号 sign 与 stack 的栈顶元素相乘,得到当前环境下的符号,更新 stack 的栈顶元素。最后,将 num 重置为 0,开始下一个数字的解析。

  3. 最后,将 ans 与最后一个数字 num 乘以当前环境的符号 sign 相乘的结果相加,得到最终的计算结果。

需要注意的是,这个实现并没有考虑乘除法的优先级,只是按照从左到右的顺序进行计算。如果需要考虑优先级,可以在遍历时将乘除法的结果先计算出来,再计算加减法的结果。