单调栈
什么时候用?
通常是一维数组,要寻找任一个元素的右边或者左边第一个比自己大或者小的元素的位置。
是什么?
本质是空间换时间,因为在遍历的过程中需要用一个栈来记录右边第一个比当前元素的元素,优点是只需要遍历一次。时间复杂度是O(n)
存什么?
存放元素的下标i,栈头到栈底的顺序
,递增循序
739. 每日温度
参考:https://www.programmercarl.com/0739.%E6%AF%8F%E6%97%A5%E6%B8%A9%E5%BA%A6.html#%E6%80%9D%E8%B7%AF
情况一:当前遍历的元素T[i]小于栈顶元素T[st.top()]的情况,入栈
情况二:当前遍历的元素T[i]等于栈顶元素T[st.top()]的情况,入栈
情况三:当前遍历的元素T[i]大于栈顶元素T[st.top()]的情况,一直出栈到保持【栈头到栈底是递增(中间允许有相同的数)】这一条件
所以可以把情况一和二合并。
class Solution(object):
def dailyTemperatures(self, temperatures):
"""
:type temperatures: List[int]
:rtype: List[int]
"""
ans = [0]*len(temperatures)
stack = [0]
for i in range(1, len(temperatures)):
# 栈存的是下标
# 情况一和二 当前元素小于等于栈顶元素 入栈
if temperatures[i] <= temperatures[stack[-1]]:
stack.append(i)
#情况三 当前元素大于栈顶元素 出栈 直到保持递增条件
else:
while len(stack) != 0 and temperatures[i] > temperatures[stack[-1]]:
# 更新ans 详细看图解
ans[stack[-1]] = i - stack[-1]
stack.pop()
stack.append(i)
return ans
496 下一个更大元素 I
https://leetcode.cn/problems/next-greater-element-i/solution/zhan-xia-yi-ge-geng-da-yuan-su-i-by-demi-cumj/
参考,顶一个map key和value分别:当前元素和大于key 的下一个元素,如果不存在大于key 的下一个元素定义为-1
class Solution(object):
def nextGreaterElement(self, nums1, nums2):
"""
:type nums1: List[int]
:type nums2: List[int]
:rtype: List[int]
"""
# 这里stack存的是具体的值 也可以存下标
stack = []
map = {key:-1 for key in nums2}
# key存的是当前元素 value存的是大于当前元素的下一个元素 所以初始化为-1
for i in nums2:
# 当前元素大于栈顶元素 更新map 栈顶元素出栈
while stack and i > stack[-1]:
key = stack[-1]
map[key] = i
stack.pop()
# 其他情况下 当前元素入栈
stack.append(i)
res = []
for j in nums1:
res.append(map[j])
return res
503. 下一个更大元素 II
思路:与上一题类似,但是是循环数组,可以用%来模拟
class Solution(object):
def nextGreaterElements(self, nums):
"""
:type nums: List[int]
:rtype: List[int]
"""
n = len(nums)
res = [-1] * n
stack = [] #存的是数组下标
for i in range(2 * n):
# 当前值大于栈顶元素 更新栈顶元素下标的res值 并出栈
while stack and nums[i % n] > nums[stack [-1]]:
res[stack[-1]] = nums[i % n]
stack.pop()
stack.append(i % n)
return res
42接雨水![在这里插入图片描述](https://img-blog.csdnimg.cn/5b0dfae09c3546e0b8820fa7b3eca1bc.png)
参考https://www.programmercarl.com/0042.%E6%8E%A5%E9%9B%A8%E6%B0%B4.html#%E5%8F%8C%E6%8C%87%E9%92%88%E8%A7%A3%E6%B3%95
双指针 o(n^2) 超时
class Solution(object):
def trap(self, height):
"""
:type height: List[int]
:rtype: int
"""
sum = 0
# 双指针 对于位置i的列 找他左右最高的列的高度
for i in range(len(height)):
# 第一个柱子和最后一个柱子不接雨水
if i == 0 or i == len(height) - 1:continue
# 找i右边的最高的列的值
rh = max(height[i+1:len(height)])
# 找i左右的最高的列的值 i取不到 所以是0~i-1
lh = max(height[0:i])
# 高度差 把每一列的高度差求和 就是最后结果
h = min(rh, lh) - height[i]
if h > 0: sum += h
return sum
动态规划
当前列雨水面积:min(左边柱子的最高高度,记录右边柱子的最高高度) - 当前柱子高度。
状态方程:当前位置,左边的最高高度是前一个位置的左边最高高度和本高度的最大值。
即从左向右遍历:maxLeft[i] = max(height[i], maxLeft[i - 1]);
从右向左遍历:maxRight[i] = max(height[i], maxRight[i + 1]);
class Solution(object):
def trap(self, height):
"""
:type height: List[int]
:rtype: int
"""
maxleft = [0] * len(height)
maxright = [0] * len(height)
# 从左向右遍历 找maxleft 记录每个柱子左边柱子最大高度
maxleft[0] = height[0]
for i in range(1,len(height)):
maxleft[i] = max(height[i], maxleft[i - 1])
# 从右向左遍历 找maxright 记录每个柱子右边柱子最大高度
maxright[-1] = height[-1]
for i in range(len(height)-2, -1, -1):
maxright[i] = max(height[i], maxright[i + 1])
sum = 0
for i in range(len(height)):
h = min(maxright[i], maxleft[i]) - height[i]
if h > 0 :
sum += h
return sum
单调栈
一旦发现添加的柱子高度大于栈头元素了,此时就出现凹槽了,
栈头
元素就是凹槽底部
的柱子,栈头第二个元素就是凹槽左边的柱子
,而添加的元素就是凹槽右边的柱子
。
遇到相同元素?
如果添加第二个5的时候就应该将第一个5的下标弹出,把第二个5添加到栈中。
因为我们要求宽度的时候 如果遇到相同高度的柱子,需要使用最右边的柱子来计算宽度。
栈存的是下标
ps :
与之前不同的是情况1,2(当前元素<=栈顶元素)可以合并处理,都入栈。但是,在这里当遇到当前元素==栈顶元素时,需要先将栈顶元素弹出,再入栈
栈顶(凹槽)和栈顶的后一个元素(左边柱子)以及要入栈元素(右边柱子)的三个元素来接水!
那么雨水高度
是 min(凹槽左边高度, 凹槽右边高度) - 凹槽底部高度
,
代码为:int h = min(height[st.top()], height[i]) - height[mid];
雨水的宽度
是 凹槽右边的下标 - 凹槽左边的下标 - 1
(因为只求中间宽度),代码为:int w = i - st.top() - 1 ;
当前凹槽雨水的体积就是:h * w
class Solution:
def trap(self, height: List[int]) -> int:
'''
单调栈是按照 行 的方向来计算雨水
从栈顶到栈底的顺序:从小到大
通过三个元素来接水:栈顶,栈顶的下一个元素,以及即将入栈的元素
雨水高度是 min(凹槽左边高度, 凹槽右边高度) - 凹槽底部高度
雨水的宽度是 凹槽右边的下标 - 凹槽左边的下标 - 1(因为只求中间宽度)
'''
res = 0
stack = [0]# 存的是下标
for i in range(1,len(height)):
# 情况一
if height[i] < height[stack[-1]]:
stack.append(i)
# 情况二
# 当当前柱子高度和栈顶一致时,左边的一个是不可能存放雨水的,所以保留右侧新柱子
# 需要使用最右边的柱子来计算宽度
elif height[i] == height[stack[-1]]:
stack.pop()
stack.append(i)
# 情况三 抛出所有较低的柱子
else:
while stack and height[i] > height[stack[-1]]:
# 栈顶就是中间的柱子:储水槽,就是凹槽的地步
mid = height[stack[-1]]
stack.pop()
if stack:
right = height[i]
left = height[stack[-1]]
# 两侧的较矮一方的高度 - 凹槽底部高度
h = min(left, right) - mid
# 凹槽右侧下标 - 凹槽左侧下标 - 1: 只求中间宽度
w = i - stack[-1] - 1
# 体积:高乘宽
res += h * w
stack.append(i)
return res
# 单调栈压缩版
class Solution:
def trap(self, height: List[int]) -> int:
stack = [0]
result = 0
for i in range(1, len(height)):
while stack and height[i] > height[stack[-1]]:
mid_height = stack.pop()
if stack:
# 雨水高度是 min(凹槽左侧高度, 凹槽右侧高度) - 凹槽底部高度
h = min(height[stack[-1]], height[i]) - height[mid_height]
# 雨水宽度是 凹槽右侧的下标 - 凹槽左侧的下标 - 1
w = i - stack[-1] - 1
# 累计总雨水体积
result += h * w
stack.append(i)
return result
84.柱状图中最大的矩形
思路:找到i左边第一个高度小于height[i]的下标l,和右边第一个高度小于height[i]的下标r,高是height[i],宽是r-l-1
双指针 超时了
class Solution(object):
def largestRectangleArea(self, heights):
"""
:type heights: List[int]
:rtype: int
"""
# 从左向右遍历:以每一根柱子为主心骨(当前轮最高的参照物),迭代直到找到左侧和右侧各第一个矮一级的柱子
res = 0
for i in range(len(heights)):
left = i
right = i
# 找左边第一个矮的
while left >=0:
if heights[left] < heights[i]:
break
left -= 1
# 找右边第一个矮的
while right < len(heights):
if heights[right] < heights[i]:
break
right += 1
w = right - left - 1
h = heights[i]
res = max(res, w * h)
return res
动态规划
# DP动态规划
class Solution:
def largestRectangleArea(self, heights: List[int]) -> int:
size = len(heights)
# 两个DP数列储存的均是下标index
min_left_index = [0] * size
min_right_index = [0] * size
result = 0
# 记录每个柱子的左侧第一个矮一级的柱子的下标
min_left_index[0] = -1 # 初始化防止while死循环
for i in range(1, size):
# 以当前柱子为主心骨,向左迭代寻找次级柱子
temp = i - 1
while temp >= 0 and heights[temp] >= heights[i]:
# 当左侧的柱子持续较高时,尝试这个高柱子自己的次级柱子(DP
temp = min_left_index[temp]
# 当找到左侧矮一级的目标柱子时
min_left_index[i] = temp
# 记录每个柱子的右侧第一个矮一级的柱子的下标
min_right_index[size-1] = size # 初始化防止while死循环
for i in range(size-2, -1, -1):
# 以当前柱子为主心骨,向右迭代寻找次级柱子
temp = i + 1
while temp < size and heights[temp] >= heights[i]:
# 当右侧的柱子持续较高时,尝试这个高柱子自己的次级柱子(DP
temp = min_right_index[temp]
# 当找到右侧矮一级的目标柱子时
min_right_index[i] = temp
for i in range(size):
area = heights[i] * (min_right_index[i] - min_left_index[i] - 1)
result = max(area, result)
return result
单调栈
参考https://www.programmercarl.com/0084.%E6%9F%B1%E7%8A%B6%E5%9B%BE%E4%B8%AD%E6%9C%80%E5%A4%A7%E7%9A%84%E7%9F%A9%E5%BD%A2.html#%E5%8D%95%E8%B0%83%E6%A0%88
接雨水 (opens new window)中单调栈从栈头(元素从栈头弹出)到栈底的顺序应该是从小到大
的顺序。
本题是要找每个柱子左右两边第一个小于该柱子
的柱子,所以从栈头(元素从栈头弹出)到栈底的顺序应该是从大到小的顺序!与接雨水相反
只有栈里从大到小的顺序,才能保证栈顶元素找到左右两边第一个小于栈顶元素的柱子。
栈顶(mid)和栈顶的下一个元素(left)以及要入栈(right)
的三个元素组成了我们要求最大面积的高度和宽度
class Solution:
def largestRectangleArea(self, heights: List[int]) -> int:
'''
找每个柱子左右侧的第一个高度值小于该柱子的柱子
单调栈:栈顶到栈底:从大到小(每插入一个新的小数值时,都要弹出先前的大数值)
栈顶,栈顶的下一个元素,即将入栈的元素:这三个元素组成了最大面积的高度和宽度
情况一:当前遍历的元素heights[i]大于栈顶元素的情况
情况二:当前遍历的元素heights[i]等于栈顶元素的情况
情况三:当前遍历的元素heights[i]小于栈顶元素的情况
一二可以合并
输入数组首尾各补上一个0(与42.接雨水不同的是,本题原首尾的两个柱子可以作为核心柱进行最大面积尝试
'''
heights.insert(0, 0)
heights.append(0)
res = 0
stack = [0] # 保存的是下标
for i in range(1, len(heights)):
# 情况1
if heights[i] > heights[stack[-1]]:
stack.append(i)
# 情况2
elif heights[i] == heights[stack[-1]]:
stack.pop()
stack.append(i)
# 情况3 抛出所有较高的柱子
else:
while stack and heights[i] < heights[stack[-1]]:
# 栈顶元素就是中间的柱子 主心骨
mid_index = stack[-1]
stack.pop()
if stack:
left_index = stack[-1]
right_index = i
w = right_index - left_index -1
h = heights[mid_index]
res = max(res, w * h)
stack.append(i)
return res
# 单调栈精简
class Solution:
def largestRectangleArea(self, heights: List[int]) -> int:
heights.insert(0, 0)
heights.append(0)
stack = [0]
result = 0
for i in range(1, len(heights)):
while stack and heights[i] < heights[stack[-1]]:
mid_height = heights[stack[-1]]
stack.pop()
if stack:
# area = width * height
area = (i - stack[-1] - 1) * mid_height
result = max(area, result)
stack.append(i)
return result