单调栈思想及解题分析:
概念:
单调栈(Monotone Stack):一种特殊的栈。在栈的「先进后出」规则基础上,要求「从 栈顶 到 栈底 的元素是单调递增(或者单调递减)」。其中满足从栈顶到栈底的元素是单调递增的栈,叫做「单调递增栈」。满足从栈顶到栈底的元素是单调递减的栈,叫做「单调递减栈」。
单调递增栈:只有比栈顶元素小的元素才能直接进栈,否则需要先将栈中比当前元素小的元素出栈,再将当前元素入栈。
这样就保证了:栈中保留的都是比当前入栈元素大的值,并且从栈顶到栈底的元素值是单调递增的。
单调递减栈:只有比栈顶元素大的元素才能直接进栈,否则需要先将栈中比当前元素大的元素出栈,再将当前元素入栈。
这样就保证了:栈中保留的都是比当前入栈元素小的值,并且从栈顶到栈底的元素值是单调递减的。
单调栈适用场景
单调栈可以在时间复杂度为 O(n) 的情况下,求解出某个元素左边或者右边第一个比它大或者小的元素。
所以单调栈一般用于解决一下几种问题:
寻找左侧第一个比当前元素大的元素。寻找左侧第一个比当前元素小的元素。
寻找右侧第一个比当前元素大的元素。寻找右侧第一个比当前元素小的元素。
具体求解方法:
1 寻找左侧第一个比当前元素大的元素
从左到右遍历元素,构造单调递增栈(从栈顶到栈底递增):
一个元素左侧第一个比它大的元素就是将其「插入单调递增栈」时的栈顶元素。
如果插入时的栈为空,则说明左侧不存在比当前元素大的元素。
2 寻找左侧第一个比当前元素小的元素
从左到右遍历元素,构造单调递减栈(从栈顶到栈底递减):
一个元素左侧第一个比它小的元素就是将其「插入单调递减栈」时的栈顶元素。
如果插入时的栈为空,则说明左侧不存在比当前元素小的元素。
3 寻找右侧第一个比当前元素大的元素
从左到右遍历元素,构造单调递增栈(从栈顶到栈底递增):
一个元素右侧第一个比它大的元素就是将其「弹出单调递增栈」时即将插入的元素。
如果该元素没有被弹出栈,则说明右侧不存在比当前元素大的元素。
从右到左遍历元素,构造单调递增栈(从栈顶到栈底递增):
一个元素右侧第一个比它大的元素就是将其「插入单调递增栈」时的栈顶元素。
如果插入时的栈为空,则说明右侧不存在比当前元素大的元素。
4 寻找右侧第一个比当前元素小的元素
从左到右遍历元素,构造单调递减栈(从栈顶到栈底递减):
一个元素右侧第一个比它小的元素就是将其「弹出单调递减栈」时即将插入的元素。
如果该元素没有被弹出栈,则说明右侧不存在比当前元素小的元素。
从右到左遍历元素,构造单调递减栈(从栈顶到栈底递减):
一个元素右侧第一个比它小的元素就是将其「插入单调递减栈」时的栈顶元素。
如果插入时的栈为空,则说明右侧不存在比当前元素小的元素。
————————————————
以上内容参考原文链接:https://blog.youkuaiyun.com/zy_dreamer/article/details/131036101
经典例题:
1475. 商品折扣后的最终价格 (简单)
496. 下一个更大元素 I (简单)
739. 每日温度 (中等)
503. 下一个更大元素 II (中等)
239. 滑动窗口最大值 (困难)
42. 接雨水 (困难)
84. 柱状图中最大的矩形(困难)
解答:(个人感觉倒序遍历方式简单一点)
简单:
1475. 商品折扣后的最终价格
倒序遍历:stack中储存右边第一个最大值
class Solution {//java
public int[] finalPrices(int[] prices) {
Stack<Integer> st = new Stack();
int[] res = new int[prices.length];
for (int i = prices.length - 1; i >= 0; i--) {
while (!st.isEmpty() && st.peek() > prices[i]) {
st.pop();
}
res[i] = st.isEmpty() ? prices[i] : prices[i] - st.peek();
st.push(prices[i]);
}
return res;
}
}
正序遍历:栈中储存下标
class Solution {//java
public int[] finalPrices(int[] prices) {
Stack<Integer> st = new Stack();
int[] res = new int[prices.length];
for (int i = 0; i < prices.length; i++) {
while (!st.isEmpty() && prices[i] <= prices[st.peek()]) {
res[st.peek()] = prices[i];//先记录每件商品折扣
st.pop();
}
st.push(i);
}
//计算折扣之后的价格
for (int i = 0; i < prices.length; i++) {
res[i] = prices[i] - res[i];
}
return res;
}
}
496. 下一个更大元素 I
思想:单调栈+哈希
倒序遍历:stack中储存右边第一个最大值
class Solution {//java
public int[] nextGreaterElement(int[] nums1, int[] nums2) {
Map<Integer, Integer> map = new HashMap<Integer, Integer>();//hash储存对应元素及其右边第一个更大的值
Stack<Integer> st = new Stack();
for (int i = nums2.length - 1; i >= 0; i--) {
int num = nums2[i];
while (!st.isEmpty() && num >= st.peek()) {
st.pop();
}
map.put(num, st.isEmpty() ? -1 : st.peek());
st.push(num);
}
int[] res = new int[nums1.length];
for (int i = 0; i < nums1.length; i++) {
res[i] = map.get(nums1[i]);
}
return res;
}
}
正序遍历:栈中储存下标
class Solution {//c++
public:
vector<int> nextGreaterElement(vector<int>& nums1, vector<int>& nums2) {
stack<int> st;
vector<int> res(nums1.size(), -1);
unordered_map<int, int> umap;//key:下标元素,value:下标
for (int i = 0; i < nums1.size(); i++) {
umap[nums1[i]] = i;
}
for (int i = 0; i < nums2.size(); i++) {
while (!st.empty() && nums2[i] > nums2[st.top()]) {//找到右侧第一个最大值下标
if (umap.find(nums2[st.top()]) != umap.end()) {//找到栈口在Nums1中
int index = umap.find(nums2[st.top()])->second;
res[index] = nums2[i];
}
st.pop();
}
st.push(i);//栈里存放的是下标
}
return res;
}
};
中等:
739. 每日温度
倒序遍历:栈中存放右边第一个最大值下标方便计算天数
class Solution {//java
public int[] dailyTemperatures(int[] temperatures) {
Stack<Integer> st = new Stack();
int[] res = new int[temperatures.length];
for (int i = temperatures.length - 1; i >= 0; i--) {
while(!st.isEmpty() && temperatures[st.peek()] <= temperatures[i]) {
st.pop();
}
if (!st.isEmpty()) {
res[i] = st.peek() - i;
} else {
res[i] = 0;
}
st.push(i);
}
return res;
}
}
正序遍历:栈中储存下标
class Solution {//java
public int[] dailyTemperatures(int[] temperatures) {
Stack<Integer> st = new Stack();
int[] res = new int[temperatures.length];//栈初始即为0
for (int i = 0; i < temperatures.length; i++) {
//栈里存放遍历过的元素下标
while (!st.isEmpty() && temperatures[i] > temperatures[st.peek()]) {
res[st.peek()] = i - st.peek();
st.pop();
}
st.push(i);
}
return res;
}
}
503. 下一个更大元素 II
本题给定循环数组判断下一个最大元素,只需将循环数组拉直再扩充一倍即可判断,操作时只需在处理时对下标取模即可。
倒序遍历:栈中存放右边第一个最大值
class Solution {//java
public int[] nextGreaterElements(int[] nums) {
int n = nums.length;
int[] res = new int[n];
Stack<Integer> st = new Stack();
for (int i = 2 * n - 1; i >= 0; i--) {
while (!st.isEmpty() && nums[i % n] >= st.peek()) {
st.pop();
}
res[i % n] = st.isEmpty() ? -1 : st.peek();
st.push(nums[i % n]);
}
return res;
}
}
正序遍历:栈中储存下标
class Solution {//c++
public:
vector<int> nextGreaterElements(vector<int>& nums) {
vector<int> res(nums.size(), -1);
stack<int> st;
for (int i = 1; i < nums.size() * 2; i++) {
//模拟遍历两遍nums,注意一下都是用i % nums.size()来操作
while (!st.empty() && nums[i % nums.size()] > nums[st.top()]) {
res[st.top()] = nums[i % nums.size()];
st.pop();
}
st.push(i % nums.size());
}
return res;
}
};
class Solution {//java
public int[] nextGreaterElements(int[] nums) {
int n = nums.length;
int[] ret = new int[n];
Arrays.fill(ret, -1);//此方法给数组复制操作需记住
Deque<Integer> stack = new LinkedList<Integer>();
for (int i = 0; i < n * 2 - 1; i++) {
while (!stack.isEmpty() && nums[stack.peek()] < nums[i % n]) {
ret[stack.pop()] = nums[i % n];
}
stack.push(i % n);
}
return ret;
}
}
困难:
239. 滑动窗口最大值
思想:单调队列,双端队列(队列头部储存当前最大值下标,且头部下标最小)
class Solution {//java
public int[] maxSlidingWindow(int[] nums, int k) {
Deque<Integer> dq = new LinkedList<>();//双端队列
int[] res = new int[nums.length - k + 1];
int j = 0;
for (int i = 0; i < nums.length; i++) {
while (!dq.isEmpty() && nums[i] >= dq.peekLast()) {
dq.pollLast();
}
dq.offer(i);//储存最大值下标,在队列头部,其下标也最小
if (i - dq.peek() >= k) {
//队列中元素多了说明队头元素不在滑动窗口范围内了
dq.pollLast();
}
if (i + 1 >= k) {
res[j++] = nums[dq.peek()];
}
}
return res;
}
}
42. 接雨水
单调栈:维护一个单调栈,单调栈存储的是下标,满足从栈底到栈顶的下标对应的数组中的元素递减。
class Solution {
public int trap(int[] height) {
int res = 0;
Stack<Integer> st = new Stack();
int n = height.length;
for (int i = 0; i < n; i++) {
while (!st.isEmpty() && height[i] > height[st.peek()]) {
int top = st.pop();
if (st.isEmpty()) {
break;
}
int left = st.peek();
int curW = i - left - 1;
int curH = Math.min(height[left], height[i]) - height[top];
res += curW * curH;
}
st.push(i);
}
return res;
}
}
84. 柱状图中最大的矩形
思路:同接雨水,但实际处理有些区别,这里单调栈中元素存放下标,栈顶到栈底递减。
class Solution {
public int largestRectangleArea(int[] heights) {
int n = heights.length;
//数组扩容,在头和尾各加入一个元素
int [] newHeights = new int[n + 2];
newHeights[0] = 0;
newHeights[newHeights.length - 1] = 0;
for (int i = 0; i < n; i++) {
newHeights[i + 1] = heights[i];
}
heights = newHeights;
n = heights.length;
Stack<Integer> st = new Stack();
int res = 0;
st.push(0);
for (int i = 0; i < n; i++) {
while (!st.isEmpty() && heights[st.peek()] > heights[i]) {
int mid = st.pop();
if (!st.isEmpty()) {
int w = i - st.peek() - 1;
int h = heights[mid];
res = Math.max(res, h * w);
}
}
st.push(i);
}
return res;
}
}