单调栈的实现
单调栈的特点
单调栈是一种由栈演化而来的数据结构。
应用环境:通常是一维数组,要寻找任一个元素的右边或者左边第一个比自己大或者小的元素的位置,此时我们就要想到可以用单调栈了
单调栈主要分成两种情况:单调递增栈和单调递减栈,这两种栈都是用来解决这个问题,但我们要思考在不同的题目中该怎么使用这两种栈的结构、这是一个单调栈的一个难点。
现在我们先来介绍一下这个数据结构,这个结构的本质是空间交换时间,我们用一个栈来存放我们遍历过的元素。
我们在使用单调栈的时候,我们一定要明确两个问题:1.单调栈中存放的元素 ,2 .单调栈的元素是单调递增还是单调递减的顺序。我们在下面讲解这两个问题的解决方法。
-
我们如何确定这个栈中存放的元素,一般情况我们存放的是一个遍历元素的索引。毕竟我们是寻找第一个比自己大或者比自己小的元素的位置。所以我们的单调栈中存放的主要是索引。
-
我们如何确定顺序,这里笔者在学习过程中总结了一个规律:如果求一个元素右边第一个更大元素,单调栈就是递增的,如果求一个元素右边第一个更小元素,单调栈就是递减的(这里的顺序指的是从栈顶到栈底的顺序),接下来我们将重点讲解这个问题,我们从单调出入栈的条件来进行分析
单调栈中出入栈的情况主要有三个:
- 栈顶元素大于当前遍历元素
- 栈顶元素小于当前遍历元素
- 栈顶元素等于当前遍历元素
我们通过一道题目来更好的理解这三种情况
739 每日温度
给定一个整数数组 temperatures
,表示每天的温度,返回一个数组 answer
,其中 answer[i]
是指对于第 i
天,下一个更高温度出现在几天后。如果气温在这之后都不会升高,请在该位置用 0
来代替。
示例 1:
输入: temperatures = [73,74,75,71,69,72,76,73]
输出: [1,1,4,2,1,1,0,0]
在做题目之前我们首先要先对返回的数组进行一个赋初值操作因为返回的数值在找不到的时候返回0,那么我们在最开始的时候赋值这个数组的值全为0;
int stack[temperaturesSize];
for(int i = 0; i < temperaturesSize; i++){
array[i] = 0;
}
我们先将一个元素入栈,然后栈中的元素为一个下标为0。
加入T[1] = 74,因为T[1] > T[0](当前遍历的元素T[i]大于栈顶元素T[st.top()]的情况)。
这时候我们可以将这个元素弹出,并且在返回数组中记录这个元素的索引。
array[stack[top]] = i - stack[top];
top--;
然后我们继续遍历这个数组,让后往后读后面的数组内的内容,读到75时后我们在弹出这个栈顶元素74,这时候我们可以将这个元素弹出,并且在返回数组中记录这个元素的索引。
然后就是读入71到数组中,在读入69后发现该数字小于这个栈顶元素然后我们将这个数字入栈。然后我们现在只用找到下一比这个数这个大的数字,然后我们读取到72这个数字然后我们就要进行一个操作:只要栈顶元素比该数字小,我们就一直出栈,直到找到栈顶元素比这个元素大这样我们就找到了栈中元素后面比他大的元素,我们就这样实现了寻找后面第一个比该元素大的元素索引。
完整代码如下:
int* dailyTemperatures(int* temperatures, int temperaturesSize, int* returnSize) {
int* array = (int*)malloc(sizeof(int) * temperaturesSize);
int top = -1;
int stack[temperaturesSize];
for(int i = 0; i < temperaturesSize; i++){
array[i] = 0;
}
*returnSize = temperaturesSize;
for(int i = 0; i < temperaturesSize; i++){
if (top == -1) {
stack[++top] = i;
}
else {
while (top >= 0 && temperatures[i] > temperatures[stack[top]]) {
array[stack[top]] = i - stack[top];
top--;
}
stack[++top] = i;
}
}
return array;
}
503 下一个更大元素||
给定一个循环数组(最后一个元素的下一个元素是数组的第一个元素),输出每个元素的下一个更大元素。数字 x 的下一个更大的元素是按数组遍历顺序,这个数字之后的第一个比它更大的数,这意味着你应该循环地搜索它的下一个更大的数。如果不存在,则输出 -1。
示例 1:
- 输入: [1,2,1]
- 输出: [2,-1,2]
- 解释: 第一个 1 的下一个更大的数是 2;数字 2 找不到下一个更大的数;第二个 1 的下一个最大的数需要循环搜索,结果也是 2。
这道题目也是这个单调栈的一个应用,我们可以通过一个将原先数组变成原来两倍的操作,然后在按单调栈的操作进行对数组的进行一个赋值操作
int* nextGreaterElements(int* nums, int numsSize, int* returnSize) {
int* array = (int*)malloc(sizeof(int) * numsSize * 2);
if (numsSize == 0) {
return NULL;
}
for (int i = 0;i < numsSize; i++) {
array[i] = nums[i];
}
for (int i = 0;i < numsSize; i++) { //对于新建数组复制为原来的两倍
array[i + numsSize] = nums[i];
}
int stack[numsSize * 2];
int top = -1;
int* returnArray = (int*)malloc(sizeof(int) * numsSize * 2);
for (int i = 0; i < numsSize; i++) {
returnArray[i] = -1;
}
for (int i = 0; i < 2 * numsSize; i++) {
while (top >= 0 && array[i] > array[stack[top]]) { //实现单调栈的操作
returnArray[stack[top]] = array[i];
top--;
}
stack[++top] = i;
}
*returnSize = numsSize;
return returnArray;
}
在经过我们前面的学习,我们可以尝试一道单调栈中比较难的题目了,我们来做一下上次考核的最后一道题目接雨水。
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 个单位的雨水(蓝色部分表示雨水)。
单调栈在对进行接雨水操作的时候我们是以行为单位接雨水的,我们就是在同一个高度进行一层的接雨水,明白了这个道理,我们就可以开始进行对三种入栈情况的一个分析了,
- 栈顶元素大于当前遍历元素
- 栈顶元素小于当前遍历元素
- 栈顶元素等于当前遍历元素
第一种情况,我们因为是要找到第一个比他大的就弹出栈,然后对栈中的元素进行一个求栈中雨水的操作了。
重点在于我们如何进行一个求雨水,我接下来放出一段代码,来对代码进行分析。
tmp = stack[top--]; //找到我们的一个中间元素
h = fmin(height[stack[top]], height[i]) - height[tmp]; //这一步是求出两边高度的一个最低值,然后减去中间元素,这样我们就可以求出一个高度
w = i - stack[top] - 1; //这一步求出他的一个长度,因为我们的栈中存储的一般都是一个长度
sum += h * w; //进行一个求和
第二种情况,我们因为我们的单调栈是一个单调递增栈,所以,我们直接将元素入栈。
第三种情况,因为我们要找最后一个靠近水的栈,所以我们要将栈中元素出栈,再将遍历到的元素入栈,这样避免我们在求宽度的时候出现多余的部分。
分析清楚这三种情况的时候,我们就可以开始实现单调栈的部分。
这个代码对后面两种情况进行了一个合并操作,是单调栈的精简版本。
int trap(int* height, int heightSize) {
int stack[heightSize];
int top = -1;
int sum = 0;
int h;
int w;
int tmp;
for (int i = 0; i < heightSize; i++) { //对所有元素进行一个遍历入栈
while (top >= 0 && height[i] > height[stack[top]]) {
tmp = stack[top--];
if (top != -1) {
h = fmin(height[stack[top]], height[i]) - height[tmp];
w = i - stack[top] - 1;
sum += h * w;
}
}
stack[++top] = i;
}
return sum;
}
柱状图中的最大矩形
给定 n 个非负整数,用来表示柱状图中各个柱子的高度。每个柱子彼此相邻,且宽度为 1 。
求在该柱状图中,能够勾勒出来的矩形的最大面积。
其实这道题目和上面那道题目是差不多的一个思路,只不过一个是找到后一个比他大的,而这个是找到后一个比他小的柱状图型。
但是这道题目有一个小问题就是我们要将最后的单调栈清空,为了方便我们清空这个栈,我们可以在我们原先的数组的开头和结尾进行一个添加0的操作。这样如果单调栈是单调递增的,我们的末尾0,会实现一个清空效果。
我将代码附在下方
int largestRectangleArea(int* heights, int heightsSize) {
int* array = (int*)malloc(sizeof(int) * (heightsSize + 2));
for (int i = 0 ;i < heightsSize; i++) {
array[i + 1] = heights[i];
}
array[0] = 0;
array[heightsSize + 1] = 0; //完成对数组的预先操作,这样方便我们实现后续操作
int stack[heightsSize + 2];
int sum = 0;
int top = -1;
for (int i = 0 ; i < heightsSize + 2; i++) {
while (top != -1 && array[i] < array[stack[top]]) {
int tmp = stack[top--];
if (top != -1) {
int h = array[tmp];
int w = i - stack[top] - 1;
sum = fmax(sum, w * h);
}
}
stack[++top] = i;
}
return sum;
}
小结:单调栈是一种在面对寻找右侧第一个大的元素时候比较方便的一种数据结构,有利于我们解决这类问题,笔者能力有限,也只能进行一个简单入门方面的学习。但是只要我们能将单调栈入栈的三种情况弄清楚了,我们就可以掌握相应的问题。