目录
栈的特性是先入后出,栈的主要题型包括常规栈和单调栈。
常规栈应用
简化路径
使用栈缓存当前到达的路径,遇到"…"弹出栈顶,返回上级目录。注意对最终栈为空处理
string simplifyPath(string path) {
if(path.empty()) return "";
string curr_path;
stack<string>st;
for(int i=0;i<path.size();++i){
//跳过 /符号
if(path[i]=='/'){
continue;
}
//记录当前层级目录名称
curr_path+=path[i];
//当前层级目录结束
if(i+1==path.size ()|| path[i+1]=='/'){
if(curr_path.empty()==false){
if(curr_path==".."){ //返回当前父目录,弹出栈顶
if(st.empty()==false) st.pop();
}else if(curr_path != "."){ //过滤.符号
st.push(curr_path);
}
}
curr_path.clear();
}
}
if(st.empty())return "/"; //栈为空代表根目录
string ret;
while(st.empty()==false){ //将栈内目录名称使用 / 符号连接返回
ret="/"+st.top()+ret;
st.pop();
}
return ret;
}
计算波兰表达式
依次遍历表达式,如果是数字,则直接入栈,如果是操作符,则从栈内弹出左右操作数,并进行符号操作后,将操作结果入栈。注意,弹出操作数时,弹出的第一个是右操作数,第二个是左操作数
int do_operator(char c , int lNum,int rNum)
{
switch(c)
{
case '*':return lNum*rNum;
case '+':return lNum+rNum;
case '-':return lNum-rNum;
case '/':return lNum/rNum;
}
return 0;
}
bool isOperator(string s)
{
if(s == "*" || s=="+" ||s == "-" || s=="/") return true;
return false;
}
class Solution {
public:
int evalRPN(vector<string>& tokens) {
stack<string> s;
for(int i = 0 ; i < tokens.size() ; ++i)
{
if(isOperator(tokens[i]))
{
//弹出两个操作数
int rNum = stoi(s.top());
s.pop();
int lNum = stoi(s.top());
s.pop();
int result = do_operator(tokens[i][0],lNum,rNum);
s.push(to_string(result));
//计算结果
//将计算结果入栈
}
else
{
s.push(tokens[i]);
}
}
if(!s.empty()) return stoi(s.top());
return 0;
}
};
使用栈模拟队列
使用队列模拟栈
代码略
去除重复字母333
主要思路是,如果要让最终字符串最小,如果后面的字符b比前面的字符a小,则应该尽可能删除b,把a放到前面。
因此使用栈保存最终结果,遍历原始字符串A依次进栈内:当前字符A与栈顶元素存在3种大小关系
1.如果当前元素A比top 元素小
< a > 栈内已经存在字符A,则跳过当前元素 ,记录的剩余A的数目-1
< b > 栈内没有A存在
< i > 剩余top元素的数目>0 即A后面还有top存在,弹出top,继续判断A与弹出后栈的新top元素的关系
< ii > 剩余top元素数目==0.即后面没有top元素,则A入栈 A剩余数目-1,栈内A字符个数+1
2.如果当前元素A>top元素
栈内已有A,跳过,否则A入栈 ;剩余A数目-1
3.当前A == top 元素 ,跳过A 剩余A数目-1;
因此需要记录栈内每种字符的数量,以及剩余字符串内每种字符的数量
class Solution {
public:
string removeDuplicateLetters(string s) {
vector<char>st;
//入栈前
int left_count[26]; //用于记录原是字符串内每种字符数量
int stack_count[26];//用于记录栈内每种字符的数量
//初始化为0
for(int i=0;i<26;++i){
left_count[i]=0; //用于记录各个字符剩余的数量
stack_count[i]=0;//用于记录栈内各个字符的数量
}
//初始化原始字符串内每种字符数量
for(auto i:s){
left_count[i-'a']++; //初始化剩余字符的对应数量
}
for(int index=0;index<s.size();){
char a=s[index];
bool toNext=true;
//栈为空,直接入栈
if(st.empty()){
st.push_back(a);
//更新当前字符在栈内以及剩余数量
--left_count[a-'a'];
++stack_count[a-'a'];
}else{
auto top = st.back();
//<1>当前元素与栈顶元素相同,直接跳过,并剩余数量-1
if(a == top){
--left_count[a-'a'];
//<2>当前元素大于栈顶元素
}else if(a>top){
//栈内不存在当前元素,则该字符入栈,否则直接忽略该字符
if(stack_count[a-'a']== 0){
st.push_back(a);
++stack_count[a-'a'];
}
--left_count[a-'a'];
}else{ //当前元素小于栈顶元素,分类讨论
//栈内已有该字符,直接忽略该字符
if(stack_count[a-'a']>0){
--left_count[a-'a'];
}else{
//由于把当前元素放到当前top字符前面去,可以减少最终字符串大小
//因此如果剩余字符里面还有top,就把当前top弹出
//剩余字符里没有top了
if(left_count[top-'a'] == 0){
st.push_back(a);
--left_count[a-'a'];
++stack_count[a-'a'];
}else{
//剩余字符里还有top,也就是top可以弹出
st.pop_back();
--stack_count[top-'a'];
//注意,此时a继续与下一个top比较,并不是直接入栈
toNext=false;
}
}
}
}
if(toNext)++index;
}
string ret;
for(auto i:st) ret.push_back(i);
return ret;
}
};
检查是否为先序遍历序列333
这个方法简单的说就是利用栈访问过程中不断的砍掉叶子节点。最后看看能不能全部砍掉。只剩下一个NULL,也就是#符号
以例子一为例,:”9,3,4,#,#,1,#,#,2,#,6,#,#” 遇到x # #也就是叶子节点的时候,就把它变为 #
模拟一遍过程:
9,3,4,#,# => 9,3,# 继续读
9,3,#,1,#,# => 9,3,#,# => 9,# 继续读
9,#2,#,6,#,# => 9,#,2,#,# => 9,#,# => #
bool isValidSerialization(string preorder) {
vector<string>st;
string curr_node;
for(int i=0;i<preorder.size();++i){
if(preorder[i]==',') continue; //逗号分割字段
curr_node+=preorder[i];
if(i+1==preorder.size() || preorder[i+1]==','){ //当前字段分割完毕
st.push_back(curr_node);
//检查是否有叶子节点,有的话持续删除叶子节点
while(st.size()>=3 && st[st.size()-1]=="#" && st[st.size()-2]=="#" && st[st.size()-3] !="#"){
st.pop_back();
st.pop_back();
st[st.size()-1]="#";
}
curr_node.clear();
}
}
if(st.size()==1 && st.back()=="#"){
return true;
}
return false;
}
};
层次列表迭代器
此题的思路是,如果某列表嵌套多层列表,直到最后一层纯数字列表才能访问,即最深的最先访问到,因此使用栈。此外,对于同一层元素,第一个元素先于最后一个元素访问,因此对于同一层列表需要倒序入栈,即最后一个元素最先入栈。
class NestedIterator {
stack<NestedInteger>s;
public:
NestedIterator(vector<NestedInteger> &nestedList) {
//初始化时将列表倒序入栈
for(int i=nestedList.size()-1;i>=0;--i){
s.push(nestedList[i]);
}
}
int _next =0;
int next() {
return _next;
}
bool hasNext() {
while(!s.empty()){
if(s.top().isInteger()){
//纯数字元素,则直接访问
int ret = s.top().getInteger();
s.pop();
_next = ret;
return true;
}else{
//元素依然为列表,则继续将该列表展开,并倒序入栈
auto top = s.top();
s.pop();
auto nestedList = top.getList();
for(int i=nestedList.size()-1;i>=0;--i){
s.push(nestedList[i]);
}
}
}
return false;
}
};
解码字符串
递归是最直接思路
public String decodeString2(String s) {
if (s.length() == 0)
return "";
StringBuilder sb = new StringBuilder();
for (int i = 0; i < s.length(); i++) {
char c = s.charAt(i);
if (c >= '0' && c <= '9') {
// 解析次数
int digitStart = i++;
while (s.charAt(i) >= '0' && s.charAt(i) <= '9')
i++;
int num = Integer.parseInt(s.substring(digitStart, i));
// 找到对应的右括号
int strStart = i+1; // number must be followed by '['
int count = 1;
i++;
while (count != 0) {
if (s.charAt(i) == '[')
count++;
else if (s.charAt(i) == ']')
count--;
i++;
}
i--;
// 取子字符串
String subStr = s.substring(strStart, i);
// 将子字符串解码
String decodeStr = decodeString(subStr);
// 将解码的结果拼接到当前的字符串后面
for (int j = 0; j < num; j++) {
sb.append(decodeStr);
}
} else {
// 添加首元素
sb.append(c);
}
}
return sb.toString();
}
删除K个数字
类似于删除字符串重复字母,使得最小题目。此处也是使用栈缓存最终数字,如果当前数字i比栈顶数字小,则在删除指标k>0情况下,应该尽量删除当前栈顶元素,使得小数字尽量进位。
class Solution {
public:
string removeKdigits(string num, int k) {
string ret;
for(auto c:num){
while(k && ret.size() && c<ret.back()){//只要还存在删减指标,并且当前数小于前缀末尾,不断删除,从而使得当前小数字c前移
ret.pop_back();
--k;
}
if(!(ret.empty()&&c=='0')) ret.push_back(c);//不存在前缀0
}
//确保所有删除指标用完
while(ret.size() && (k--)){
ret.pop_back();
}
return ret.empty()?"0":ret;
}
};
单调栈
单调栈是在栈的性质上加入新的限制,即保持栈内元素单调增长或单调减少。也可是严格单调增长
或严格单调减少
对于给定栈st,数组A{1 3 5 2 8 1 4 4},假设要求栈从栈底到栈顶保持单调递增,则遍历数组A
栈底部 [ 1 当前元素1 栈为空,直接入栈
栈底部 [1 3 当前元素3大于栈顶元素1,因此栈递增性质不变,3直接入栈
栈底部[1 3 5 当前元素5,>top元素3,递增性质不变,直接入栈
栈底部[1 3 5 : 2 == 当前元素 2 < 5 破坏单调性质,弹出当前栈顶5
== > [1 3 : 2 继续弹出3 == > [1 2
栈底部[1 2 8 当前元素8>栈顶2 直接入栈
栈底部[1 2 8:1 == >[1 2 :1 == > [1:1 == > [1
栈底部[1:4 == >[1 4
栈底部[1 4 : 4 == >[1 4 当前元素==栈元素4,破坏了严格递增条件,因此弹出。
由以上过程可见,单调递增栈,当某元素a最终可以入栈时,此时栈顶元素top是其左边第一个<a的元素。而当a入栈后,由于某一个新的待入栈元素b破坏了当前栈的单调性,使得a需要弹出时,此时b是a在原数组中右边第一个<=于a的元素。
因此,利用单调递增栈,可以找到数组元素a左边第一个<a的元素以及a右边第一个<=a的元素。
由于每个元素只进栈出栈一次,因此时间复杂度为O(n)
对于单调递减栈,则可以找到元素a左边第一个大于a的元素和元素a右边第一个大于等于a的元素。
有时候,需要知道的是元素a距离其左边或右边第一个大于等于a元素的距离,此时栈内可以保存a在原始数组的索引。
总结:
假设 原始数组 {1 3 4 2},使用单调递增栈,当2要入栈时,破坏了单调递增性质
.----------
[ 1 3 4 <–2
.----------
此时首先弹出4,当4被弹出时,我们可以知道4的左边第一个<它的元素是新的栈顶元素3,而右边
第一个<4的元素是当前待入栈元素2.
.--------
[ 1 3 <–2
.--------
继续弹出3,当3被弹出后,我们可以知道3的左边第一个<它的元素是新的栈顶元素1,而右边
第一个<3的元素是当前待入栈元素2.
.--------
[ 1 2
.--------
2达到入栈条件,此时我们知道2的左边第一个<2的元素是当前栈顶元素1.
因此,以栈底在左边的严格单调递增栈为例:
当一个元素达到入栈条件时,我们可求得当前元素a左边第一个<a的元素,即为当前待入栈的栈顶元素
当一个元素被弹出时,我们可以求得当前元素a左端第一个<a的元素和右端第一个<=a的元素。
因此如果我们要求的是左右两端第一个小于或等于a的元素,需要等该元素被弹出时计算。此时为了
保证原始数组内所有元素一定会有被弹出的时候,我们可以在元素数组末尾添加一个比所有元素都小的值。
接雨水333
对于一个能蓄水的凹槽,一定是底部元素a与左右两端第一个>a的元素形成。
因此问题转化为求数组内任意一个元素a左右两端第一个>a的元素aL,aR
,因此可使用单调递增栈。由于是求面积,除了需要知道aL,aR的值,还需要到a的距离,因此栈内保存的是元素的索引。由于当a>右边所有元素时,一定不能形成蓄水凹槽,因此无需a一定被弹出。
class Solution{
public:
int trap(vector<int>&height){
if(height.size()<=2) return 0;
stack<int>s;
int vol=0;
for(int i=0;i<height.size();++i){
//当单调递减栈条件被打破时,说明当前top可以与当前top栈内左边第一个元素,以及当前待入栈height[i]形成凹槽 (如果待入栈元素==栈顶元素,形成的凹槽容积为0)
while(s.empty()==false && height[i]>=height[s.top()]){
int mid=height[s.top()];//当前要弹出元素的左右>自身的位置已经找到
s.pop();
if(!s.empty()){
vol+= (min(height[i],height[s.top()])-mid)*(i-s.top()-1);
}
}
s.push(i);
}
return vol;
}
直方图中最大矩形333
依次扫描以每个柱子a为高度所能形成的最大长方形,最终取最大的那个。
对于任何一个柱子a,以a为高度能形成的最大长方形的左边界在a左边第一个<a位置aL向右+1
即aL+1.同理,该长方形的右边界在a右边第一个<a位置aR向右-1.即aR-1.
因此问题转化为:对于任何元素a,找打该元素左右两端第一个<a的位置aL,aR,从而以a为高度
所能形成的最大长方形左右边界位置[aL+1,aR-1]长度为(aR-1)-(aL+1)+1=aR-aL-1
(注:单调递增栈只能找到右边第一个<=a的元素位置,当破坏单调性的元素aR==a时,此时虽然a的右边界可以向aR以及可能aR的更右边拓展。但是由于aR == a,因此a被弹出后,aR依然能计算以a为高度的长方形,且该长方形的右边界>aR,即包含了a以aR-1为右边界形成的长方形,所以最终求出的最大长方形面积不变)
由于要保证每个柱子能形成的最大长方形都能被计算到,因此需要每个元素都能被弹出,因此可以
在数组末尾添加一个比所有元素都小的元素-1.
class Solution {
public:
int largestRectangleArea(vector<int>& height) {
if(height.size() == 0) return 0;
if(height.size() == 1) return height[0];
//添加一个最小元素,保证原始每一个元素都能被弹出
height.push_back(-1);
int max_area = 0;
stack<int>s;
for(int i=0; i<height.size();++i){
//当前元素入栈不改变栈的单调递增性质,直接入栈
if(s.empty() || height[i] >= height[s.top()]){
s.push(i);
}else{
//当前元素<栈顶元素,破坏了栈顶有序状态
//需要将栈内不能维持有序状态的元素全部弹出
//此时被弹出元素的左右第一个小于当前元素的位置可求出
while((!s.empty())&& height[s.top()] > height[i]){
//求得被弹出元素形成的矩形面积。
//矩形高度
int h = height[s.top()];
s.pop();
//矩形的左边界索引 注意为空时的处理
int left_index = (s.empty()?-1:s.top())+1;
int right_index = i-1;
int width = right_index-left_index+1;
max_area = max(max_area,width*h);
}
//所有不能与当前元素形成有序状态的元素均被弹出,此时可以将当前元素push进入
s.push(i);
}
}
return max_area;
}
};
最大矩形
每一行可以看作直方图,从而利用单调栈
class Solution {
public:
int maxArea(vector<int>&height){
int area = 0;
height.push_back(-1);
stack<int>s;
for(int i=0;i<height.size();++i){
while(!s.empty() &&height[s.top()]>=height[i]){
int h=height[s.top()];
s.pop();
int left_border=s.empty()?0:s.top()+1;
int right_border=i-1;
int curr_area = h*(right_border-left_border+1);
area=max(curr_area,area);
}
s.push(i);
}
height.pop_back();
return area;
}
int maximalRectangle(vector<vector<char>>& matrix) {
vector<int>heights(matrix[0].size(),0);
int ret=0;
for(int i=0;i<matrix.size();++i){
for(int j=0;j<matrix[0].size();++j){
if(matrix[i][j]=='0'){
heights[j]=0;
}else{
heights[j]++;
}
}
ret=max(ret,maxArea(heights));
}
return ret;
}
};
132模式寻找
此处利用非严格单调递减栈的特殊性质,即对于一个被从非严格单调递减栈中弹出的元素a2,表示在数组a2的后方且入过栈的元素里必然存在一个数字b3>a2.它破坏了栈的单调性,才使得a2被弹出来,记录下这个a2,如果后面再碰到某个未入栈元素c1<a2。则找到了一个数列a2b3c1。 如果从数组的右边向左边遍历并依次入递减栈,则可以找到的形式为
c1b3a2,有c1<a2<b3。即为题意。
class Solution {
public:
bool find132pattern(vector<int>& nums) {
if(nums.size()<3) return false;
int a2 = INT_MIN;
stack<int>s;
for(int i=nums.size()-1;i>=0;--i){
int b3=nums[i];
while(!s.empty() && b3 > s.top()){
a2=s.top();
s.pop();
}
s.push(nums[i]);
int c1=nums[i];
if(c1 < a2) return true;
}
return false;
}
};
下一个更大元素1333
使用栈底在右边,从数组右边向左遍历的单调递减栈,即可找到每个元素左边第一个>的元素。
此处由于元素不重复,用map暂存。
class Solution {
public:
vector<int> nextGreaterElement(vector<int>& nums1, vector<int>& nums2) {
map<int,int>nextGreater;
stack<int>s;
for(int i=nums2.size()-1;i>=0;--i){
while(s.empty()==false && nums2[i]>s.top()){
s.pop();
}
nextGreater[nums2[i]]=s.empty()?-1:s.top();
s.push(nums2[i]);
}
vector<int>ret;
for(auto i:nums1){
ret.push_back(nextGreater[i]);
}
return ret;
}
};
下一个更大元素2
此处的小技巧是将数组翻倍,从而相当于环形展开。
阅读文章:特殊数据结构–单调栈
class Solution {
public:
//此处将数组翻倍后,将数组翻倍,相当于环形展开
vector<int> nextGreaterElements(vector<int>& nums) {
int n=nums.size();
vector<int>ret(n,0);
stack<int>s;
for(int i=2*n-1;i>=0;--i){
int ii=i%n;
while(s.empty()==false && nums[ii]>=s.top()){
s.pop();
}
ret[ii]=s.empty()?-1:s.top();
s.push(nums[ii]);
}
return ret;
}
};