算法数据结构——关于单调栈(Monotone Stack)的详细讲解及应用案例

1. 单调栈简介

单调栈(Monotone Stack):一种特殊的栈。在栈的「先进后出」规则基础上,要求「从 栈顶栈底 的元素是单调递增(或者单调递减)」。其中满足从栈顶到栈底的元素是单调递增的栈,叫做「单调递增栈」。满足从栈顶到栈底的元素是单调递减的栈,叫做「单调递减栈」。

注意:这里定义的顺序是从「栈顶」到「栈底」。有的文章里是反过来的。本文全文以「栈顶」到「栈底」的顺序为基准来描述单调栈。

1.1 单调递增栈

单调递增栈:只有比栈顶元素小的元素才能直接进栈,否则需要先将栈中比当前元素小的元素出栈,再将当前元素入栈。

这样就保证了:栈中保留的都是比当前入栈元素大的值,并且从栈顶到栈底的元素值是单调递增的。

单调递增栈的入栈、出栈过程如下:

  • 假设当前进栈元素为 x,如果 x 比栈顶元素小,则直接入栈。

  • 否则从栈顶开始遍历栈中元素,把小于 x 或者等于 x 的元素弹出栈,直到遇到一个大于 x 的元素为止,然后再把 x 压入栈中。

下面我们以数组 [2, 7, 5, 4, 6, 3, 4, 2] 为例,模拟一下「单调递增栈」的进栈、出栈过程。具体过程如下:

  • 数组元素:[2, 7, 5, 4, 6, 3, 4, 2],遍历顺序为从左到右。

第 i 步待插入元素操 作结 果(左侧为栈底)作 用
122 入栈[2]元素 2 的左侧无比 2 大的元素
272 出栈,7 入栈[7]元素 7 的左侧无比 7 大的元素
355 入栈[7, 5]元素 5 的左侧第一个比 5 大的元素为:7
444 入栈[7, 5, 4]元素 4 的左侧第一个比 4 大的元素为:5
564 出栈,5 出栈,6 入栈[7, 6]元素 6 的左侧第一个比 6 大的元素为:7
633 入栈[7, 6, 3]元素 3 的左侧第一个比 3 大的元素为:6
743 出栈,4 入栈[7, 6, 4]元素 4 的左侧第一个比 4 大的元素为:6
822 入栈[7, 6, 4, 2]元素 2 的左侧第一个比 2 大的元素为:4

最终栈中元素为 [7, 6, 4, 2]。因为从栈顶(右端)到栈底(左侧)元素的顺序为 2, 4, 6, 7,满足递增关系,所以这是一个单调递增栈。

我们以上述过程第 5 步为例,所对应的图示过程为:

1.2 单调递减栈

单调递减栈:只有比栈顶元素大的元素才能直接进栈,否则需要先将栈中比当前元素大的元素出栈,再将当前元素入栈。

这样就保证了:栈中保留的都是比当前入栈元素小的值,并且从栈顶到栈底的元素值是单调递减的。

单调递减栈的入栈、出栈过程如下:

  • 假设当前进栈元素为 x,如果 x 比栈顶元素大,则直接入栈。

  • 否则从栈顶开始遍历栈中元素,把大于 x 或者等于 x 的元素弹出栈,直到遇到一个小于 x 的元素为止,然后再把 x 压入栈中。

下面我们以数组 [4, 3, 2, 5, 7, 4, 6, 8] 为例,模拟一下「单调递减栈」的进栈、出栈过程。具体过程如下:

  • 数组元素:[4, 3, 2, 5, 7, 4, 6, 8],遍历顺序为从左到右。

第 i 步待插入元素操 作结 果(左侧为栈底)作用
144 入栈[4]元素 4 的左侧无比 4 小的元素
234 出栈,3 入栈[3]元素 3 的左侧无比 3 小的元素
323 出栈,2 入栈[2]元素 2 的左侧无比 2 小的元素
455 入栈[2, 5]元素 5 的左侧第一个比 5 小的元素是:2
577 入栈[2, 5, 7]元素 7 的左侧第一个比 7 小的元素是:5
647 出栈,5 出栈,4 入栈[2, 4]元素 4 的左侧第一个比 4 小的元素是:2
766 入栈[2, 4, 6]元素 6 的左侧第一个比 6 小的元素是:4
888 入栈[2, 4, 6, 8]元素 8 的左侧第一个比 8 小的元素是:6

最终栈中元素为 [2, 4, 6, 8]。因为从栈顶(右端)到栈底(左侧)元素的顺序为 8, 6, 4, 2,满足递减关系,所以这是一个单调递减栈。

我们以上述过程第 6 步为例,所对应的图示过程为:

2. 单调栈适用场景

单调栈可以在时间复杂度为 $O(n)$ 的情况下,求解出某个元素左边或者右边第一个比它大或者小的元素。

所以单调栈一般用于解决一下几种问题:

  • 寻找左侧第一个比当前元素大的元素。

  • 寻找左侧第一个比当前元素小的元素。

  • 寻找右侧第一个比当前元素大的元素。

  • 寻找右侧第一个比当前元素小的元素。

下面分别说一下这几种问题的求解方法。

2.1 寻找左侧第一个比当前元素大的元素

  • 从左到右遍历元素,构造单调递增栈(从栈顶到栈底递增):

    • 一个元素左侧第一个比它大的元素就是将其「插入单调递增栈」时的栈顶元素。

    • 如果插入时的栈为空,则说明左侧不存在比当前元素大的元素。

2.2 寻找左侧第一个比当前元素小的元素

  • 从左到右遍历元素,构造单调递减栈(从栈顶到栈底递减):

    • 一个元素左侧第一个比它小的元素就是将其「插入单调递减栈」时的栈顶元素。

    • 如果插入时的栈为空,则说明左侧不存在比当前元素小的元素。

2.3 寻找右侧第一个比当前元素大的元素

  • 从左到右遍历元素,构造单调递增栈(从栈顶到栈底递增):

    • 一个元素右侧第一个比它大的元素就是将其「弹出单调递增栈」时即将插入的元素。

    • 如果该元素没有被弹出栈,则说明右侧不存在比当前元素大的元素。

  • 从右到左遍历元素,构造单调递增栈(从栈顶到栈底递增):

    • 一个元素右侧第一个比它大的元素就是将其「插入单调递增栈」时的栈顶元素。

    • 如果插入时的栈为空,则说明右侧不存在比当前元素大的元素。

2.4 寻找右侧第一个比当前元素小的元素

  • 从左到右遍历元素,构造单调递减栈(从栈顶到栈底递减):

    • 一个元素右侧第一个比它小的元素就是将其「弹出单调递减栈」时即将插入的元素。

    • 如果该元素没有被弹出栈,则说明右侧不存在比当前元素小的元素。

  • 从右到左遍历元素,构造单调递减栈(从栈顶到栈底递减):

    • 一个元素右侧第一个比它小的元素就是将其「插入单调递减栈」时的栈顶元素。

    • 如果插入时的栈为空,则说明右侧不存在比当前元素小的元素。

上边的分类解法有点绕口,可以简单记为以下条规则:

  • 无论哪种题型,都建议从左到右遍历元素。

  • 查找 「比当前元素大的元素」 就用 单调递增栈,查找 「比当前元素小的元素」 就用 单调递减栈

  • 「左侧」 查找就看 「插入栈」 时的栈顶元素,从 「右侧」 查找就看 「弹出栈」 时即将插入的元素。

3. 单调栈模板

以从左到右遍历元素为例,介绍一下构造单调递增栈和单调递减栈的模板。

3.1 单调递增栈模板

def monotoneIncreasingStack(nums):
    stack = []
    for num in nums:
        while stack and num >= stack[-1]:
            stack.pop()
        stack.append(num)

3.2 单调递减栈模板

def monotoneDecreasingStack(nums):
    stack = []
    for num in nums:
        while stack and num <= stack[-1]:
            stack.pop()
        stack.append(num)

4. 单调栈的应用

4.1 下一个更大元素 I

4.1.1 题目链接

4.1.2 题目大意

给定两个没有重复元素的数组 nums1nums2 ,其中 nums1nums2 的子集。

要求:找出 nums1 中每个元素在 nums2 中的下一个比其大的值。

  • nums1 中数字 x 的下一个更大元素是指: xnums2 中对应位置的右边的第一个比 x 大的元素。如果不存在,对应位置输出 -1

4.1.3 解题思路

第一种思路是根据题意直接暴力求解。遍历 nums1 中的每一个元素。对于 nums1 的每一个元素 nums1[i],再遍历一遍 nums2,查找 nums2 中对应位置右边第一个比 nums1[i] 大的元素。这种解法的时间复杂度是 $O(n^2)$。

第二种思路是使用单调递增栈。因为 nums1nums2 的子集,所以我们可以先遍历一遍 nums2,并构造单调递增栈,求出 nums2 中每个元素右侧下一个更大的元素。然后将其存储到哈希表中。然后再遍历一遍 nums1,从哈希表中取出对应结果,存放到答案数组中。这种解法的时间复杂度是 $O(n)$。具体做法如下:

  • 使用数组 res 存放答案。使用 stack 表示单调递增栈。使用哈希表 num_map 用于存储 nums2 中下一个比当前元素大的数值,映射关系为 当前元素值:下一个比当前元素大的数值

  • 遍历数组 nums2,对于当前元素:

    • 如果当前元素值较小,则直接让当前元素值入栈。

    • 如果当前元素值较大,则一直出栈,直到当前元素值小于栈顶元素。

      • 出栈时,出栈元素是第一个大于当前元素值的元素。则将其映射到 num_map 中。

  • 遍历完数组 nums2,建立好所有元素下一个更大元素的映射关系之后,再遍历数组 nums1

  • num_map 中取出对应的值,将其加入到答案数组中。

  • 最终输出答案数组 res

4.1.4 代码

class Solution:
    def nextGreaterElement(self, nums1: List[int], nums2: List[int]) -> List[int]:
        res = []
        stack = []
        num_map = dict()
        for num in nums2:
            while stack and num > stack[-1]:
                num_map[stack[-1]] = num
                stack.pop()
            stack.append(num)
​
        for num in nums1:
            res.append(num_map.get(num, -1))
        return res

4.2 每日温度

4.2.1 题目链接

4.2.2 题目大意

描述:给定一个列表 temperaturestemperatures[i] 表示第 i 天的气温。

要求:输出一个列表,列表上每个位置代表「如果要观测到更高的气温,至少需要等待的天数」。如果之后的气温不再升高,则用 0 来代替。

说明

  • $1 \le temperatures.length \le 10^5$。

  • $30 \le temperatures[i] \le 100$。

示例

输入: temperatures = [73,74,75,71,69,72,76,73]
输出: [1,1,4,2,1,1,0,0]
​
​
输入: temperatures = [30,40,50,60]
输出: [1,1,1,0]

4.2.3 解题思路

题目的意思实际上就是给定一个数组,每个位置上有整数值。对于每个位置,在该位置右侧找到第一个比当前元素更大的元素。求「该元素」与「右侧第一个比当前元素更大的元素」之间的距离,将所有距离保存为数组返回结果。

最简单的思路是对于每个温度值,向后依次进行搜索,找到比当前温度更高的值。

更好的方式使用「单调递增栈」,栈中保存元素的下标。

思路 1:单调栈

  1. 首先,将答案数组 ans 全部赋值为 0。然后遍历数组每个位置元素。

  2. 如果栈为空,则将当前元素的下标入栈。

  3. 如果栈不为空,且当前数字大于栈顶元素对应数字,则栈顶元素出栈,并计算下标差。

  4. 此时当前元素就是栈顶元素的下一个更高值,将其下标差存入答案数组 ans 中保存起来,判断栈顶元素。

  5. 直到当前数字小于或等于栈顶元素,则停止出栈,将当前元素下标入栈。

  6. 最后输出答案数组 ans

思路 1:代码

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

思路 1:复杂度分析

  • 时间复杂度:$O(n)$。

  • 空间复杂度:$O(n)$。

  • 53
    点赞
  • 134
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 3
    评论
当使用React进行数据可视化时,可以借助一些流行的图表库来实现,例如: 1. Recharts:Recharts是一个基于React和D3的图表库,提供了多种常见的图表类型,如折线图、柱状图、饼图等。你可以使用Recharts来根据接口返回的数据生成动态的图表。 2. Chart.js:Chart.js是一个简单灵活的图表库,也可以与React结合使用。它支持多种图表类型,并提供了丰富的配置选项,可以根据接口数据自定义图表样式。 3. D3.js:D3.js是一个强大的数据可视化库,可以创建各种复杂的图表和可视化效果。虽然D3.js本身不是React专用的,但你可以通过React的生命周期方法和钩子函数来结合使用,实现与接口数据的交互。 下面是一个使用Recharts库的简单示例: ```jsx import React, { useState, useEffect } from 'react'; import { LineChart, Line, XAxis, YAxis, CartesianGrid, Tooltip, Legend } from 'recharts'; function App() { const [data, setData] = useState([]); useEffect(() => { fetchData(); }, []); const fetchData = async () => { try { const response = await fetch('https://api.example.com/data'); const result = await response.json(); setData(result); } catch (error) { console.error('Error fetching data:', error); } }; return ( <div> <LineChart width={500} height={300} data={data}> <XAxis dataKey="date" /> <YAxis /> <CartesianGrid stroke="#eee" /> <Tooltip /> <Legend /> <Line type="monotone" dataKey="value" stroke="#8884d8" /> </LineChart> </div> ); } export default App; ``` 在上面的代码中,我们使用了Recharts库来创建一个折线图。通过调用接口获取数据后,将数据传递给`LineChart`组件,然后使用`Line`组件来指定要绘制的数据线。 当然,这只是一个简单的示例,你可以根据自己的需求和实际数据进行定制和扩展。同时,你需要替换示例代码中的接口URL为你实际的接口地址,并根据接口返回的数据结构来配置图表。 希望这个示例对你有帮助!如果你有其他问题,请随时提问。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

白话机器学习

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值