1.浏览器的返回功能可以用栈来实现,当前浏览的页面我们叫它为栈顶元素,跳转到一个新页面我们叫元素入栈,点击“返回”按钮我们叫元素出栈,当然出栈前提是栈里要有元素,比如在浏览器里,如果已经返回到了最开始的页面,那就无法返回了。栈有一个重要的性质,就是先进后出,First In Last Out(FILO)。
什么是先进后出?在浏览器的例子里,先打开的页面的一定是到后面才会被返回到,后面打开的页面一定是先被返回到的。这个过程,我们可以用栈这种数据结构来模拟实现。
2.栈的性质:
新元素插入栈顶(新打开的页面会加到栈顶)
栈顶元素先出(最后打开的页面会先被返回到)
栈为空不可以出栈(在最开始的页面不可以返回上一级)
3.入栈,出栈,访问栈顶元素等操作
1 #include<iostream> 2 #include<string> 3 using namespace std; 4 template<class Type>class Stack{ 5 private: 6 Type *urls; //定义一个Type类型的指针变量,用来存储浏览过网页的地址 7 int max_size,top_index; //栈里最多可以有的元素个数和栈顶元素的位置 8 public: 9 Stack(int length_input){ //构造函数 10 urls=new Type[length_input]; 11 max_size=length_input; 12 top_index=-1; //栈顶元素初始化为-1 13 } 14 ~Stack(){ //析构函数 15 delete[]urls; 16 } 17 bool push(const Type &element){ //入栈操作,为节省内存,在参数名前加一个引用,在参数类型前加一个const 18 if(top_index>=max_size-1){ //特殊情况,栈顶元素的位置已经大于栈的容量 19 return false; 20 } 21 top_index++; //普通情况,栈顶位置先加一,在把元素插入栈顶 22 urls[top_index]=element; 23 return true; 24 } 25 bool pop(){ //出栈操作 26 if(top_index<0){ //栈为空,不能出栈 27 return false; 28 } 29 top_index--; //否则栈顶元素位置向前移动一位,表示出栈 30 return true; 31 } 32 Type top(){ //访问栈顶元素 33 assert(top_index>=0); //宏断言,判断栈是否为空,为空则结束程序 34 return urls[top_index]; //不为空则返回栈顶元素 35 } 36 bool empty(){ //判断栈是否为空 37 if(top_index<0){ 38 return true; 39 } 40 else{ 41 return false; 42 } 43 } 44 }; 45 int main() { 46 int n,num; 47 cin>>n; 48 Stack<int> stack(n); 49 for(int i=1;i<=n;i++){ 50 cin>>num; 51 stack.push(num); 52 } 53 while(!stack.empty()){ //实现数列的翻转 54 cout<<stack.top()<<" "; 55 stack.pop(); 56 } 57 return 0; 58 }
4.栈的应用:表达式的求值
1 #include<iostream> 2 #include<string> 3 #include<cassert> 4 using namespace std; 5 template<class Type> class Stack { 6 private: 7 Type *urls; 8 int max_size, top_index; 9 public: 10 Stack(int length_input) { 11 urls = new Type[length_input]; 12 max_size = length_input; 13 top_index = -1; 14 } 15 ~Stack() { 16 delete[] urls; 17 } 18 bool push(const Type &element) { 19 if (top_index >= max_size - 1) { 20 return false; 21 } 22 top_index++; 23 urls[top_index] = element; 24 return true; 25 } 26 bool pop() { 27 if (top_index < 0) { 28 return false; 29 } 30 top_index--; 31 return true; 32 } 33 Type top() { 34 assert(top_index >= 0); 35 return urls[top_index]; 36 } 37 bool empty() { 38 if (top_index < 0) { 39 return true; 40 } else { 41 return false; 42 } 43 } 44 }; 45 bool precede(char a, char b) { //判断两个运算符a和b的优先级,如果a的优先级高则返回true 46 if (a == '*') { 47 return true; 48 } else { 49 return false; 50 } 51 } 52 int operate(char theta, int a, int b) { //根据运算符theta计算两个数a和b的值,并返回计算结果 53 if (theta == '+') { 54 return a + b; 55 } else { 56 return a * b; 57 } 58 } 59 void calc(Stack<int> &numbers, Stack<char> &operators) { //根据运算符栈顶的运算符,计算数字栈顶的两个数的结果,并把结果加入到数字栈里 60 int a=numbers.top(); 61 numbers.pop(); 62 int b=numbers.top(); 63 numbers.pop(); 64 numbers.push(operate(operators.top(),a,b)); 65 operators.pop(); 66 } 67 int main() { 68 int n; //输入的表达式长度 69 cin>>n; 70 Stack<int> numbers(n); 71 Stack<char> operators(n); 72 string buffer; //输入的表达式 73 cin>>buffer; 74 int i=0; 75 while(i<n){ 76 if(isdigit(buffer[i])){ //是数字则化成int型后加入到数字栈 77 numbers.push(buffer[i]-'0'); 78 i++; 79 } 80 else{ //是运算符的情况 81 if(operators.empty()||precede(buffer[i],operators.top())){ //运算符栈为空或当前运算符的优先级大于栈顶的符号的优先级 82 operators.push(buffer[i]); 83 i++; 84 } 85 else{ 86 calc(numbers,operators); 87 } 88 } 89 90 } 91 while(!operators.empty()){ 92 calc(numbers,operators); 93 } 94 cout<<numbers.top()<<endl; 95 return 0; 96 }
5.单调栈
单调栈就是栈内元素单调递增或者单调递减的栈,这一点和单调队列很相似,但是单调栈只能在栈顶操作。
借用拿号排队的场景来说明下。现在有很多人在排队买蒜味可乐,每个人手里都拿着号,越靠前的人手里的号越小,但是号不一定是连续的。一个人拿了号后并没有去排队,而是跑去约会了。等他回来后,发现队伍已经排得很长了,他不能直接插入到队伍里,不然人家以为他是来插队的。他只能跑到队伍最后,挨个询问排队人手里的号,蒜头君认为号比他大的人都是“插队”的,蒜头君就会施魔法把这些人变消失,直到蒜头君找到号比他小的为止。
在上面这个场景里,大家排的队伍就像是单调栈,因为大家手里拿的号是单调递增的。蒜头君找自己位置的这个过程就是元素加入单调栈的过程。新加入的元素如果加到栈顶后,栈里的元素不再是单调递增了,那么我们就删除加入前的栈顶元素,就像施魔法把“插队”的人变消失一样。直到新元素加入后,栈依然是单调递增时,我们才把元素加进栈里。
给定一个包含若干个整数的数组,我们从第 1 个元素开始依次加入单调栈里,并且加入后更新单调栈。那么单调栈有这样的性质:对于单调递增的栈,如果此时栈顶元素为 b,加入新元素 a 后进行更新时,如果 a 大于 b,说明 a 在数组里不能再往左扩展了,也就是说,如果从 a 在数组中的位置开始往左边遍历,则 b 一定是第一个比 a 大的元素;如果 a 小于 b,说明在数组里,a 前面至少有一个元素不能扩展到 a 的位置,也就是对于这些元素来说,a 是其在数组右侧第一个比它小的元素。
单调栈的维护是 O(n) 级的时间复杂度,因为所有元素只会进入栈一次,并且出栈后再也不会进栈了。
单调栈的性质:
- 使用单调栈可以找到元素向左遍历第一个比他大的元素
- 元素加入栈前,会在栈顶端把破坏栈单调性的元素都删除
- 单调栈里的元素具有单调性
eg:我们来看看这样一道题:地上从左到右竖立着 n 块木板,从 1 到 n 依次编号,如下图所示。我们知道每块木板的高度,在第 n 块木板右侧竖立着一块高度无限大的木板,现对每块木板依次做如下的操作:对于第 i 块木板,我们从其右侧开始倒水,直到水的高度等于第 i 块木板的高度,倒入的水会淹没 ai 块木板(如果木板左右两侧水的高度大于等于木板高度即视为木板被淹没),求 n 次操作后,所有 ai 的和是多少。如图上所示,在第 4 块木板右侧倒水,可以淹没第 5 块和第 6 块一共 2 块木板,a4 = 2。
答案:
我们来分析下,什么时候水的高度会等于第 i 块木板的高度 hi 呢,一定是水往右边蔓延遇到了一块高度大于等于 hi 的木板 j,ai 就等于木板 i 和木板 j 之间的木板数。于是,问题就变成了寻找在第 i 个数右边第一个比它大的数。可以暴力求解,从 1 循环到 n,对每块木板再往右循环一遍,这样的时间复杂度是 O(n2) 的。有没有高效一点的做法呢?我们回想下单调栈的性质,可以在某点左右扩展出一段连续区间,且该点在区间里始终保证是最值。是不是和这题很像呀,而且这道题只要看点右侧扩展出来的区间即可。那么,接下来我们就用单调栈来解这道题吧。
1 #include<iostream> 2 #include<cassert> 3 using namespace std; 4 class Node { 5 public: 6 int id, height; 7 }; 8 template<class Type> class Stack { 9 private: 10 Type *urls; 11 int max_size, top_index; 12 public: 13 Stack(int length_input) { 14 urls = new Type[length_input]; 15 max_size = length_input; 16 top_index = -1; 17 } 18 ~Stack() { 19 delete[] urls; 20 } 21 bool push(const Type &element) { 22 if (top_index >= max_size - 1) { 23 return false; 24 } 25 top_index++; 26 urls[top_index] = element; 27 return true; 28 } 29 bool pop() { 30 if (top_index < 0) { 31 return false; 32 } 33 top_index--; 34 return true; 35 } 36 Type top() { 37 assert(top_index >= 0); 38 return urls[top_index]; 39 } 40 bool empty() { 41 if (top_index < 0) { 42 return true; 43 } else { 44 return false; 45 } 46 } 47 }; 48 int main() { 49 int n,ans=0; 50 cin>>n; 51 Stack<Node> stack(n); 52 Node temp; 53 for(int i=1;i<=n;i++){ 54 cin>>temp.height; 55 temp.id=i; 56 while(!stack.empty()&&stack.top().height<=temp.height){ 57 ans=ans+i-stack.top().id-1; 58 stack.pop(); 59 } 60 stack.push(temp); 61 } 62 while(!stack.empty()){ 63 ans=ans+n+1-stack.top().id-1; 64 stack.pop(); 65 } 66 cout<<ans<<endl; 67 return 0; 68 }