14. 栈五题(一道困难题)

14. 栈五题(一道困难题)

  1. 20. 有效的括号 - 力扣(LeetCode)

    栈做法:

    class Solution:
        def isValid(self, s: str) -> bool:
            stack = [s[0]]
            for i in range(1, len(s)):
                if stack and (
                    s[i] == ')' and stack[-1] == '('
                    or s[i] == ']' and stack[-1] == '['
                    or s[i] == '}' and stack[-1] == '{'
                    ):
                    stack.pop()
                else: # 其余情况都append
                    stack.append(s[i])
            if stack: return False # stack若非空,要么说明左括号没有遇到对应右括号导致未被消除,要么说明右括号没有对应左括号导致被append
            else: return True
    

    不断合并做法:

    class Solution:
        def isValid(self, s: str) -> bool:
            s_len = len(s)
            if s_len % 2 == 1: #长度为奇数则必无效
                return False
    
            while '{}' in s or '[]' in s or '()' in s:
                s = s.replace('{}', '')
                s = s.replace('[]', '')
                s = s.replace('()', '')
    
            return s == ''
    
  2. 155. 最小栈 - 力扣(LeetCode)

    本题细究之下还是比较难的,O(1)时间O(n)额外空间可以用辅助栈,但若再要求O(1)额外空间呢?

    本题难点在于实现O(1)时间获取栈内最小值(尤其是当push和pop之后栈内最小值可能有变动),以及如果更进阶的,需要同时实现O(1)额外空间

    思路为维护一个类似前缀和概念的前缀最小值preMin,每一个结点(或者说索引)的preMin都是当前结点及其之前结点的最小值。(可以写成下面的递推形式preMin[i+1] = min(preMin[i], val[i+1])

    灵茶山艾府的解法(O(n)额外空间,其实就是辅助栈记录每一个结点的前缀和):

    该解法中,栈中的每一个元素是一个元组,记录(当前结点值,当前结点值对应的preMin)

    class MinStack:
        def __init__(self):
            # 这里的 0 写成任意数都可以,反正用不到
            self.st = [(0, inf)]  # 栈底哨兵
    
        def push(self, val: int) -> None:
            self.st.append((val, min(self.st[-1][1], val)))
    
        def pop(self) -> None:
            self.st.pop()
    
        def top(self) -> int:
            return self.st[-1][0]
    
        def getMin(self) -> int:
            return self.st[-1][1]
    

    真正O(1)额外空间的解法:

    重点在于,要用栈记录一路以来的差值:当前结点值 - 前一结点对应的前缀最小值preMin​,而不是用栈记录当前结点值!然后再用额外的一个变量preMin来维护当前的前缀最小值(仅在差值为负数时更新)。

    这样的做法用O(1)额外空间和原本必要的O(n)栈空间,变相记录了每一处结点的preMin和值!

    class MinStack:
    
        def __init__(self):
            # diffStack数据栈,但是存储的是当前结点减去前一个结点的(也就是未更新的)preMin的差值!
            # 这样当push时,先计算当前差值并保存,如果差值为负说明当前结点的(也就是新preMin)是由当前结点提供的,需要更新preMin;
            # 当pop时,由于diffStack保存的是当前结点与前一个结点的preMin的差值,所以如果栈顶元素对应差值为负,说明栈顶元素push时修改了preMin,我们可以根据栈顶元素差值和栈顶元素对应的preMin恢复到前一个元素的preMin!然后再pop掉栈顶元素即可。
            self.diffStack = [] #初始化为空
            self.preMin = inf # 记录的是当前最小值(前缀最小值),初始化为无穷大可以在push第一个元素时简化判断
    
        def push(self, val: int) -> None:
            diff = val - self.preMin # 获取当前结点值 与 前一结点对应preMin 的差值
            self.diffStack.append(diff) # append操作本身就是O(1)时间
            if diff < 0: # diff小于0时,说明当前结点值更小,需要修改preMin
                self.preMin = val # 更新preMin
    
        def pop(self) -> None:
            diff = self.diffStack.pop() # pop栈顶的diff元素
            if diff < 0: # 差值为负时,说明当前结点(也就是未经过pop的栈顶)修改了前一个结点处的preMin,需要恢复preMin为之前更大的preMin
                self.preMin -= diff
    
        def top(self) -> int:
            top_diff = self.diffStack[-1]
            if top_diff < 0: # 注意这里也要判断,如果栈顶元素为负数,说明在栈顶元素处更新了preMin,且更新的preMin必然等于当前栈顶元素对应的真正的结点值!此时是不能用差值+preMin来算的
                return self.preMin
            return top_diff + self.preMin
    
        def getMin(self) -> int:
            return self.preMin
    

    题目下方评论区用链表的实现法(比较巧妙,但并不是O(1)额外空间):

    class Node:
        def __init__(self, val, min_, next_):
            self.val = val
            self.min = min_
            self.next = next_
    
    
    class MinStack:
    
        def __init__(self):
            self.head = Node(0, float('inf'), None)
    
        def push(self, val: int) -> None:
            self.head = Node(val, min(self.getMin(), val), self.head)
    
        def pop(self) -> None:
            self.head = self.head.next
    
        def top(self) -> int:
            return self.head.val
    
        def getMin(self) -> int:
            return self.head.min
    
  3. 394. 字符串解码 - 力扣(LeetCode)

    递归写法比较直观好写,但是记得本题要考虑的细节比较多!(尤其是k可能是100这种多位的数字,而不一定是一位数字)

    class Solution:
        def decodeString(self, s: str) -> str:
            def getSubString(s, start):
                # 递归写法,从start索引处开始(保证start处必为字母或数字),向后搜索,扩充子串;
                # 如果遇到右括号,立刻返回子串和右括号右边格子的索引newStart。
                # 如果遇到字母,直接并入子串;
                # 如果遇到数字,从下下个索引(必为字母或数字)开始递归getSubString,将递归结果并入子串并继续向后搜索直到遇见第一个右括号
                i = start
                subs = "" # 子串
                while i < len(s) and s[i] != ']':
                    char = s[i] # 当前字符
                    if char.isalpha(): # 当前字符是字母
                        subs += char
                        i += 1
                    elif char.isdigit(): # 当前字符是数字
                        # 注意这里k不一定是只有一位!可能有连续几位数字,所以要获取完整的数字
                        while s[i+1].isdigit():
                            char += s[i+1]
                            i += 1
    
                        temps, i = getSubString(s, i + 2) # 从数字的下下个字符开始递归,返回递归获取到的子串,此处同时还已经在递归后更新索引i防止重复'访问
                        subs += int(char) * temps
                return subs, i + 1
    
            # 注意getSubString无法处理类似"2[ab]cd3[ef]gh"中最外层括号外的末尾字母,如cd和gh,因为到ab的右括号就直接返回了,所以要额外处理末尾的纯字符!
            # i = 0
            # subs = ""
            # while i < len(s):
            #     newsubs, newStart = getSubString(s, i)
            #     subs += newsubs
            #     i = newStart
    
            # 也可以直接在整个s外层套一个"1[]"变成1[s],就可以直接传入不用考虑末尾纯字符了
            subs, _ = getSubString("1[" + s + "]", 0)
    
            return subs
    

    学习k神题解:

    递归法和我思路基本一致,代码如下,比我的简洁,对连续数字字符、括号外末尾字符的处理也比我的好:

    class Solution:
        def decodeString(self, s: str) -> str:
            def dfs(s, i):
                res, multi = "", 0 # multi对应着k
                while i < len(s):
                    if '0' <= s[i] <= '9':
                        multi = multi * 10 + int(s[i])
                    elif s[i] == '[':
                        i, tmp = dfs(s, i + 1)
                        res += multi * tmp
                        multi = 0 # multi用完之后要清零!
                    elif s[i] == ']':
                        return i, res
                    else:
                        res += s[i]
                    i += 1
                return res
            return dfs(s,0)
    

    辅助栈法:

    算法流程:

    • 构建辅助栈 stack, 遍历字符串 s 中每个字符 c;

      • 当 c 为数字时,将数字字符转化为数字 multi,用于后续倍数计算;

      • 当 c 为字母时,在 res 尾部添加 c;(这一步能够保证可以读到最外层括号外部末尾的字母了!)

      • 当 c 为 [ 时,将当前 multi 和 res 入栈,并分别置空置 0:

        • 记录此 [ 前的临时结果 res 至栈,用于发现对应 ] 后的拼接操作;
        • 记录此 [ 前的倍数 multi 至栈,用于发现对应 ] 后,获取 multi × [...] 字符串。
        • 进入到新 [ 后,res 和 multi 重新记录
      • 当 c 为 ] 时,stack 出栈,拼接字符串 res = last_res + cur_multi * res,其中:

        • last_res是上个 [ 到当前 [ 的字符串,例如 "3[a2[c]]" 中的 a;
        • cur_multi是当前 [ 到 ] 内字符串的重复倍数,例如 "3[a2[c]]" 中的 2。
        • 返回字符串 res。
    class Solution:
        def decodeString(self, s: str) -> str:
            stack, res, multi = [], "", 0
            for c in s:
                if c == '[':
                    stack.append([multi, res]) # 发现新的左括号,那么之前的multi和res都要入栈保存,直到发现对应的右括号,那时pop出来的multi和res分别对应右边子串的重复次数cur_multi和左边已有字串last_res
                    res, multi = "", 0 # 在递归函数中记录新的res和multi
                elif c == ']':
                    cur_multi, last_res = stack.pop()
                    res = last_res + cur_multi * res
                elif '0' <= c <= '9':
                    multi = multi * 10 + int(c)            
                else:
                    res += c
            return res
    
  4. 739. 每日温度 - 力扣(LeetCode)

    经典的单调栈题目

    本题从左向右遍历,单调栈内的元素都是索引,从栈底到栈顶对应的温度总是保持呈递减趋势,遇到比栈顶元素温度高的待入栈元素,就不断pop直到待入栈元素能够在保证栈单调递减的前提下入栈

    class Solution:
        def dailyTemperatures(self, temperatures: List[int]) -> List[int]:
            # 显然是单调栈解决
            stack = [0] # 单调栈内的元素都是索引,从栈底到栈顶呈递减趋势
            n = len(temperatures)
            res = [0] * n # 提前开好结果空间
            for i in range(1, n):
                t = temperatures[i] # 当前待入栈的温度
                while stack and temperatures[stack[-1]] < t: # 出栈直到栈顶元素对应温度高于当前温度或栈为空
                    top = stack.pop() # 栈顶出栈
                    res[top] = i - top # i是栈顶右侧遇到的第一个比它高的索引
                stack.append(i)
            return res
    
  5. 84. 柱状图中最大的矩形 - 力扣(LeetCode)

    这是第二遍做,虽然这道题依然没啥思路,但是对单调栈的理解还是变得比之前第一次做的时候要深刻多了。

    • 基本思路:

      遍历每一个heights中的高度,并利用单调栈在每个对应高度上获取该索引上左侧和右侧的边界, 从而获取到该索引高度对应的宽度,继而得到面积,并取最大面积。

    • 如何获得当前索引高度最左侧和最右侧的索引呢?

      • 用从栈底到栈顶递增的单调栈,一旦遇到比栈顶元素低的高度,立刻触发对栈顶的循环pop,直到当前待入栈元素入栈后依然能保持原本的单调性!

      • 每次触发,相当于将当前一整段连续递增且比待入栈元素(短板)高的高度子序列提取出来,

        从右向左从高到低一个一个计算当前高度对应的宽度乃至面积。

        (连续重复元素需要使用技巧总是只入栈最新的索引,防止重复入栈)

      • 每次触发,待入栈元素的索引right相当于这段递增的非连续子序列(非连续是因为我们记录的索引不一定连续)右边的第一个低点,

        所以right-1​就是子序列最右侧(包含在子序列中)的索引,必然是子序列中每个高度的右边界。

      • 同时在当次触发中,不断pop栈顶元素(令索引为peak_idx),相当于取出当前栈顶元素高度peak,该高度一定小于等于索引区间[peak_idx, right-1]中所有高度,

        而peak_idx左侧也就是新的栈顶元素索引left处高度必然小于peak(因为我们已经去掉了站内的连续重复,当然不去的话也问题不大,想象一下即可知道为什么了),也就是说left处是peak_idx左边第一个低点!

      • 所以实际上当前peak对应的宽度索引区间为(left, right-1]。

    • 最后一个要点是:

      • 由于最后抵达末尾之后,可能stack内还留有一个单调递增的子序列,所以需要在heights的最后手动添加dummy值-1,使得遍历到-1的时候能直接清空栈;

        heights.append(-1)

      • 另外,又由于当栈中只有一个数的时候,pop出栈顶peak_idx,则栈变空,无法正常获取left = stack[-1]了!因此还需要为栈stack初始化一个-1元素,对应着索引0的左边索引,使得宽度计算也能正常进行。

        stack = [-1]

    class Solution:
        def largestRectangleArea(self, heights: List[int]) -> int:
            # 基本思路:遍历每一个heights中的高度,并利用单调栈在每个对应高度上获取该索引上左侧和右侧的边界,从而获取到该索引高度对应的宽度,继而得到面积,并取最大面积
            # 如何获得当前索引高度最左侧和最右侧的索引呢?
            # 用从栈底到栈顶递增的单调栈,一旦遇到比栈顶元素低的高度,立刻触发对栈顶的循环pop,直到当前待入栈元素入栈后依然能保持原本的单调性!
            # 每次触发,相当于将当前一整段连续递增且比待入栈元素(短板)高的高度子序列提取出来,从右向左从高到低一个一个计算当前高度对应的宽度乃至面积。(连续重复元素需要使用技巧总是只入栈最新的索引,防止重复入栈)
            # 每次触发,待入栈元素的索引right相当于这段递增的非连续子序列(非连续是因为我们记录的索引不一定连续)右边的第一个低点,所以right-1就是子序列最右侧(包含在子序列中)的索引,必然是子序列中每个高度的右边界。
            # 同时在当次触发中,不断pop栈顶元素(令索引为peak_idx),相当于取出当前栈顶元素高度peak,该高度一定小于等于索引区间[peak_idx, right-1]中所有高度,
            # 而peak_idx左侧也就是新的栈顶元素索引left处高度必然小于peak(因为我们已经去掉了站内的连续重复,当然不去的话也问题不大,想象一下即可知道为什么了),也就是说left处是peak_idx左边第一个低点!
            # 所以实际上当前peak对应的宽度索引区间为(left, right-1]。
    
            # 最后一个要点是:
            # 由于最后抵达末尾之后,可能stack内还留有一个单调递增的子序列,所以需要在heights的最后手动添加dummy值-1,使得遍历到-1的时候能直接清空栈;
            # 另外,又由于当栈中只有一个数的时候,pop出栈顶peak_idx,则栈变空,无法正常获取left = stack[-1]了!因此还需要为栈stack初始化一个-1元素,对应着索引0的左边索引,使得宽度计算也能正常进行。
    
            heights.append(-1) # 由于最后抵达末尾之后,可能stack内还留有一个单调递增的子序列,所以需要在heights的最后手动添加dummy值-1,使得遍历到-1的时候能直接清空栈
            stack = [-1] # 由于当栈中只有一个数的时候,pop出栈顶peak_idx,则栈变空,无法正常获取left = stack[-1]了!因此还需要为栈stack初始化一个-1元素,对应着索引0的左边索引,使得宽度计算也能正常进行。
            res = 0 # 记录最大矩形面积
            for i, h in enumerate(heights):
                while stack[-1] != -1 and heights[stack[-1]] >= h: 
                # 第一个条件:stack[-1] != -1是为了防止pop到栈底-1时继续pop后导致引用stack[-1]出错,相当于常规单调栈中的while stack,这个条件也可以写为len(stack) > 1,效果一样;
                # 第二个条件:取等号可以去掉栈中连续的重复高度,只记录最新的重复元素索引
                    peak_idx = stack.pop() # 当前峰值索引
                    peak = heights[peak_idx] # 矩形高度(峰值)
                    left, right = stack[-1], i # peak高度左边第一个低点索引和右边第一个低点索引
                    width = right - left - 1 # 矩形宽度
                    res = max(res, peak * width)
                stack.append(i)
            return res
    
    

你可能感兴趣的:(Hot,100,Mophead的小白刷题笔记,leetcode,python)