关于数据结构和算法:西法带你学算法单调栈解题模板秒杀八道题

6次阅读

共计 3174 个字符,预计需要花费 8 分钟才能阅读完成。

枯燥栈

顾名思义,枯燥栈是一种栈。因而要学枯燥栈,首先要彻底搞懂栈。

栈是什么?

栈是一种受限的数据结构,体现在只容许新的内容从一个方向插入或删除,这个方向咱们叫栈顶,而从其余地位获取内容是不被容许的

栈最显著的特色就是 LIFO(Last In, First Out – 后进先出)

举个例子:

栈就像是一个放书本的抽屉,进栈的操作就好比是想抽屉里放一本书,新进去的书永远在最上层,而退栈则相当于从里往外拿书本,永远是从最上层开始拿,所以拿进去的永远是最初进去的哪一个

栈的罕用操作

  1. 进栈 – push – 将元素搁置到栈顶
  2. 退栈 – pop – 将栈顶元素弹出
  3. 栈顶 – top – 失去栈顶元素的值
  4. 是否空栈 – isEmpty – 判断栈内是否有元素

栈的罕用操作工夫复杂度

因为栈只在尾部操作就行了,咱们用数组进行模仿的话,能够很容易达到 O(1)的工夫复杂度。当然也能够用链表实现,即链式栈。

  1. 进栈 – O(1)
  2. 出栈 – O(1)

利用

  • 函数调用栈
  • 浏览器后退后退
  • 匹配括号
  • 枯燥栈用来寻找下一个更大(更小)元素

题目举荐

  • 394. 字符串解码
  • 946. 验证栈序列
  • 1381. 设计一个反对增量操作的栈

枯燥栈又是什么?

枯燥栈是一种非凡的栈。栈原本就是一种受限的数据结构了,枯燥栈在此基础上又受限了一次(受限 ++)。

枯燥栈要求栈中的元素是枯燥递增或者枯燥递加的。

是否严格递增或递加能够依据理论状况来。

这里我用 [a,b,c] 示意一个栈。其中 左侧为栈底,右侧为栈顶。

比方:

  • [1,2,3,4] 就是一个枯燥递增栈
  • [3,2,1] 就是一个枯燥递加栈
  • [1,3,2] 就不是一个非法的枯燥栈

那这个限度有什么用呢?这个限度(个性)可能解决什么用的问题呢?

实用场景

枯燥栈适宜的题目是求解 第一个一个大于 xxx或者 第一个小于 xxx这种题目。所有当你有这种需要的时候,就应该想到枯燥栈。

那么为什么枯燥栈适宜求解 第一个一个大于 xxx或者 第一个小于 xxx这种题目?起因很简略,我这里通过一个例子给大家解说一下。

这里举的例子是枯燥递增栈

比方咱们须要顺次将数组 [1,3,4,5,2,9,6] 压入枯燥栈。

  1. 首先压入 1,此时的栈为:[1]
  2. 持续压入 3,此时的栈为:[1,3]
  3. 持续压入 4,此时的栈为:[1,3,4]
  4. 持续压入 5,此时的栈为:[1,3,4,5]
  5. 如果 持续压入 2,此时的栈为:[1,3,4,5,2] 不满足枯燥递增栈的个性,因而须要调整。如何调整?因为栈只有 pop 操作,因而咱们只好一直 pop,直到满足枯燥递增为止。
  6. 下面其实咱们并没有压入 2,而是先 pop,pop 到压入 2 仍然能够放弃枯燥递增再 压入 2,此时的栈为:[1,2]
  7. 持续压入 9,此时的栈为:[1,2,9]
  8. 如果 持续压入 6,则不满足枯燥递增栈的个性,咱们故技重施,一直 pop,直到满足枯燥递增为止。此时的栈为:[1,2,6]

留神这里的栈依然是非空的,如果有的题目须要用到所有数组的信息,那么很有可能因没有思考边界而不能通过所有的测试用例。这里介绍一个技巧 – 哨兵法,这个技巧常常用在枯燥栈的算法中。

对于下面的例子,我能够在原数组 [1,3,4,5,2,9,6] 的右侧增加一个小于数组中最小值的项即可,比方 -1。此时的数组是 [1,3,4,5,2,9,6,-1]。这种技巧能够简化代码逻辑,大家尽量把握。

下面的例子如果你明确了,就不难理解为啥枯燥栈适宜求解 第一个一个大于 xxx或者 第一个小于 xxx这种题目了。比方下面的例子,咱们就能够很容易地求出 在其之后第一个小于其自身的地位 。比方 3 的索引是 1,小于 3 的第一个索引是 4,2 的索引 4,小于 2 的第一个索引是 0,然而其在 2 的索引 4 之后,因而不符合条件,也就是不存在 在 2 之后第一个小于 2 自身的地位

下面的例子,咱们在第 6 步开始 pop,第一个被 pop 进去的是 5,因而 5 之后的第一个小于 5 的索引就是 4。同理被 pop 进去的 3,4,5 也都是 4。

如果用 ans 来示意 在其之后第一个小于其自身的地位,ans[i] 示意 arr[i] 之后第一个小于 arr[i] 的地位,ans[i] 为 -1 示意这样的地位不存在,比方前文提到的 2。那么此时的 ans 是 [-1,4,4,4,-1,-1,-1]。

第 8 步,咱们又开始 pop 了。此时 pop 进去的是 9,因而 9 之后第一个小于 9 的索引就是 6。

这个算法的过程用一句话总结就是,如果压栈之后依然能够放弃枯燥性,那么间接压。否则先弹出栈的元素,直到压入之后能够放弃枯燥性。
这个算法的原理用一句话总结就是,被弹出的元素都是大于以后元素的,并且因为栈是枯燥增的,因而在其之后小于其自身的最近的就是以后元素了

上面给大家举荐几道题,大家趁着常识还在脑子来,连忙去刷一下吧~

伪代码

下面的算法能够用如下的伪代码示意,同时这是一个通用的算法模板,大家遇到枯燥栈的题目能够间接套。

倡议大家用本人相熟的编程语言实现一遍,当前改改符号根本就能用。

class Solution:
    def monostoneStack(self, arr: List[int]) -> List[int]:
        stack = []
        ans = 定义一个长度和 arr 一样长的数组,并初始化为 -1
        循环 i in  arr:
            while stack and arr[i] > arr[栈顶元素]:
                peek = 弹出栈顶元素
                ans[peek] = i - peek
            stack.append(i)
        return ans

复杂度剖析

  • 工夫复杂度:因为 arr 的元素最多只会入栈,出栈一次,因而工夫复杂度依然是 $O(N)$,其中 N 为数组长度。
  • 空间复杂度:因为应用了栈,并且栈的长度最大是和 arr 长度统一,因而空间复杂度是 $O(N)$,其中 N 为数组长度。

代码

这里进步两种编程语言的枯燥栈模板供大家参考。

Python3:

class Solution:
    def monostoneStack(self, T: List[int]) -> List[int]:
        stack = []
        ans = [0] * len(T)
        for i in range(len(T)):
            while stack and T[i] > T[stack[-1]]:
                peek = stack.pop(-1)
                ans[peek] = i - peek
            stack.append(i)
        return ans

JS:

var monostoneStack = function (T) {let stack = [];
  let result = [];
  for (let i = 0; i < T.length; i++) {result[i] = 0;
    while (stack.length > 0 && T[stack[stack.length - 1]] < T[i]) {let peek = stack.pop();
      result[peek] = i - peek;
    }
    stack.push(i);
  }
  return result;
};

题目举荐

上面几个题帮忙你了解枯燥栈,并让你明确什么时候能够用枯燥栈进行算法优化。

  • 42. 接雨水
  • 84. 柱状图中最大的矩形
  • 739. 每日温度
    1. 去除反复字母
    1. 移掉 K 位数字
    1. 下一个更大元素 I
    1. 最短无序间断子数组
    1. 股票价格跨度

总结

枯燥栈实质就是栈,栈自身就是一种受限的数据结构。其受限指的是只能在一端进行操作。而枯燥栈在栈的根底上进一步受限,即要求栈中的元素始终保持枯燥性。

因为栈中都是枯燥的,因而其天生适宜解决 在其之后第一个小于其自身的地位 的题目。大家如果遇到题目须要找 在其之后第一个小于其自身的地位 的题目,就可是思考应用枯燥栈。

枯燥栈的写法绝对比拟固定,大家能够本人参考我的伪代码本人总结一份模板,当前间接套用能够大大提高做题效率和容错率。

我整顿的 1000 多页的电子书曾经开发下载了,大家能够去我的公众号《力扣加加》后盾回复电子书获取。

大家对此有何认识,欢送给我留言,我有工夫都会一一查看答复。更多算法套路能够拜访我的 LeetCode 题解仓库:https://github.com/azl3979858…。目前曾经 37K star 啦。

大家也能够关注我的公众号《力扣加加》带你啃下算法这块硬骨头。

正文完
 0