9道LeetCode学烂单调栈
前言
本文讲解利用单调栈去解决一些问题,总结出单调栈能解决的问题是什么?思考单调栈为什么能解决这类问题?以及单调栈解决问题的基本模板。
一、单调栈
维护栈中元素从栈顶到栈底递增或者递减的栈。
单调递增栈:栈顶到栈底元素单调递增,或栈底到栈顶元素单调不增。
单调递减栈:栈顶到栈底元素单调递减,或栈底到栈顶元素单调不减。
二、单调栈的基本应用场景
解决的一个数组中,找出某个元素后面第一个比该元素大或者小的值(next greater element问题)。
为什么单调栈能够解决next greater element问题?如果从单调栈的定义出发,可能很难理解,为什么单调栈就解决了这个问题呢?
但如果我们反向思考:不是单调栈的出现引出了解决next greater element 问题的方法,而是next greater element问题的解决引出了单调栈。这样很多单调栈的解法也许就好理解多了。
三、单调栈的基本模板
三步走 基本模板
- 维护递增 increase(减 decrease)栈
- 放入最后结果数组
- 当前元素入栈
四、9道单调栈解决的问题
1.最基本的Next Greater Element
- 问题描述:
修改elements数组中当前元素为当前元素后面首个比当前元素大的值,如果没有你当前元素大的值,则修改为-1。 - 示例:
输入 elements=[2,1,2,4,3],则返回数组[4,2,4,-1,-1] - 单调栈解法
private static int[] nextGreaterElement(int [] elements)
{
//0. 存放结果数组 result
int[] result = new int[elements.length];
//1. 遍历数组elements,
Stack<Integer> stack_in = new Stack<>();
for (int i = elements.length-1; i >= 0; i--) {
//2. 维护单调递增栈 stack_in(如果递增栈不为空,如果当前元素小于栈顶元素,这当前元素的next greater element就是栈顶元素)
while (!stack_in.isEmpty() && elements[i] >= stack_in.peek()) {
stack_in.pop();
}
//3. 将栈顶元素存入result
result[i]=stack_in.isEmpty()?-1:stack_in.peek();
//4. 将当前元素入栈
stack_in.push(elements[i]);
}
return result;
}
2. Next Greater Element(LeetCode 496)
-
问题描述:
给你两个 没有重复元素 的数组nums1和nums2,其中nums1是nums2的子集。请你找出 nums1中每个元素在nums2中的下一个比其大的值。
nums1中数字x的下一个更大元素是指x在nums2中对应位置的右边的第一个比x大的元素。如果不存在,对应位置输出 -1 。 -
示例 1:
输入: nums1 = [4,1,2], nums2 = [1,3,4,2].
输出: [-1,3,-1]
解释:
对于 num1 中的数字 4 ,你无法在第二个数组中找到下一个更大的数字,因此输出 -1 。
对于 num1 中的数字 1 ,第二个数组中数字1右边的下一个较大数字是 3 。
对于 num1 中的数字 2 ,第二个数组中没有下一个更大的数字,因此输出 -1 。 -
单调栈解法
/**
* 单调递增栈的解决方案
*
* 1.将num2利用单调递增栈求所有元素的下一个更大值,存储到result数组中(BaseModel解法)
* 2.根据num1的值获取其在num2中的索引,然后获取result中对应索引位置处的值
* @param nums1
* @param nums2
* @return
*/
public static int[] nextGreaterElement(int[] nums1, int[] nums2) {
//0. 存储每个元素最终结果的键值对result
Map<Integer, Integer> result = new HashMap<>();
//1. 创建栈 stack_in
Stack<Integer> stack_in = new Stack<>();
//2. 遍历num2数组元素
for (int i = nums2.length-1; i >= 0; i--) {
//3. 维护单调递增栈
while (!stack_in.isEmpty() && nums2[i]>=stack_in.peek())
{
stack_in.pop();
}
//4. 将栈顶元素放入result中
result.put(nums2[i],stack_in.isEmpty()?-1:stack_in.peek());
//5. 将当前元素入栈
stack_in.push(nums2[i]);
}
//6. 获取num1在result对应索引处的值
int[] res = new int[nums1.length];
for (int i = 0; i < nums1.length; i++) {
res[i]=result.get(nums1[i]);
}
return res;
}
3. 循环数组的next greater element(LeetCode 503)
-
问题描述
给定一个循环数组(最后一个元素的下一个元素是数组的第一个元素),输出每个元素的下一个更大元素。
数字 x 的下一个更大的元素是按数组遍历顺序,这个数字之后的第一个比它更大的数,这意味着你应该循环地搜索它的下一个更大的数。
如果不存在,则输出 -1。 -
示例:
输入: [1,2,1]
输出: [2,-1,2]
解释: 第一个 1 的下一个更大的数是 2;
数字 2 找不到下一个更大的数;
第二个 1 的下一个最大的数需要循环搜索,结果也是 2。 -
单调栈解法一
/**
* 使用单调递增栈方法(倒序遍历)
*
* 1.因为nums为循环数组,最简单的思路就是我们可以把这个循环数组「拉直」,把nums数组拷贝两份存储到nums1数组中,
* 然后对nums1数组利用单调递增栈输出每个元素的下一个更大元素。(BaseModel解法)
*
* 2.但相比拷贝两份nums得到nums1,更好的方法是通过循环索引代替将nums拷贝两份到nums1
*
* 解题思路关键:
* 倒序遍历nums
* 如果单调递增栈中元素不为空,
* 如果当前元素值小于栈顶元素,则当前元素的next greater element 就是栈顶元素值;
* 当前元素的next greater element 找到后,因为当前元素可能是其之前元素的next greater element,所以将当前元素值入栈
* @param nums
* @return
*/
public static int[] nextGreaterElements_0(int[] nums) {
//0. 数组result存储每个元素的下一个更大元素
int[] result = new int[nums.length];
//1. 用于维护从栈顶待到栈底单调增的栈
Stack<Integer> stack_in = new Stack<>();
//2. 循环遍历nums,因为nums为循环数组,因此索引长度为nums的2倍,然后通过取余的方式,循环获取nums中的元素
int n=nums.length;
for (int i = n * 2 -1 ; i >= 0; i--) {
//3. 维护单调递增栈
while (!stack_in.isEmpty() && nums[i%n]>=stack_in.peek())
{
stack_in.pop();
}
//4. 将栈顶元素保存到result
result[i%n]=stack_in.isEmpty()?-1:stack_in.peek();
//5. 将当前元素入栈
stack_in.push(nums[i%n]);
}
return result;
}
- 单调栈解法二
/**
* 单调递增栈方法(正序遍历)
*
* 维护栈中元素单调性:栈底到栈顶元素单调递减
* 单调栈中保存的是下标,从栈底到栈顶的下标在数组nums中对应的值是单调不升的。
*
* 1.因为nums为循环数组,一个朴素的思想是,我们可以把这个循环数组「拉直」,
* 即复制该序列的前 n-1个元素拼接在原序列的后面。这样我们就可以将这个新序列当作普通序列,用上文的方法来处理。
* 2.而在本题中,我们不需要显性地将该循环数组「拉直」,而只需要在处理时对下标取模即可.
*
* 解题思路关键点:
* 正序遍历nums
* 如果单调递增栈不为空,
* 如果当前元素值大于栈顶索引对应的值,则当前元素值为栈顶索引对应值的next Greater element,(证明很简单:如果有更靠前的更大元素,那么这些位置将被提前弹出栈)
* 否则当前元素入栈,继续寻找栈顶元素的next greater element;
* 栈顶索引对应值的next Greater element找到后,则不必再找,直接将栈顶元素出栈即可,这一过程会动态维护栈中元素从栈底到栈顶的单调递减
*
* @param nums
* @return
*/
public static int[] nextGreaterElements(int[] nums) {
//0. 数组result存储next greater element结果,result数组元素初始化为-1
int[] result = new int[nums.length];
Arrays.fill(result,-1);
//1. 栈低到栈顶单调递减的栈 stack_in
Deque<Integer> stack_in = new LinkedList<>();
//2. 正序遍历nums
int n=nums.length;
for (int i = 0; i < n*2-1; i++) {
//3. 循环:如果stack_in不为空,并且当前元素值大于栈顶元素索引对应值,则当前元素即为栈顶元素索引对应值的next greater,同时弹出当前元素维护递增栈
while (!stack_in.isEmpty() && nums[i%n]>nums[stack_in.peek()])
{
result[stack_in.pop()]=nums[i%n];
}
//4. 当前元素入栈
stack_in.push(i%n);
}
return result;
}
4. 链表的下一个更大节点(LeetCode 1019)
- 问题描述
给出一个以头节点 head 作为第一个节点的链表。链表中的节点分别编号为:node_1, node_2, node_3, …。
每个节点都可能有下一个更大值(next larger value):
对于node_i,如果其next_larger(node_i)是node_j.val,那么就有j > i且node_j.val > node_i.val,
而j是可能的选项中最小的那个。如果不存在这样的j,那么下一个更大值为0
返回整数答案数组 answer,其中 answer[i] = next_larger(node_{i+1}) 。
注意:在下面的示例中,诸如 [2,1,5] 这样的输入(不是输出)是链表的序列化表示,其头节点的值为 2,第二个节点值为 1,第三个节点值为 5 。
(就是一个套娃题,该题本质和求数组中nextGreaterElement一样的,只需要把链表转为数组,然后采用单调栈方法求解即可) - 示例1:
输入:[2,1,5]
输出:[5,5,0]
/**
* 单调栈方法
*
* 1. 将链表转为数组 nums
* 2. 倒序遍历nums,维护单调递增栈 stack_in
* 3. 把栈顶元素放入结果数组result中
* @param head
* @return
*/
public static int[] nextLargerNodes(ListNode head) {
//1. 链表转为数组nums
ArrayList<Integer> nums = new ArrayList<Integer>();
ListNode curr_Node = head;
while (curr_Node!=null)
{
nums.add(curr_Node.val);
curr_Node=curr_Node.next;
}
//2. 存储结果的数组result
int[] result = new int[nums.size()];
//3. 使用的单调递增栈 stack_in
Stack<Integer> stack_in = new Stack<Integer>();
//4. 倒序遍历nums
for (int i = nums.size()-1; i >= 0; i--) {
//5. 维护单调增栈
while (!stack_in.isEmpty()&&nums.get(i)>=stack_in.peek())
{
stack_in.pop();
}
//6. 栈顶元素即为当前元素的next greater element
result[i]=stack_in.isEmpty()?0:stack_in.peek();
//7. 当前元素入栈
stack_in.add(nums.get(i));
}
return result;
}
public class ListNode {
int val;
ListNode next;
ListNode(int x) {
val = x;
}
}
5. 每日温度(LeetCode 739)
-
问题描述
请根据每日 气温 列表,重新生成一个列表。
对应位置的输出为:要想观测到更高的气温,至少需要等待的天数。
如果气温在这之后都不会升高,请在该位置用 0 来代替。 -
示例:
给定一个列表 temperatures = [73, 74, 75, 71, 69, 72, 76, 73],
你的输出应该是 [1, 1, 4, 2, 1, 1, 0, 0]。 -
单调栈解法一
/**
* 单调栈方法(逆向遍历)
*
* 本质也是nextGreaterElement问题
* 关键点:求出当前温度的下一个更高温度隔的天数,即索引差,因此栈中存储索引
* @param T
* @return
*/
public static int[] dailyTemperatures_0(int[] T) {
//1. 数组result存储结果
int[] result = new int[T.length];
//2. 单调递增栈stack_in
Stack<Integer> stack_in = new Stack<Integer>();
//3. 倒序遍历T
for (int i = T.length-1; i >= 0; i--) {
//4. 维护stack_in
while (!stack_in.isEmpty()&&T[i]>=T[stack_in.peek()])
{
stack_in.pop();
}
//5. 栈顶元素即为当前元素的next greater element,将其存入result
result[i]=stack_in.isEmpty()?0:stack_in.peek()-i;
//6. 当前元素入栈
stack_in.add(i);
}
return result;
}
- 单调栈解法二
/**
* 单调栈方法(正向遍历)
*
* 本质也是nextGreaterElement问题
* 关键点:求出当前温度的下一个更高温度隔的天数,即索引差,因此栈中存储索引
* @param T
* @return
*/
public static int[] dailyTemperatures(int[] T) {
//1. 存储结果数组result(默认初始化数组中的值都为0)
int[] result = new int[T.length];
//2. 待维护的单调递增栈stack_in(栈底到栈顶单调不增),栈中存储索引
Stack<Integer> stack_in = new Stack<Integer>();
//3. 正向遍历T
for (int i = 0; i < T.length; i++) {
//4. 维护单调递增栈stack_in
while (!stack_in.isEmpty()&&T[i]>T[stack_in.peek()])
{
//4.1 如果当前元素大于栈顶元素代表当前元素就是栈顶元素的next更高温度,计算并存储结果值,因为已经找到了,将栈顶元素出栈
result[stack_in.peek()]=stack_in.isEmpty()?0:i-stack_in.peek();
stack_in.pop();
}
//5 将当前元素索引入栈
stack_in.add(i);
}
return result;
}
6.去除重复字母(LeetCode 316)
-
问题描述
给你一个字符串 s ,请你去除字符串中重复的字母,使得每个字母只出现一次。需保证 返回结果的字典序最小(要求不能打乱其他字符的相对位置)。 -
示例 1:
输入:s = “bcabc”
输出:“abc”
提示:
1 <= s.length <= 104
s 由小写英文字母组成 -
单调栈解法
/**
* 单调栈方法
*
* 返回结果的字典序最小,即要求从前往后是递增的序列,因此其本质还是next greater element问题
*
* 时间复杂度:O(N),其中 N 为字符串长度。代码中虽然有双重循环,但是每个字符至多只会入栈、出栈各一次。
*
* @param s
* @return
*/
public static String removeDuplicateLetters(String s) {
//1. lastIndex数组记录每个元素最后的索引(小写英文字母的ASCII码为0-127)
int[] lastIndex = new int[128];
for (int i = 0; i < s.length(); i++) {
lastIndex[s.charAt(i)]=i;
}
//2. 单调递减栈stack_de(从栈底到栈顶单调不减)
Stack<Character> stack_de = new Stack<Character>();
//3. 正向遍历s字符
for (int i = 0; i <s.length() ; i++) {
char c=s.charAt(i);
//4. 如果当前字符已在栈中,则不处理该字符
if (stack_de.search(c)!=-1)
continue;
//5. 维护单调递减栈stack_de:如果当前字符小于栈顶字符,且栈顶字符不是最后一次出现,则栈顶字符出栈
while (!stack_de.isEmpty()&&c<stack_de.peek()&&i<lastIndex[stack_de.peek()])
{
stack_de.pop();
}
//6. 当前字符入栈
stack_de.push(c);
}
//7. 拼接stack_de,返回字典序最小
StringBuffer stringBuffer = new StringBuffer();
for (Character character : stack_de) {
stringBuffer.append(character);
}
return stringBuffer.toString();
}
7. 移除k位数字(LeetCode 402)
-
问题描述
给定一个以字符串表示的非负整数 num,移除这个数中的 k 位数字,使得剩下的数字最小。注意:
num 的长度小于 10002 且 ≥ k。
num 不会包含任何前导零。 -
示例1:
输入: num = “1432219”, k = 3
输出: “1219”
解释: 移除掉三个数字 4, 3, 和 2 形成一个新的最小的数字 1219。 -
示例2:
输入: num = “10200”, k = 1
输出: “200”
解释: 移掉首位的 1 剩下的数字为 200. 注意输出不能有任何前导零。 -
单调栈解法
/**
* 单调递减栈方法(栈底到栈顶单调不减)
*
* 关键点:
* 对于两个相同长度的数字序列,最左边不同的数字决定了这两个数字的大小。
* 基于此,我们可以知道,若要使得剩下的数字最小,需要保证靠前的数字尽可能小。、
*
* 因此,对于一个数字数组将其分为两部分,k次数用尽为前一部分,用于决定数字大小;
* 剩余的为后一部分,如果存在,则不必管它。
* 而对于前一部分仍其本质还是一个next greater 问题。
*
* @param num
* @param k
* @return
*/
public static String removeKdigits(String num, int k) {
//1. 单调递减栈 stack_de
Stack<Character> stack_de = new Stack<>();
//2. 正序遍历num
for (int i = 0; i < num.length(); i++) {
//2.1 维护单调递减栈
Character n=num.charAt(i);
while (!stack_de.isEmpty()&&n<stack_de.peek()&&k>0)
{
stack_de.pop();
k--;
}
//2.2 当前元素入栈
stack_de.push(n);
}
//3 如果k>0,则代表k次未用完,num已经单调递增序列了,把尾部剩余k次数字去掉即可
while (k>0)
{
stack_de.pop();
k--;
}
//4. 处理栈中首位为0的数字,将其去掉
StringBuffer buffer = new StringBuffer();
boolean firstZero=true;//stack_de栈底元素是否为为0
for (Character c : stack_de) {
if (c!='0')
{
buffer.append(c);
firstZero=false;
}
else
{
if (!firstZero)
buffer.append('0');
}
}
return buffer.length()==0? "0" : buffer.toString();
}
8. 接雨水(LeetCode 42)
-
问题描述
给定 n 个非负整数表示每个宽度为 1 的柱子的高度图,计算按此排列的柱子,下雨之后能接多少雨水。 -
示例:
输入:height = [0,1,0,2,1,0,1,3,2,1,2,1]
输出:6
解释:上面是由数组 [0,1,0,2,1,0,1,3,2,1,2,1] 表示的高度图,在这种情况下,可以接 6 个单位的雨水(蓝色部分表示雨水)。 -
示例2:
输入:height = [4,2,0,3,2,5]
输出:9 -
动态规划解法
/**
* 动态规划方法
*
* 关键点:
* 对于一排木桩的储水量,可以看做是从左边发出一束光形成的阴影,同时再从右边发出一束光形成的阴影,两阴影的重叠部分即为储水部分。
* 储水量为形成当前重叠阴影部分的左右较小高度减去当前阴影部分的高度。
*
* 对于以上思路,你就用简单的两块木桩进行考虑,就比较容易想通了。
*
* @param height
* @return
*/
public static int trap_0(int[] height) {
//0. 如果没有柱状,储水量为0
if (height.length==0)
return 0;
//1. 动态规划储水量 dp
int dp=0;
//2. 左束光各个位置看到的高度 left
int n=height.length;
int[] left = new int[n];
left[0]=height[0];
for (int i = 1; i < n; i++) {
left[i]=Math.max(left[i-1],height[i]);
}
//3. 右束光各个位置看到的高度 right
int[] right = new int[n];
right[n-1]=height[n-1];
for (int i = n-2; i >= 0; i--) {
right[i]=Math.max(right[i+1],height[i]);
}
//4. 遍历height
for (int i = 0; i < height.length; i++) {
//4.1 取当前位置左束光与右束光形成的阴影较小高度,减去当前阴影的高度,累加到dp中
dp+=Math.min(left[i],right[i])-height[i];
}
//5. 返回dp
return dp;
}
- 单调栈解法
/**
* 单调栈方法
*
* 关键点:
* 如果柱子想要储水,必须满足当前柱子有左右柱子,并且当前柱子的高度小于左右柱子的高度。
*
* 基于此,我们不考虑柱子高度单调递减的,因此,维护一个从栈底到栈顶的单调不增栈,栈中存储柱子索引。
* 如果入栈柱子的高度要破坏单调栈,即当前的栈顶柱子与待入栈柱子可能储水,
* 此时,我们计算栈顶柱子的储水量(左右柱子的最小高度减去栈顶柱子高度再乘以左右柱子的距离),计算完成后将其出栈。
*
* @param height
* @return
*/
public static int trap(int[] height) {
//1. 储水结果 result
int result=0;
//2. 单调递增栈
Stack<Integer> stack_in = new Stack<>();
//3. 正向遍历height
for (int i = 0; i < height.length; i++) {
//3.1 维护单调栈,同时计算储水量
while (!stack_in.isEmpty()&&height[i]>height[stack_in.peek()])
{
int currHeight=height[stack_in.pop()];
//如果栈为空,代表没有左墙壁,无法储水
if (stack_in.isEmpty())
break;
result+=(Math.min(height[i],height[stack_in.peek()])-currHeight)*(i-stack_in.peek()-1);
}
//3.2 当前索引入栈
stack_in.push(i);
}
//4. 返回result
return result;
}
9.柱形图中最大的矩阵(LeetCode 84)
-
问题描述
给定 n 个非负整数,用来表示柱状图中各个柱子的高度。每个柱子彼此相邻,且宽度为 1 。
求在该柱状图中,能够勾勒出来的矩形的最大面积。 -
示例:
输入: [2,1,5,6,2,3]
输出: 10 -
单调栈解法
/**
* 单调栈方法求解
*
* 关键点:
* 维护一个从栈低到栈顶单调不减栈。即如果栈中柱子高度递增,则不处理,如果入栈高度破坏递增性,
* 则计算栈中柱子的最大面积,因为面积即需要柱子的高度,又需要柱子的宽度,所以,栈中存储索引。
*
* 时间复杂度O(N)
* @param heights
* @return
*/
public static int largestRectangleArea(int[] heights) {
//1. 最大面积 maxArea
int maxArea=0;
//2. 单调递减栈 stack_de
Stack<Integer> stack_de = new Stack<>();
//3. 正向遍历heights
for (int i = 0; i < heights.length; i++) {
//4. 维护单调递减栈
while (!stack_de.isEmpty()&&heights[i]<=heights[stack_de.peek()])
{
int height = heights[stack_de.pop()];
int width = i-(stack_de.isEmpty()?0:stack_de.peek()+1);
//5. 计算栈中元素的最大面积
maxArea=Math.max(maxArea,height*width);
}
//6. 将当前元素索引入栈
stack_de.push(i);
}
//7. 如果栈不为空,说明栈中元素从栈底到栈顶递增,继续计算栈中元素的最大面积
while (!stack_de.isEmpty())
{
int height = heights[stack_de.pop()];
int width = heights.length - (stack_de.isEmpty()?0:stack_de.peek()+1);
maxArea=Math.max(maxArea,height*width);
}
return maxArea;
}