一.什么是单调栈
单调栈算法是一种使用栈的数据结构来解决问题的算法。它的主要特点是栈中的元素保持单调性,即栈中的元素从栈底到栈顶是单调递增或单调递减的。
二.应用场景 单调栈可以应用于各种需要维护数据元素单调性的抽象场景。下面是一些抽象场景的例子:
- 顺序相关查询:在一个数据序列中,需要快速查询到某个元素左边或右边第一个比它大(或小)的元素。
- 区间扩展问题:寻找一个元素向左和向右扩展的最远距离,直到遇到一个更大(或更小)的元素。
- 局部极值问题:在数据序列中寻找局部最大值或最小值,单调栈可以帮助快速定位这些局部极值的位置。
- 直方图问题:在处理直方图或柱状图相关问题时,单调栈可以帮助寻找每个柱子能够扩展的最大宽度。
- 时间序列分析:在股票价格走势、气温变化等时间序列数据中,单调栈可以用来分析某个数据点前后数据的变化趋势。
- 优化性能问题:在动态规划等算法中,单调栈有时可以用来优化时间复杂度,通过维护单调性减少不必要的计算。
- 界限问题:在处理诸如接雨水、最大矩形面积等问题时,单调栈可以帮助确定元素的界限,进而计算面积或容积。
- 游戏规则应用:在某些游戏中,可能需要根据特定规则(如卡牌的大小关系)快速判断胜负或进行计分,单调栈可以应用于这种场景。
- 数据流处理:在处理实时数据流时,单调栈可以用来维护一个窗口内的数据单调性,从而进行快速的最大值或最小值查询。
- 编码优化:在编译器设计中,单调栈可能用于优化代码,如寻找可优化的代码块边界等。
单调栈的关键在于其能够保持栈内元素的单调性,这一特性使得它在以上提及的场景中能够快速提供所需的查询结果或进行相关的计算。因此,在设计算法和数据结构时,如果遇到需要处理单调性问题的场景,可以考虑是否适合使用单调栈来简化问题或提高效率。
三. leetcode实例操练
(1) leetcode739 每日温度
请根据每日 气温
列表 temperatures
,重新生成一个列表,要求其对应位置的输出为:要想观测到更高的气温,至少需要等待的天数。如果气温在这之后都不会升高,请在该位置用 0
来代替。
示例 1:
ini
输入: temperatures = [73,74,75,71,69,72,76,73]
输出: [1,1,4,2,1,1,0,0]
示例 2:
ini
输入: temperatures = [30,40,50,60]
输出: [1,1,1,0]
示例 3:
ini
输入: temperatures = [30,60,90]
输出: [1,1,0]
代码实现:
go
func dailyTemperatures(temperatures []int) []int {
if len(temperatures) == 0 {
return []int{}
}
stack := []int{0}
res := make([]int,len(temperatures))
for i := 1;i < len(temperatures);i++ {
for len(stack) > 0 && temperatures[i] > temperatures[stack[len(stack)-1]] {
res[stack[len(stack)-1]] = i-stack[len(stack)-1]
stack = stack[:len(stack)-1]
}
stack = append(stack,i)
}
return res
}
为了实现这个功能,代码使用了一个单调栈的算法,栈中保存的是温度数组的索引,并且保持这些索引在温度数组中对应的温度是单调递减的。当遍历到一个新的温度时,会不断地检查栈顶的温度。如果当前温度比栈顶的温度高,则栈顶元素对应的天数已经找到了一个更热的日子,计算出等待的天数,并将它从栈中移除。这个过程会一直重复,直到当前温度不再比栈顶温度高,或栈为空为止。然后当前日的索引被压入栈中,以便后续使用。
最后,函数返回一个等待天数的数组,为每一天提供了一个等待更高温度的天数的值。这种单调栈的方法可以有效地解决问题,因为它避免了不必要的重复比较,使得时间复杂度保持在O(n)。
(2) leetcode 84 柱状图中最大的矩形
给定 n 个非负整数,用来表示柱状图中各个柱子的高度。每个柱子彼此相邻,且宽度为 1 。
求在该柱状图中,能够勾勒出来的矩形的最大面积。
示例 1:
ini
输入: heights = [2,1,5,6,2,3]
输出: 10
解释: 最大的矩形为图中红色区域,面积为 10
示例 2:
ini
输入: heights = [2,4]
输出: 4
scss
func largestRectangleArea(heights []int) int {
maxArea := 0
stack := []int{} // 使用切片作为栈
for i, h := range heights {
// 当栈不为空且当前柱子的高度小于栈顶柱子的高度
for len(stack) > 0 && h < heights[stack[len(stack)-1]] {
// 弹出栈顶柱子,并计算以该柱子为高的最大矩形面积
top := stack[len(stack)-1]
stack = stack[:len(stack)-1]
width := i
if len(stack) > 0 {
width = i - stack[len(stack)-1] - 1
}
// 更新最大面积
maxArea = max(maxArea, heights[top]*width)
}
// 将当前柱子索引压入栈
stack = append(stack, i)
}
// 处理栈中剩余的柱子
for len(stack) > 0 {
top := stack[len(stack)-1]
stack = stack[:len(stack)-1]
width := len(heights)
if len(stack) > 0 {
width = len(heights) - stack[len(stack)-1] - 1
}
// 更新最大面积
maxArea = max(maxArea, heights[top]*width)
}
return maxArea
}
func max(x, y int) int {
if x > y {
return x
}
return y
}
解释: 代码的执行流程如下:
-
初始化:
maxArea
变量用于记录遇到的最大矩形面积。stack
切片用作栈,存储柱子的索引,而非其高度。
-
正向遍历柱状图:
- 使用一个for循环遍历输入数组
heights
中的每个柱子。 - 当栈不为空且当前柱子的高度小于栈顶柱子的高度时,这意味着我们找到了栈顶柱子的右边界(当前柱子)。
- 弹出栈顶元素(即栈中最后一个索引),这个弹出的柱子的高度将作为矩形的高。
- 计算矩形的宽度,如果栈不为空,矩形的宽度是当前索引
i
减去新的栈顶索引再减去1;如果栈为空,矩形的宽度就是当前索引i
(因为没有左边界)。 - 计算当前弹出柱子的矩形面积,并更新
maxArea
。 - 将当前柱子的索引压入栈中。
- 使用一个for循环遍历输入数组
-
处理剩余的柱子:
- 当所有柱子都遍历完毕后,栈中可能仍然有一些柱子的索引。这些柱子的右边界是柱状图的末尾。
- 继续弹出栈中的每个索引,并重复上面的面积计算步骤,直到栈为空。
-
返回结果:
- 遍历完成后,
maxArea
变量中存储的就是最大矩形面积,返回这个值作为函数结果。
- 遍历完成后,
这种方法的关键在于利用栈来跟踪可能的矩形的宽度。当栈中的某个索引被弹出时,意味着当前的柱子是它的右边界,而栈中下一个索引对应的柱子是它的左边界。这样,我们就可以计算每个可能的矩形的面积,并维护一个最大值。由于每个索引最多被压入和弹出栈一次,所以这个算法的时间复杂度是O(n)。
(3) leetcode 496 下一个更大的元素
nums1
中数字 x
的 下一个更大元素 是指 x
在 nums2
中对应位置 右侧 的 第一个 比 x
****大的元素。
给你两个 没有重复元素 的数组 nums1
和 nums2
,下标从 0 开始计数,其中nums1
是 nums2
的子集。
对于每个 0 <= i < nums1.length
,找出满足 nums1[i] == nums2[j]
的下标 j
,并且在 nums2
确定 nums2[j]
的 下一个更大元素 。如果不存在下一个更大元素,那么本次查询的答案是 -1
。
返回一个长度为 nums1.length
的数组 **ans
**作为答案,满足 **ans[i]
**是如上所述的 下一个更大元素 。
示例 1:
ini
输入: nums1 = [4,1,2], nums2 = [1,3,4,2].
输出: [-1,3,-1]
解释: nums1 中每个值的下一个更大元素如下所述:
- 4 ,用加粗斜体标识,nums2 = [1,3,4,2]。不存在下一个更大元素,所以答案是 -1 。
- 1 ,用加粗斜体标识,nums2 = [1,3,4,2]。下一个更大元素是 3 。
- 2 ,用加粗斜体标识,nums2 = [1,3,4,2]。不存在下一个更大元素,所以答案是 -1 。
示例 2:
ini
输入: nums1 = [2,4], nums2 = [1,2,3,4].
输出: [3,-1]
解释: nums1 中每个值的下一个更大元素如下所述:
- 2 ,用加粗斜体标识,nums2 = [1,2,3,4]。下一个更大元素是 3 。
- 4 ,用加粗斜体标识,nums2 = [1,2,3,4]。不存在下一个更大元素,所以答案是 -1 。
go
func nextGreaterElement(nums1 []int, nums2 []int) []int {
lookUpTable := make(map[int]int,0)
stack := make([]int,0)
for i := 0; i < len(nums2);i++ {
for len(stack)>0 && nums2[i]> nums2[stack[len(stack)-1]] {
lookUpTable[nums2[stack[len(stack)-1]]] = nums2[i]
stack = stack[:len(stack)-1]
}
stack = append(stack,i)
}
res := make([]int,len(nums1))
for i,num := range nums1 {
if biggerNum,ok := lookUpTable[num];ok {
res[i] = biggerNum
} else {
res[i] = -1
}
}
return res
}
代码的工作原理如下:
-
初始化:
lookUpTable
是一个哈希表(map),用于存储nums2
中每个元素的下一个更大元素。stack
是一个栈(用切片实现),用来存储nums2
中元素的索引,这些元素还没有找到下一个更大的元素。
-
遍历
nums2
数组:- 通过一个for循环遍历
nums2
数组的每个元素。 - 如果栈非空,且当前元素大于栈顶索引对应的元素,则说明找到了栈顶元素的下一个更大元素。
- 使用一个while循环(在Go中实现为for循环),不断弹出栈顶元素,并在
lookUpTable
中记录这些元素与它们的下一个更大元素的对应关系。 - 将当前元素的索引压入栈中。
- 通过一个for循环遍历
-
处理
nums1
数组:- 创建一个结果数组
res
,长度与nums1
相同,用于存放最终结果。 - 遍历
nums1
数组,对于每个元素,查找lookUpTable
中是否有记录的下一个更大元素。 - 如果找到,将下一个更大元素放入结果数组
res
的对应位置;如果没有找到(即ok
为false
),将-1放入结果数组res
的对应位置。
- 创建一个结果数组
-
返回结果:
- 返回结果数组
res
,它包含nums1
中每个元素在nums2
中的下一个更大元素。
- 返回结果数组
这个算法利用了单调栈的特性,通过维护一个栈来快速查找下一个更大元素。由于每个元素最多被压入和弹出栈一次,所以该算法的时间复杂度是O(n),其中n是nums2
数组的长度。