019数据结构之栈——算法备赛
栈
左右更小/大值【模型】
下一个更大元素|
问题描述
给定一个循环数组 nums
( nums[nums.length - 1]
的下一个元素是 nums[0]
),返回 nums
中每个元素的 下一个更大元素下标 。
数字 x
的 下一个更大的元素 是按数组遍历顺序,这个数字之后的第一个比它更大的数,这意味着你应该循环地搜索它的下一个更大的数。如果不存在,则输出 -1
。
结果返回一个数组ans
,ans[i]
表示nums[i]
的下一个更大元素的下标。
原题链接
思路分析
创建一个栈,0入栈,从左往右遍历数组nums,判断nums[i]是否大于栈顶元素
- 大于则说明栈顶元素的下一个更大元素的下标为i,
重复判断过程,直到栈顶元素小于等于num[i]
或栈为空后i
入栈。
最后遍历结束后,栈里还残存有元素,需要在从头遍历一次表示数组循环。
vector<int> nextGreaterElements(vector<int>& nums) {int n=nums.size();vector<int>tar(n,-1);int maxn=nums[0];stack<int>st;st.push(0);for(int i=1;i<n;i++){while(!st.empty()&&nums[i]>nums[st.top()]){tar[st.top()]=i;st.pop();}st.push(i);}for(int i=0;i<n;i++){while(!st.empty()&&nums[i]>nums[st.top()]){tar[st.top()]=i;st.pop();}}return tar;}
下一个更大元素||🚨
问题描述
给你一个下标从 0 开始的非负整数数组 nums
。对于 nums
中每一个整数,你必须找到对应元素的 第二大 整数。
如果 nums[j]
满足以下条件,那么我们称它为 nums[i]
的 第二大 整数:
j > i
nums[j] > nums[i]
- 恰好存在 一个
k
满足i < k < j
且nums[k] > nums[i]
。
如果不存在 nums[j]
,那么第二大整数为 -1
。
- 比方说,数组
[1, 2, 4, 3]
中,1
的第二大整数是4
,2
的第二大整数是3
,3
和4
的第二大整数是-1
。
请你返回一个整数数组 answer
,其中 answer[i]
是 nums[i]
的第二大整数。
原题链接
思路分析
如果是寻找右边第一个更大元素,那只需要定义一个单调栈,从前往后遍历,遍历到nums[i]
,弹出的栈顶元素对应的的右边第一大的元素就是nums[i]
。
现在要找右边第二大的元素,我们可以定义两个单调栈,遍历到nums[i]
,当第一个单调栈的栈顶弹出后不直接舍弃,而是添加进第二个单调栈,当第二个栈的栈顶弹出,则该栈顶元素的右边第二大的元素就是nums[i]
。
但这也产生问题:
-
直接将第一个栈的栈顶入栈到第二个栈,如果多次操作,因为栈是单调递减的,这会导致直接入到第二个栈变成递增的。
对于这个问题,可以把第二个栈定义成双端队列,在入队的时候,从底部入队,这就保证单次入队的元素中,小的元素先入队在前面,大的元素后入队在后面。
-
当第二个栈里还有元素,将第一个栈里的栈顶从底部入第二个栈的话,导致小的元素在栈底。
将第二个栈定义为双端队列,显然不能解决第二个问题,有个简单粗暴的方法,将第二个栈定义为优先队列,这保证了队头元素一定 是最小的,但似乎也进行了不必要的计算(每次入队的复杂度为O(logk))。
力扣灵神提供了一个理想的方案,将两个栈定义为vector
数组,将第一个栈中的小于nums[i]
的元素批量添加进第二个栈,这可以保持批量元素间的单调性,而且第二个栈如果留有元素,那批量元素也会添加进第二个栈的上方。
代码
vector<int> secondGreaterElement(vector<int> &nums) {int n = nums.size();vector<int> ans(n, -1), s, t;for (int i = 0; i < n; i++) {int x = nums[i];while (!t.empty() && nums[t.back()] < x) {ans[t.back()] = x; // t 栈顶的下下个更大元素是 xt.pop_back();}int j = s.size();while (j && nums[s[j - 1]] < x) {j--; // s 栈顶的下一个更大元素是 x}t.insert(t.end(), s.begin() + j, s.end()); // 把从 s 弹出的这一整段元素加到 ts.resize(j); // 弹出一整段元素s.push_back(i); // 当前元素(的下标)加到 s 栈顶}return ans;}
问:第二个栈的栈顶(也就是t.back()是否会小于批量添加进来的元素)?
答案是不会,因为nums[i]先于t中栈顶比较,t.back()大于等于nums[i]才不被删除,nums[i]在与s栈中的元素比较,批量元素都小于nums[i],而t.back()大于等于nums[i],所以t.back()大于等于待添加的所有批量元素。
时间复杂度O(n)
空间复杂度O(n)
左右更高楼房
问题描述
给定一排楼房的高度,第 i 个楼房的高度为h[i]
,对于每个楼房,请你找出它左边第一个比它高的楼房下标(没有测为 -1)和右边第一个比它高的楼房下标(没有则为 -1)。
代码
#include<bits/stdc++.h>
using namespace std;
int main()
{int n; cin>>n;vector<int>h(n);for(int i=0;i<n;i++) cin>>h[i];vector<int>ansL(n,-2);vector<int>ansR(n,-2);stack<int>stL;stack<int>stR;for(int i=0;i<n;i++){while(!stL.empty()&&h[stL.top()]<=h[i]){ansR[stL.top()]=i;stL.pop();}if(!stL.empty()) ansL[i]=stL.top();stL.push(i);}for(int i:ansL) cout<<i+1<<" ";cout<<endl;for(int i:ansR) cout<<i+1<<" ";return 0;
}
子数组的最小值之和
问题描述
给定一个整数数组 arr
,找到 min(b)
的总和(arr中所有子数组的最小值的总和),其中 b
的范围为 arr
的每个(连续)子数组。
由于答案可能很大,因此 返回答案模 10^9 + 7
。
原题链接
思路分析
本题是【左右更小/大值】的具体应用,请先完成上题。
题目要求所有子数组的最小值的总和,如果一个一个暴力枚举子数组再统计最小值的话,就是统计最小值能在O(1)的时间复杂度内完成,枚举子数组的复杂度也要O(n2)。根据题给数据规模,需要设计一个O(n)的算法。
直接统计子数组不好统计,可以统计元素本身,使用单调栈计算出每个元素arr[i]
的距离其左右更小值下标的长度L
和R
,根据排列组合原理,以该元素为最小值的子数组就有L*R
个,对答案的贡献就是L*R*arr[i]
,统计总的贡献就是答案。
图例:
如arr[3]=2
,它左边能延伸的长度为3,右边能延伸的长度为3,根据排列组合的乘法原理,以arr[3]
为最小值的子数组个数就是3*3=9
。
时间复杂度:O(n)。
代码
int sumSubarrayMins(vector<int>& arr) {int mod=1e9+7,n=arr.size();stack<int>st;vector<int>ls(n),rs(n);for(int i=0;i<n;i++){while(!st.empty()&&arr[st.top()]>=arr[i]){rs[st.top()]=i-st.top();st.pop();}ls[i]=st.empty()?i+1:i-st.top();st.push(i);}while(!st.empty()){ //处理栈内残留rs[st.top()]=n-st.top();st.pop();}long long ans=0;for(int i=0;i<n;i++){ans+=(long long)arr[i]*ls[i]*rs[i]%mod;ans%=mod;}return ans;
}
柱状图中最大矩形
问题描述
给定 n 个非负整数,用来表示柱状图中各个柱子的高度。每个柱子彼此相邻,且宽度为 1 。
求在该柱状图中,能够勾勒出来的矩形的最大面积。
原题链接
暴力法
首先可以考虑暴力法,枚举矩形的宽和高,其中「宽」表示矩形贴着柱状图底边的宽度,「高」表示矩形在柱状图上的高度。
枚举每个柱子的高作为目标矩形的高,左右扩散寻找最大的宽,每次遍历到的目标矩形与历史最值比较更新,最后的历史最值就是答案。
代码
int largestRectangleArea(vector<int>& heights) {int n = heights.size();int ans = 0;for (int mid = 0; mid < n; ++mid) {// 枚举高int height = heights[mid];int left = mid, right = mid;// 确定左右边界while (left - 1 >= 0 && heights[left - 1] >= height) {--left;}while (right + 1 < n && heights[right + 1] >= height) {++right;}// 计算面积ans = max(ans, (right - left + 1) * height);}return ans;}
可以发现,这种暴力方法的时间复杂度为 O(N2),会超出时间限制,我们必须要进行优化。
单调栈
我们归纳一下枚举「高」的方法:
-
首先我们枚举某一根柱子 i 作为高 h=heights[i];
-
随后我们需要进行向左右两边扩展,使得扩展到的柱子的高度均不小于 h。换句话说,我们需要找到左右两侧最近的高度小于 h 的柱子,这样这两根柱子之间(不包括其本身)的所有柱子高度均不小于 h,并且就是 i 能够扩展到的最远范围。
对于两根柱子 j0
以及 j1
,如果 j0<j1
并且 heights[j0]≥heights[j1]
,那么对于任意的在它们之后出现的柱子 i(j1<i)
,j0
一定不会是i
左侧且最近的小于其高度的柱子。
换句话说,如果有两根柱子 j0
和 j1
,其中j0
在 j1
的左侧,并且 j0
的高度大于等于 j1
,那么在后面的柱子i
向左找小于其高度的柱子时j1
会「挡住」j0
,j0
就不会作为左边界了。
这样,我们可以对数组从左向右进行遍历,同时维护一个「可能作为左边界」的栈,其中按照从小到大的顺序存放了一些 j 值。
这样我们就可以使用二分查找的方法找到 i
对应的 j
,但真的需要吗?当我们枚举到 i+1
时,原来的i
也变成了 j
值,因此i
会被放入数据结构。由于所有在数据结构中的j
值均小于 i
,那么所有高度大于等于 height[i]
的 j
都不会作为左边界,需要从栈中移除。移除前可以判断,要移除元素对应的右边界就是i
,对应的左边界就是下一个栈顶。
代码
int largestRectangleArea(vector<int>& heights) {int n = heights.size();vector<int> left(n), right(n, n); //记录每个元素对应的左边界(默认为0)和右边界(默认为n)stack<int> mono_stack;for (int i = 0; i < n; ++i) {while (!mono_stack.empty() && heights[mono_stack.top()] >= heights[i]) {right[mono_stack.top()] = i; //记录右边界mono_stack.pop(); }left[i] = (mono_stack.empty() ? -1 : mono_stack.top()); //栈为空,左边界为-1,否则为栈顶mono_stack.push(i); //入栈。}//每次遍历的i 的左右边界不能在当前遍历时立刻求出,所以采用离线更新历史最值。int ans = 0;for (int i = 0; i < n; ++i) {ans = max(ans, (right[i] - left[i] - 1) * heights[i]);}return ans;
}
最小字典序【模型】
根据模式串构造最小数字
问题描述
给你下标从 0 开始、长度为 n
的字符串 pattern
,它包含两种字符,'I'
表示 上升 ,'D'
表示 下降 。
你需要构造一个下标从 0 开始长度为 n + 1
的字符串num,且它要满足以下条件:
num
只包含数字'1'
到'9'
,其中每个数字 至多 使用一次。- 如果
pattern[i] == 'I'
,那么num[i] < num[i + 1]
。 - 如果
pattern[i] == 'D'
,那么num[i] > num[i + 1]
。
请你返回满足上述条件字典序 最小 的字符串 num
。
1 <= pattern.length <= 8
pattern
只包含字符'I'
和'D'
。
原题链接
代码
string smallestNumber(string pattern){string flag="123456789";int n=pattern.size()+1;string res; //要返回的目标字符串pattern.push_back('I'); //pattern最后一个为‘D’也能添加数字stack<char> st;for(int i=0;i<n;i++){if(pattern[i]=='I'){res+=flag[i];while(!st.empty()){ //把前面出现‘D’的对应的字符添加进来res+=st.top();st.pop();}}else{st.push(flag[i]); //出现降序,先将数字添加进栈,等待后面出现升序,再将栈中数字全部导出。}}return res;
}
使用机器人打印字典序最小的字符串
问题描述
给你一个字符串 s
和一个机器人,机器人当前有一个空字符串 t
。执行以下操作之一,直到 s
和 t
都变成空字符串:
- 删除字符串
s
的 第一个 字符,并将该字符给机器人。机器人把这个字符添加到t
的尾部。 - 删除字符串
t
的 最后一个 字符,并将该字符给机器人。机器人将该字符写到纸上。
请你返回纸上能写出的字典序最小的字符串。
原题链接
思路分析
题目意思其实是可以使用一个栈暂存字符,求从栈中输出的字典序最小的字符串。
遍历s字符串,对于枚举到的字符ch
,将其添加入栈,而后可以选择 1.枚举下一字符,2.从栈中输出栈顶字符。要想总的字符串字典序最小,靠前的字符就要尽量小,若s
的ch
字符后面没有比栈顶更小的字符,那就应该让栈顶字符出栈,否则应该枚举下一字符。
如何快速知道s
的ch
字符后面有没有比栈顶更小的字符呢?可以先预处理好数据,具体地,使用suf_min
数组存储,suf_min[i]
记录了s[i,n-1]
子串的最小字符。枚举到s[i]
时,若suf_min[i+1]
大于等于栈顶字符,那说明后面没有比栈顶字符更小的字符。
代码
string robotWithString(string s) {int n=s.size();vector<char> suf_min(n+1); suf_min[n]='z'; //使用最大值作为后置哨兵for(int i=n-1;i>=0;i--){suf_min[i]=min(suf_min[i+1],s[i]);}string ans;stack<char>st;for(int i=0;i<n;i++){st.push(s[i]);while(!st.empty()&&st.top()<=suf_min[i+1]){ans+=st.top();st.pop();}}return ans;
}
去除重复字母
给你一个字符串 s
,请你去除字符串中重复的字母,使得每种字母只出现一次。需保证
返回结果的字典序最小
(要求不能打乱其他字符的相对位置)
原题链接
思路分析
声明一个栈(可直接用一个字符串来模拟栈,最后直接返回这个字符串)来存储前面的枚举到的字符,当枚举到一个前面没有过的字符(不重复的),若栈顶字符后面还存在且该枚举字符比栈顶元素小,为了最后保留的字符串字典序更小,该枚举字符更适合当栈顶元素,我们将栈顶元素弹出。
具体实现时,用一个set
集合辅助判断当前枚举字符串在前面字符中已存在,用dat
字典数组预处理每个字符的频数辅助判断后面字符中是否还有栈顶字符。
最后维护的字典序最小的栈字符串就是答案。
思路分析
string removeDuplicateLetters(string s) {unordered_set<char>set;vector<int>dat(26,-1); //字典数组string ans;int n=s.size();for(char ch:s) dat[ch-'a']++; //统计字频ans.push_back(s[0]);set.insert(s[0]);for(int i=1;i<sum;i++){if(!set.count(s[i])){ //该字符为新字符//若栈顶字符后面还存在且当前枚举字符更小,应该删除该栈顶字符while(!ans.empty()&&dat[ans.back()-'a']&&s[i]<ans.back()){ dat[ans.back()-'a']--;set.erase(ans.back());ans.pop_back();}ans.push_back(s[i]); //当前枚举字符作为新的栈顶set.insert(s[i]); //标记栈中已存在该字符}else dat[s[i]-'a']--;}return ans;
}