代码随想录第39天:单调栈
一、每日温度(Leetcode 739)
思路:
-
栈里存放的是**“还没等到升温的日子”**的索引;
-
每遇到一个新的温度:
-
检查是否比栈顶的温度高;
-
如果高了,说明升温来了,栈顶元素可以出栈,并计算等待天数;
-
-
一旦栈为空或当前温度不高于栈顶,就入栈。
class Solution:def dailyTemperatures(self, temperatures: List[int]) -> List[int]:n = len(temperatures)res = [0] * n # 初始化结果数组stack = [] # 存放的是索引,栈中温度递减for i in range(n):# 如果当前温度高于栈顶(之前某一天)的温度while stack and temperatures[i] > temperatures[stack[-1]]:prev_index = stack.pop()res[prev_index] = i - prev_index # 计算天数差stack.append(i) # 当前索引入栈return res
二、下一个更大元素 I(Leetcode 496)
思路:
-
用单调栈求出
nums2
中每个元素右边第一个更大的数。 -
用字典
greater_map
存储这个映射关系:{元素: 它右边第一个更大的元素}
。 -
最后,对
nums1
中的每个数,从字典中查找答案,不存在则返回-1
。
class Solution:def nextGreaterElement(self, nums1: List[int], nums2: List[int]) -> List[int]:stack = []greater_map = {}for num in nums2:# 栈中元素递减,当前元素比栈顶大 -> 出栈并记录while stack and num > stack[-1]:prev = stack.pop()greater_map[prev] = numstack.append(num)# 栈中剩下的元素右边没有更大值,默认是 -1return [greater_map.get(x, -1) for x in nums1]
三、下一个更大元素 II (Leetcode 503)
思路:
我们可以将数组遍历两遍(通过 %
模拟循环),来处理尾部比头部大的情况:
-
用
i % n
实现数组循环; -
只在第一轮把索引入栈,防止重复处理;
-
第二轮仅用来帮助“解决前面的元素右边没人比它大的问题”。
class Solution:def nextGreaterElements(self, nums: List[int]) -> List[int]:n = len(nums)res = [-1] * n # 初始化结果为 -1stack = []# 遍历两轮数组for i in range(2 * n):num = nums[i % n]while stack and nums[stack[-1]] < num:index = stack.pop()res[index] = numif i < n:stack.append(i) # 只在第一轮入栈return res
四、接雨水(Leetcode 42)
1.动态规划:
-
当前格子能接多少水」取决于它左边和右边的最大高度中的较小值。
-
预先记录每个位置左侧和右侧最大高度
-
再计算每个位置能接多少水
class Solution:def trap(self, height: List[int]) -> int:n = len(height)if n == 0:return 0 # 边界处理:空数组无法存水# 初始化左右最大高度数组left_max = [0] * nright_max = [0] * n# 计算每个位置左边(包括自己)最高的柱子高度left_max[0] = height[0]for i in range(1, n):# 当前左最大高度 = 前一个位置的左最大高度 或 当前高度,取较大者left_max[i] = max(left_max[i - 1], height[i])# 计算每个位置右边(包括自己)最高的柱子高度right_max[-1] = height[-1]for i in range(n - 2, -1, -1):# 当前右最大高度 = 后一个位置的右最大高度 或 当前高度,取较大者right_max[i] = max(right_max[i + 1], height[i])# 遍历每个位置,计算该位置可接的水量res = 0for i in range(n):# 当前柱子最多能接的水 = 左右最大高度的较小值 - 当前高度# 如果左右最大高度都大于当前高度,才有水;否则为0res += min(left_max[i], right_max[i]) - height[i]return res # 返回总的接水量
2.单调栈:
-
遇到比栈顶高的柱子,说明可以接水,弹出栈顶,计算面积。
-
左边界:栈顶弹出后的新栈顶
-
右边界:当前元素
-
宽度 =
右边 - 左边 - 1
,高度 =min(height[left], height[right]) - height[中间]
class Solution:def trap(self, height: List[int]) -> int:stack = [] # 单调递减栈,存的是柱子的索引res = 0 # 累加接的水量for i, h in enumerate(height): # 遍历每个位置的柱子高度,i 是当前柱子的索引# 当前高度 h 比栈顶柱子高,说明可以形成“凹槽”接水while stack and h > height[stack[-1]]:bottom = stack.pop() # 凹槽的底部索引if not stack:break # 栈空了说明左边没墙,不能接水left = stack[-1] # 左边高墙的索引width = i - left - 1 # 宽度 = 当前右墙索引 i - 左墙索引 left - 1bounded_height = min(height[left], h) - height[bottom] # 高度 = 两边较矮墙 - 底部高度res += width * bounded_height # 当前“凹槽”能装的水stack.append(i) # 当前柱子索引入栈,可能成为新的“左墙”或“底部”return res
3.双指针:
-
左右指针从两端向中间移动。
-
维护两个变量:
left_max
和right_max
,分别表示左侧和右侧最高的墙。 -
某位置的接水量取决于
min(left_max, right_max) - height[i]
。
class Solution:def trap(self, height: List[int]) -> int:if not height:return 0 # 空数组无法接水,直接返回0left, right = 0, len(height) - 1 # 左右指针left_max, right_max = 0, 0 # 记录左右两侧最大高度res = 0 # 存储总共接到的水量while left < right:# 谁小就移动谁,决定接水能力的是较矮一方if height[left] < height[right]:if height[left] >= left_max:left_max = height[left] # 更新左侧最大值else:res += left_max - height[left] # 当前柱子能接的水 = 左侧最大高度 - 当前高度left += 1 # 左指针右移else:if height[right] >= right_max:right_max = height[right] # 更新右侧最大值else:res += right_max - height[right] # 当前柱子能接的水 = 右侧最大高度 - 当前高度right -= 1 # 右指针左移return res
-
总是处理 较矮的一侧,因为它才是限制水位高度的关键。
-
不需要额外数组,只用两个指针与两个变量记录左右最大值,节省空间,时间复杂度
O(n)
,空间复杂度O(1)
。
五、柱状图中最大的矩形(Leetcode 84)
单调栈:
-
对每个柱子,找到左边第一个小于它的柱子
left[i]
-
找到右边第一个小于它的柱子
right[i]
-
宽度为
right[i] - left[i] - 1
class Solution:def largestRectangleArea(self, heights: List[int]) -> int:n = len(heights)left = [0] * n # 记录每个柱子左边第一个比它矮的位置索引right = [n] * n # 记录每个柱子右边第一个比它矮的位置索引stack = []# 从左到右遍历,计算 left[i]for i in range(n):# 栈中维护单调递增的柱子索引while stack and heights[stack[-1]] >= heights[i]:stack.pop()# 如果栈为空,说明左边没有比当前柱子矮的了left[i] = stack[-1] if stack else -1stack.append(i)# 清空栈用于接下来的右边界计算stack.clear()# 从右到左遍历,计算 right[i]for i in range(n - 1, -1, -1):# 栈中维护单调递增的柱子索引while stack and heights[stack[-1]] >= heights[i]:stack.pop()# 如果栈为空,说明右边没有比当前柱子矮的了right[i] = stack[-1] if stack else nstack.append(i)max_area = 0for i in range(n):# 当前柱子能延展的宽度 = right[i] - left[i] - 1width = right[i] - left[i] - 1# 面积 = 高度 × 宽度max_area = max(max_area, width * heights[i])return max_area