导读
本次团灭带来的是团灭单调栈问题,涉及到的题目为LeetCode 496、739、503、901、42、84、907这7道题,单调栈的思想是,保证栈内的元素是从大到小排列
,或者从小到大排列
,可以解决一类问题:距离最近的较大值问题
1.LeetCode496:下一个更大元素 I
idea:
本题采用单调栈的方法,栈内元素从底到顶是从 ⋆ \star ⋆到 ⋆ \star ⋆排列,分析:到底是从小到大还是从大到小呢?因为要找下一个更大的元素,故来了个更大的元素应该结算一下,故栈顶的元素应该是最小的,由此可知本题单调栈的顺序是从底到顶是从大到小.
本题特殊之处在于给定的数组是nums2,而要找的数在nums1里,故我们用字典记录nums2中每个元素右边更大的,然后再用nums1中的元素当key,去字典中找到想要的更大的元素.
法一:从左往右遍历数组,由前面的分析可知栈顶元素最小,当遇到较大的元素
n
u
m
s
2
[
i
]
nums2[i]
nums2[i]时,开始弹出栈顶元素,每弹出一个元素,都可以计算栈顶元素右边最大的元素了,即
n
u
m
s
2
[
i
]
nums2[i]
nums2[i],当来了一个较大的元素时,把栈里的元素一一pop出去并计算在res中的结果(pop和计算都在while循环里),重点在于滞后性
,即来了一个较大的元素
n
u
m
s
2
[
i
]
nums2[i]
nums2[i],才知道栈顶的元素它们右边较大的是谁。此方法当前元素是强势的一方,栈顶元素是弱势的一方。
/*
滞后性方法:从左往右遍历数组
*/
class Solution {
public:
vector<int> nextGreaterElement(vector<int>& nums1, vector<int>& nums2) {
// Java里:Stack<Integer> s = new Stack<>();
stack<int> s;
// Java里:Map<Integer,Integer> m = new HashMap<>();
unordered_map<int, int> m;
for (int n : nums2) { // 从左往右遍历,滞后性
while (s.size() && s.top() < n) {
m[s.top()] = n; // 注意C++中看一眼和pop是分开的
s.pop();
}
s.push(n);
}
vector<int> res; // 从字典里找nums1中的数字的结果
for (int n : nums1) res.push_back(m.count(n) ? m[n] : -1); // m.count(n)看一眼m中有无n对应的value
return res;
}
};
"""
滞后性方法:从左往右遍历数组
"""
class Solution:
def nextGreaterElement(self, nums1: List[int], nums2: List[int]) -> List[int]: # 方法1,从左往右遍历,"滞后性"
dic, stack = {}, []
# 从左往右遍历,当遇到较大值时,依次计算栈顶元素在res中的位置,分两步: 1.pop; 2.计算
for i in range(len(nums2)):
while stack and stack[-1] < nums2[i]:
dic[stack.pop()] = nums2[i]
stack.append(nums2[i])
return [dic.get(x, -1) for x in nums1]
法二:从右往左遍历数组,栈顶的元素
s
t
a
c
k
[
−
1
]
stack[-1]
stack[−1]就是镇站之宝,当有更大的元素
n
u
m
s
2
[
i
]
nums2[i]
nums2[i]进来时,把之前小的都pop出去(while循环中只是pop,不干别的),pop完后当前元素就小于此时栈顶的元素了,此时栈顶的雅座就是给当前元素准备的了,但是不着急压入栈顶,先计算当前元素
n
u
m
s
2
[
i
]
nums2[i]
nums2[i]在字典中的位置,然后再把当前元素压入栈顶,此方法重点在于即时性
,即来一个当前元素
n
u
m
s
2
[
i
]
nums2[i]
nums2[i]立马知道它右边的较大值是哪一个,此方法栈顶元素是强势的一方,当前元素是弱势的一方。
"""
即时性方法:从右往左遍历数组
"""
class Solution:
def nextGreaterElement(self, nums1: List[int], nums2: List[int]) -> List[int]: # 方法2,从右往左遍历,"即时性"
dic, stack = {}, []
# 从右往左遍历,当遇到较大值时,依次弹出栈顶的元素,把当前值放到合适的位置; 当没有较大值时,直接计算当前值对应的较大值,即栈顶元素
for i in range(len(nums2) - 1, -1, -1):
while stack and stack[-1] <= nums2[i]: # 当找到比栈顶元素更高的定海神针之后,把矮的pop出去
stack.pop() # pop完之后栈顶的雅座就是给当前元素nums2[i]准备的了,先不着急压入栈,先结算当前元素右边更大的元素,再压入栈
if stack: dic[nums2[i]] = stack[-1] # 先结算当前元素右边更大的元素
stack.append(nums2[i]) # 再把当前元素压入栈
return [dic.get(x, -1) for x in nums1]
2.LeetCode739:每日温度
法一:
因为要找"最近的较大值",故考虑用单调栈的方法,自底至顶是从大到小还是从小到大呢,因为来一个新的更大的元素就可以计算栈顶的元素与更大元素的距离了,故栈顶元素应该是最小的,栈底是最大的,本题采用"从左往右-即滞后性"方法,当来了一个新的更大的值时,栈顶的每个元素就可以挨个计算与它的距离了,因为本题要算距离,故栈里存下标更方便. 当前元素T[i]是强势的一方,栈顶元素Stack[-1]是弱势的一方
1.取数组长度并建立一个等长的结果列表;
2.for循环数组的每个元素,当当前元素T[i]大于栈顶元素时:
3.可以计算栈顶元素距离当前元素的距离了, 计算完再把栈顶元素pop出去,结算下一个栈顶元素.
4.栈顶元素在res中的位置: res[minStack[-1]],即下标; 距离: i - minStack[-1]
"""
滞后性方法:从左往右遍历数组
"""
class Solution:
def dailyTemperatures(self, T: List[int]) -> List[int]:
length = len(T)
res = [0] * length # 生成一个和元素组一样长度的数组,用来记录每个温度离它最近的较大值之间的距离
minStack = []
for i in range(length):
while(minStack and T[minStack[-1]] < T[i]): # 若当前元素T[i]大于栈顶元素时,则可以给栈顶元素安排了
res[minStack[-1]] = i - minStack[-1]
minStack.pop()
minStack.append(i)
return res
法二:
idea: 单调栈(从右往左-即时性)
本题采用从后往前遍历的方法,栈顶的元素就是定海神针,当来一个新的元素T[i]时,看它跟栈顶元素Stack.peek()的大小,若不如栈顶元素大,可以当场结算它与栈顶元素的距离,若T[i]比栈顶元素大,则把栈顶元素踢出去,由T[i]当栈顶元素
1.取数组长度,建等长的结果列表,还有minStack;
2.若当前元素T[i]小于栈顶元素minStack.peek()时,结算T[i]在res中的位置: minStack.peek() - i(注意栈顶元素下标更大),结算完把当前元素i加到栈顶;
3.若T[i]大于minStack.peek()时,由T[i]当定海神针,把小于它的栈顶元素都pop出去;
4.此方法minStack.peek()为强势的一方,T[i]为弱势的一方;
/*
即时性方法:从右往左遍历数组
*/
class Solution {
public int[] dailyTemperatures(int[] T) {
int[] res = new int[T.length];
Stack<Integer> minStack = new Stack();
for(int i = T.length - 1; i >= 0; i--){
while(!minStack.isEmpty() && T[i] >= T[minStack.peek()]){
minStack.pop();
}
res[i] = minStack.isEmpty() ? 0 : minStack.peek() - i;
minStack.push(i);
}
return res;
}
3.LeetCode503:下一个更大元素 II
idea: 单调栈(stack)
把原始数组想象成一个个人,元素的大小为身高,环形数组即把原始数组"翻倍",就是在后面再接一个原始数组,这样的话,按照之前"比身高"的流程,每个元素不仅可以比较自己右边的元素,而且也可以和左边的元素比较了.
此题采用从左往右遍历的方法,即"滞后性"方法:
如果当前元素大于栈顶元素,则依次计算栈顶的元素在res中的位置(本题用res返回结果,这是一般情况,上一题用字典返回属于特殊情况);
如果当前元素小于栈顶元素,则把它加入栈中.
"""
滞后性方法:从左往右遍历数组
"""
class Solution:
def nextGreaterElements(self, nums: List[int]) -> List[int]:
nums = nums * 2 # 原数组扩大一倍
stack = []
res = [-1] * len(nums)
for idx, num in enumerate(nums):
while stack and nums[stack[-1]] < num: # 当前元素大于栈顶元素了,依次计算栈顶元素
res[stack.pop()] = num # 分两步:1.pop出去; 2.计算在res中的位置
stack.append(idx) # 不管是从左往右的"滞后性"还是从右往左的"即时性"最后都要把当前值压入栈
return res[:len(nums) // 2] # 只返回res前半部分的值
4.LeetCode901:股票价格跨度
idea:
额…从左往右的即时性法,因为要找到某天价格price左边第一个比它大的,并且输出之间相差的天数,遇到比当前价格price还小的价格一律pop出去,直到找到比price大的栈顶元素.因为是"即时性"法,故栈顶元素是定海神针,(如果当前价格price上来就比Stack[-1]小,就可以直接结算)
因为本题要定义一个函数可以立马得到某个价格price之前比它小的天数,故不能用for循环遍历数组每个元素了,也不能把下标i放进栈里了,故定义两个栈,一个记录价格,一个记录价格对应天数
/*
即时性方法:从左往右遍历数组
*/
class StockSpanner {
private:
stack<int>st_price;
stack<int>st_day;
int cur_day = 0; // 用其记录现在是第几天了,100是第一天,80是第二天...
public:
StockSpanner() {
}
int next(int price) { // 输入价格,就可以得到之前几天比这个价格低的
cur_day++;
int res = 0; // 输出相差的天数
while (!st_price.empty()&& st_price.top() <= price)
{
st_price.pop();
st_day.pop();
}
// pop完后栈顶的元素就比当前的大了,输出之间的差值
if (!st_price.empty())
res = cur_day - st_day.top();
else // 如果当前的价格太大以至于把所有的栈里的价格都踢出去了,它的当前天数cur_day就是相差的天数了.
res = cur_day;
st_price.push(price);
st_day.push(cur_day);
return res;
}
};
5.LeetCode42:接雨水
idea: 栈(Stack)
本题采用从左往右遍历的方法,即滞后性,当有一个新的更大值来的时候,分别结算栈顶的若干个元素能盛水的面积.计算的时候比之前单调栈的要复杂,因为它要用到如下几个元素:
1.首先是宽:当来了一个更大的元素height[current]时,把栈顶的元素pop出去(height[stack.peek()]),得到宽current - stack.peek() - 1; (其中stack.peek是新的栈顶值)
2.其次是高:当把栈顶的元素pop出去要计算它上面能盛水的面积时,还一定要比较当前新的高height[current]和pop出去之后留下的栈顶的旧的高哪个小,用小的当做此时的高,并且还要减去pop出去的栈顶值h之差,才是实际能盛水的高.
/*
滞后性方法:从左往右遍历
*/
class Solution {
public int trap(int[] height) {
int sum = 0;
Stack <Integer> stack = new Stack<>();
int current = 0;
while (current < height.length) { // 也可以用for循环遍历数组
while (!stack.empty() && height[current] > height[stack.peek()]) { // 当有新的高时,开始一一结算栈顶的值,滞后性
int h = height[stack.pop()]; // 首先得到栈顶值的实际高度,它上面就是可以盛水的面积
// pop出去栈顶值后,栈顶的新值stack.peek()就是左边界,height[current]就是右边界
if (stack.empty()) {
break;
}
// 用新的右边界(即当前高度current)和新的左边界(pop出去后的新栈顶元素)计算可以盛水的宽
int width = current - stack.peek() - 1;
// 看一下新的右边界和新的左边界哪个更低(根据木桶原理即能盛水的高)
int min_height = Math.min(height[stack.peek()], height[current]);
// 不要忘了减去当前要计算盛水的本来的高(因为在它上面才能盛水)
sum = sum + width * (min_height - h);
}
// height[current]当作强势的一方,栈顶元素是弱势的一方,都结算完之后,把它也压到栈里
stack.push(current);
current++;
}
return sum;
}
}
/*
滞后性方法:从左往右遍历
*/
class Solution:
def trap(self, height: List[int]) -> int:
# 建立栈和累加盛水的整形变量sumWater
stack = []
sumWater = 0
# 外圈for循环或while循环,遍历整个数组
for current in range(0, len(height)): # 也可以用while循环和current指针
# 滞后性方法,当前高度为强势一方,栈顶元素为弱势一方,一一弹出栈顶元素结算
while stack and height[current] > height[stack[-1]]:
h = stack.pop() # 当前要计算盛水格子的高度
# 弹完没有左边界了
if not len(stack):
break
# 计算宽度:用新的右边界(即当前高度current)和新的左边界(pop出去后的新栈顶元素)计算可以盛水的宽
width = current - stack[-1] - 1
# 看一下新的右边界和新的左边界哪个更低(根据木桶原理即能盛水的高)
min_height = min(height[current], height[stack[-1]])
sumWater += width * (min_height - height[h]) # 不要忘了减去当前要计算盛水的本来的高(因为在它上面才能盛水)
stack.append(current)
return sumWater