【栈及其应用】
【栈及其应用】
栈是一种“后进先出”线性序列结构,和数组这种同是线性序列的结构相比,栈的一端被“封”住,元素进入和出去都只允许从某一端。
STL 里的栈
#include<stack> //要使用STL里的栈就要先引入这个头文件
template<typename T>
stack<T> sta; //声明各种类型的栈,除了C++内置的基本类型,栈的元素也可以是自定义的结构体
具体如:
stack<int> sta;
stack<double> sta;
stack<char> sta;
stack<string> sta;
..........
stack<int> sta;
sta.push(1); //往栈中放入一个元素,新元素成为新的栈顶元素
sta.pop(); //从栈顶弹出一个元素
sta.top(); //返回当前栈顶元素
sta.empty(); //判断栈是否为空
sta.size(); //查看当前栈中有几个元素
栈的应用
1.逆序输出 — 进制转换
十进制转其他进制
如: 121(10) = 1111000(2)
12345 (10) = 30071(8)
其他进制转十进制
如: 1111001(2) = 1*20 + 0 * 21 + 0 * 22 + 1 * 23 + 1 * 24 + 1 * 25 + 1 * 26 = 121
30071(8) = 1*80 + 7 * 81 + 0 * 82 + 0 * 83 + 3 * 84 = 12345
推广: 任何 k 进制的数bn bn-1 .....b3 b2 b1 b0转为十进制数
bn bn-1 .....b3 b2 b1 b0 = b0*k0 + b1 * k1 + b2 * k2 + b3 * k3 + .......+ bn-1 * kn-1 + bn * kn
十进制与其他进制相互转换示例代码如下:
#include<iostream>
#include<stdio.h>
#include<stack>
using namespace std;
//把十进制数转为base进制
stack<int> dCovert(int n,int base)
{
stack<int> sta;
while(n > 0)
{
sta.push(n%base);
n /= base;
}
return sta;
}
//将base进制的数转为十进制数
int covertD(stack<int> sta,int base)
{
int n = 0;
int i;
while(!sta.empty())
{
n = n*base + sta.top();
sta.pop();
}
return n;
}
int main()
{
stack<int> sta;
sta = dCovert(120,2);
stack<int> sta_cp = sta;
cout<<"十进制转二进制:";
while(!sta.empty())
{
cout<<sta.top();
sta.pop();
}
cout<<endl;
cout<<"二进制转十进制:"<<covertD(sta_cp,2)<<endl;
return 0;
}
2.递归嵌套 — 栈混洗,括号匹配
栈混洗
栈混洗:存在3个栈 A,B,S,其中只有栈 A 不为空,栈 B 和栈 S 都为空,现在通过某种次序将栈 A中的元素push到 B 中 ,再从 B 中push到S中,最终通过有限次的倒腾(也就是3个栈的push,pop操作),使得最后A,B栈为空,S栈不为空。我们通常称这样一个过程之为栈混洗。
先来看一个栈混洗的题目:
现有3个栈A、B、S,其中A中的元素为 [ 1 2 3 4 > , 将A中的元素以某种次序出栈,S作为临时放的栈,最终A中元素要push到B中,请问下列哪种不可能是最终B中元素次序 (注: 符号 [ 表示栈底,> 表示栈顶)
a. [ 1 2 3 4 > b. [4 3 2 1 > c. [ 3 2 1 4 > d. [ 2 1 4 3>
解答: a , b, c 答案都是 [ 1 2 3 4 > 可能的栈混洗;d 不可能,因为 2 1 最后位于栈底,那么 4 3 是先出栈放到临时栈S中的,即 [ 4 3 > , 从S中出栈入到B中只能是先出3,B变为[ 2 1 3 >,再出4,B变为[ 2 1 3 4 >,所以不可能是 [ 2 1 4 3>
括号匹配
检查某一表达式或字符串中的括号是否匹配
如:
(( 0 + (1 + 23)/4 * 5 * 67 - 8 + 9 ))
{ ( [ ] ) } ( )
方法:从前往后扫描字符串,遇到左括号先压入栈,遇到右括号就将栈顶元素弹出,看是否与当前扫描的右括号相匹配,匹配就继续扫描,直到扫描结束,所有压入栈中的左括号都找到与之匹配的右括号而并被弹出,栈为空。否则中间任有一次不匹配都认为整个字符串括号不匹配。
括号匹配示例代码:
#include<stdio.h>
#include<stack>
using namespace std;
/*
检查括号是否匹配
*/
bool braketMatch(char str[])
{
stack<char> sta;
char* p = str;
while(*p != '\0')
{
switch(*p)
{
case '(':
sta.push(*p);
break;
case '[':
sta.push(*p);
break;
case '{':
sta.push(*p);
break;
case ')':
if(sta.top() != '(')
return false; //有一次不匹配直接返回false,退出
sta.pop(); //否则弹出当前左括号,准备进行下一轮匹配检查或者检查已结束
break;
case ']':
if(sta.top() != '[')
return false;
sta.pop();
break;
case '}':
if(sta.top() != '{')
return false;
sta.pop();
break;
}
p++;
}
if(!sta.empty())//最后还要判断栈是否为空,不为空则有左括号没有找到匹配,则判定字符串不匹配
return false;
return true;//经过了层层考验终于来到这里
}
int main()
{
char* str1 = "{ [ ()} ()";
char* str2 = "( ( 0 + ( 1 + 23 ) /4*5*67-8+9))";
bool match1 = braketMatch(str1);
bool match2 = braketMatch(str2);
if(match1)
printf("str1: %s is matching\n",str1);
else
printf("str1: %s is not matching\n",str1);
if(match2)
printf("str2: %s is matching\n",str2);
else
printf("str2: %s is not matching\n",str2);
return 0;
}
3.延迟缓冲 —表达式求值
中缀表达式求值
如:
30 / 90 - 26 + 97 - 5 - 6 - 13 / 88 * 6 + 51 / 29 + 79 * 87 + 57 * 92
(( 0 + (1 + 23)/4 * 5 * 67 - 8 + 9 ) )
不能直接从左到右直接进行计算,因为后面可能存在高优先级的运算符要比前面低优先级的运算符先进行计算。所以一可以用栈作为缓存保存前面待运算的数值和运算符,等待该它们运算的时候再取出来运算。
逻辑:
- 运算符之间存在优先级关系,通常要事先约定好,如 * / 比 + - 要先计算
- 从前往后扫描中缀表达式,后面的运算符因为优先级比较高可能会先计算,所以前面某些数和低优先级的运算符要先缓存,等待后面的运算完之后才能运算。
- 一般都是有了两个数,一个运算符之后可以计算一个结果,计算的结果又可以作为等待被计算的数。
- 由以上分析可以考虑建立两个栈,一个存放数值,另一个存放运算符。从前往后扫描,遇到数的时候直接入操作数栈,而遇到运算符先与栈顶运算符进行比较,如果栈顶运算符优先级比较高,此时栈顶运算符可以取出来运算了,同时取出操作数栈里的两个操作数一起运算,将运算结果再次存入到操作数栈中。
细节:
-
初始时,操作数栈和运算符栈均为空,第一个运算符怎么和栈顶运算符做比较?
答:为了实现循环的自动化,统一算法的处理流程,这里不妨初始时将先 ‘\0’也当做一个运算符 ,人为设置其第一个入栈,将 ‘\0’ 的优先级设置为最低,也就是说其他任何运算符和它比较优先级都比它高从而入栈。其实可以把‘\0’看成一个哨兵,这个哨兵在遇到其他运算符时都让它们入栈,而当扫描到表达式末尾,遇到表达式结束标志字符‘\0’时结束表达式运算。
2.什么时候表达式运算结束?
答:可以考虑扫描到字符串末尾的时候结束。或是当运算符栈为空时结束运算,前面已经把 '\0' 放入栈中,正 常的运算符都比它优先级高,所以它不能出栈,C语言字符串都是以‘\0’结束的,所以当扫描到结束的‘\0’字符 时,让此时栈内唯一剩下的‘\0’出栈,它的使命结束了,‘\0’被出栈了,栈就为空了,也即表达式运算结束。
- 除了‘)’,‘\0’,每个运算符都要入栈,只有入栈了,当扫描到其他运算符和它进行比较时,才有出栈运算的机会
先建立一个运算符优先级表,可以快速的找到运算符之间的关系,也避免代码中使用复杂冗余的if语句,使得代码更直观简洁高效。
运算符优先级表:
/*
操作符优先级比较表
*/
//当前运算符 + - * / ( ) '\0'
char pri[7][7] = {
/* 栈 + */ {'>', '>', '<', '<', '<', '>','>'},
/* 顶 - */ {'>', '>', '<', '<', '<', '>','>'},
/* 运 * */ {'>', '>', '>', '>', '<', '>','>'},
/* 算 / */ {'>', '>', '>', '>', '<', '>','>'},
/* 符 ( */ {'<', '<', '<', '<', '<', '=',' '},
/* ) */ {' ', ' ', ' ', ' ', ' ', ' ',' '},
/* '\0' */ {'<', '<', '<', '<', '<', ' ','='}
这个表什么意思?
想要这样的效果 pri[‘+’][‘*’] = ‘<’,pri[‘*’][‘/’] = ‘>’等,即栈顶运算符和当前运算符比较得到的关系,比如'+’和‘*’的到<,那么当前扫描到的运算符压栈
经过不同表达式的分析,可以得到栈顶运算符和当前运算符存在三种关系:
关系 操作
< 栈顶运算符优先级低于当前运算符,当前运算符压栈
> 栈顶运算符优先级高于当前元素运算符,栈顶运算符和对应操作数出栈
= 栈顶运算符优先级等同于当前元素运算符,栈顶运算符出栈
但是字符不能作为下标,所以先建立一个字符和数字之间的对应关系
先对 + - * / ( ) '\0' 这6个符号和 0 1 2 3 4 5 建立一个映射关系
map<char,int> opMap;
//建立运算符和下标的关系
opMap.insert(make_pair('+',0));
opMap.insert(make_pair('-',1));
opMap.insert(make_pair('*',2));
opMap.insert(make_pair('/',3));
opMap.insert(make_pair('(',4));
opMap.insert(make_pair(')',5));
opMap.insert(make_pair('\0',6));
那么 pri[opMap['+']][opMap['+']]就可以得到 '>',同理pri[opMap['+']][opMap['*']] = ‘<’
步骤:
- 读入表达式,预处理去掉空格(空格位于数字和符号之间)
- 建立运算符优先级关系表
- 扫描表达式,分析表达式中的数字和运算符,从表达式中提取出操作数和运算符,根据运算符的优先级关系分为几种情况: ‘<’ 运算符入栈,并继续扫描; '>' 取出栈顶运算符和操作数做运算; ‘=’ 取出栈顶运算符丢弃,并继续扫描。
- 重复步骤3直至 运算符栈为空
示例代码:
测试数据:
30 / 90 - 26 + 97 - 5 - 6 - 13 / 88 * 6 + 51 / 29 + 79 * 87 + 57 * 92
( ( 0 + ( 1 + 23 ) / 4 * 5 * 67 - 8 + 9 ) )
中缀表达式求值示例代码:
#include<iostream>
#include<stack>
#include<string.h>
#include<stdio.h>
#include<iomanip>
#include<map>
#include <sstream>
using namespace std;
/*
测试用例:
30 / 90 - 26 + 97 - 5 - 6 - 13 / 88 * 6 + 51 / 29 + 79 * 87 + 57 * 92
0
*/
const int NUMOP = 7;
/*
操作符优先级比较表
*/
//当前运算符 + - * / ( ) '\0'
char pri[NUMOP][NUMOP] = {
/* + */ {'>', '>', '<', '<', '<', '>','>'},
/* - */ {'>', '>', '<', '<', '<', '>','>'},
/* * */ {'>', '>', '>', '>', '<', '>','>'},
/* / */ {'>', '>', '>', '>', '<', '>','>'},
/* ( */ {'<', '<', '<', '<', '<', '=',' '},
/* ) */ {' ', ' ', ' ', ' ', ' ', ' ',' '},
/* '\0' */ {'<', '<', '<', '<', '<', ' ','='}
};
map<char,int> opMap;
void createOpTable()
{
//建立运算符和下标的关系
opMap.insert(make_pair('+',0));
opMap.insert(make_pair('-',1));
opMap.insert(make_pair('*',2));
opMap.insert(make_pair('/',3));
opMap.insert(make_pair('(',4));
opMap.insert(make_pair(')',5));
opMap.insert(make_pair('\0',6));
}
/*
判断当前的字符是操作符还是数,是数则返回true,否则返回false
并且从字符串中提取数
*/
bool readNumber(char* &sp,double &number)
{
char num_s[9] = {0};
if(*sp >= '0' && *sp <= '9'){
int i=0;
while(*sp >= '0' && *sp <= '9')
{
num_s[i++] = *sp;
sp++;
}
sscanf(num_s,"%lf",&number);
return true;
}else{
return false;
}
}
int main()
{
createOpTable();
string str;
string RPN; //逆波兰表达式
while(getline(cin,str) && str!="0"){
stack<double> numberStack;
stack<char> operatorStack;
operatorStack.push('\0');
//删除表达式str中的空格
for(int i=0;i<str.length();i++)
{
if(str[i]==' '){
str.erase(i,1);
}
}
// cout<<"after initial:"<<str<<endl;
char stingstr[210]={0};
//将string转为str[]数组
str.copy(stingstr,210);
//设置字符指针,用来遍历表达式
char* sp = stingstr;
while(!operatorStack.empty())
{
double number;
char op;
if(readNumber(sp,number))
{
numberStack.push(number);
cout<<"number:"<<number<<endl;
char ss[5];
sprintf(ss,"%d",(int)number);
string strnumber = ss;
RPN += strnumber;//数字直接追加到RPN后
}
else
{
op = *sp;
char stackOp = operatorStack.top();
cout<<"stackOp:"<<stackOp<<" cuOp:"<<op<<endl;
char order = pri[opMap[stackOp]][opMap[op]];
switch(order){
case '<':
operatorStack.push(op);
sp++;
break;
case '>':{
double numB = numberStack.top();
numberStack.pop();
double numA = numberStack.top();
numberStack.pop();
double numSum;
switch(stackOp){
case '+':
numSum = numA + numB;
printf("%f + %f = %f\n",numA,numB,numSum);
break;
case '-':
numSum = numA - numB;
printf("%f - %f = %f\n",numA,numB,numSum);
break;
case '*':
numSum = numA * numB;
printf("%f * %f = %f\n",numA,numB,numSum);
break;
case '/':
numSum = numA / numB;
printf("%f / %f = %f\n",numA,numB,numSum);
break;
}
numberStack.push(numSum);
operatorStack.pop();
RPN += stackOp;//运算符该计算的时候追加到RPN后
}
break;
case '=':
operatorStack.pop();
sp++;
break;
default:
break;
}
}
}
if(!numberStack.empty())
{
cout<<"final result: "<<endl;
cout<<fixed << setprecision(2)<<numberStack.top()<<endl;
}
cout<<"逆波兰表达式 RPN:"<<RPN<<endl;
}
return 0;
}
4.逆波兰表达式RPN
将中缀表达式转为后缀表达式即为逆波兰表达式,RPN(Reversal Polish Notation)
如:
中缀表达式: 30 / 90 - 26 + 97 - 5 - 6 - 13 / 88 * 6 + 51 / 29 + 79 * 87 + 57 * 92
其对应逆波兰表达式: 30 90 / 26 - 97 + 5 - 6 -13 88 / 6 * - 51 29 / + 79 87 * + 57 92 * +
中缀表达式: (( 0 + (1 + 23)/4 * 5 * 67 - 8 + 9 )
其对应逆波兰表达式: 0 1 23 + 4 / 5 * 67 * + 8 - 9 +
RPN干什么的,有什么用,为什么还要有RPN?
-
RPN的好处就是每一个运算符的紧紧跟在其操作数之后,而并不需要事先定义各种运算符的优先级,只需从头到尾的一趟扫描就可以完成计算,不像中缀表达式一样需要各种条件控制逻辑,从而大大加速了计算的速度。
-
RPN中操作数的顺序和原来的中缀表达式中的顺序是一致的
求RPN表达式计算结果
RPN计算模拟过程:
如上面所示的RPN表达式 0 1 23 + 4 / 5 * 67 * + 8 - 9 +
- 从左往右扫描,扫描到 + , 然后将前面两个操作数进行运算 1 + 23 = 24,结果放在原来的位置 ,即此时表达式变为
- 0 24 4 / 5 * 67 * + 8 - 9 +
- 继续向前扫描,扫描到 / , 然后将前面两个操作数进行运算 24 / 4 = 6 ,结果放在原来的位置 ,即此时表达式变为
- 0 6 5 * 67 * + 8 - 9 +
- 继续向前扫描,扫描到 * , 然后将前面两个操作数进行运算 6 * 5 = 30 ,结果放在原来的位置 ,即此时表达式变为 0 30 67 * + 8 - 9 +
- 继续向前扫描,扫描到 * , 然后将前面两个操作数进行运算 6 * 5 = 30 ,结果放在原来的位置 ,即此时表达式变为 0 30 67 * + 8 - 9 +
- 继续向前扫描,扫描到 * , 然后将前面两个操作数进行运算 30 * 67= 2010 ,结果放在原来的位置 ,即此时表达式变为 0 2010 + 8 - 9 +
- 继续向前扫描,扫描到 + , 然后将前面两个操作数进行运算 0 * 2010= 2010 ,结果放在原来的位置 ,即此时表达式变为 2010 8 - 9 +
- 继续向前扫描,扫描到 - , 然后将前面两个操作数进行运算 2010 - 8 = 2002,结果放在原来的位置 ,即此时表达式变为 2002 9 +
- 继续向前扫描,扫描到 + , 然后将前面两个操作数进行运算 2002 + 9 = 2011,结果放在原来的位置 ,即此时表达式变为 2011 ,也即最后的结果
RPN表达式求值示例代码:
#include<stdio.h>
#include<stack>
using namespace std;
int main()
{
stack<double> sta;
// char* RPN = "0 1 23 + 4 / 5 * 67 * + 8 - 9 +";
char* RPN = "30 90 / 26 - 97 + 5 - 6 -13 88 / 6 * - 51 29 / + 79 87 * + 57 92 * +";
char *p = RPN;
while(*p != '\0')
{
if(*p >= '0' && *p <= '9'){
double number = 0;
while(*p != '\0' && *p >= '0' && *p <= '9')
{
number = number*10 + (*p - '0');
p++;
}
sta.push(number);
}else if(*p =='+' ||*p =='-'||*p =='*'||*p =='/' ){
double tempRes;
double numB = sta.top();
sta.pop();
double numA = sta.top();
sta.pop();
switch(*p){
case '+':
tempRes = numA + numB;
break;
case '-':
tempRes = numA - numB;
break;
case '*':
tempRes = numA * numB;
break;
case '/':
tempRes = numA / numB;
break;
}
sta.push(tempRes);
}
p++;
}
printf("result:%.2f\n",sta.top());
return 0;
}
中缀表达式转后缀表达式
在之前的表达式求值的代码中,其实也在中缀表达式求值的过程中顺便将中缀表达式转换成了后缀表达式也即RPN,详情可以见表达式求值代码注释。这里单独把中缀表达式转后缀表达式的代码拎出来。
中缀表达式转后缀表达式示例代码:
#include<iostream>
#include<stdio.h>
#include<stack>
#include<string>
#include<map>
using namespace std;
const int MAXN = 300;//表达式长度
const int NUMOP = 7;
/*
操作符优先级比较表
*/
//当前运算符 + - * / ( ) '\0'
char pri[NUMOP][NUMOP] = {
/* 栈顶 + */ {'>', '>', '<', '<', '<', '>','>'},
/* - */ {'>', '>', '<', '<', '<', '>','>'},
/* * */ {'>', '>', '>', '>', '<', '>','>'},
/* / */ {'>', '>', '>', '>', '<', '>','>'},
/* ( */ {'<', '<', '<', '<', '<', '=',' '},
/* ) */ {' ', ' ', ' ', ' ', ' ', ' ',' '},
/* '\0' */ {'<', '<', '<', '<', '<', ' ','='}
};
map<char,int> opMap;
void createOpTable()
{
//建立运算符和下标的关系
opMap.insert(make_pair('+',0));
opMap.insert(make_pair('-',1));
opMap.insert(make_pair('*',2));
opMap.insert(make_pair('/',3));
opMap.insert(make_pair('(',4));
opMap.insert(make_pair(')',5));
opMap.insert(make_pair('\0',6));
}
/*
判断当前的字符是操作符还是数,是数则返回true,否则返回false
并且从字符串中提取数
*/
bool readNumber(char* &sp,double &number)
{
char num_s[9] = {0};
if(*sp >= '0' && *sp <= '9'){
int i=0;
while(*sp >= '0' && *sp <= '9')
{
num_s[i++] = *sp;
sp++;
}
sscanf(num_s,"%lf",&number);
return true;
}else{
return false;
}
}
/*
中缀表达式转RPN表达式
*/
string InOrderToRPN(string str)
{
createOpTable();
string RPN; //逆波兰表达式
stack<char> operatorStack;
operatorStack.push('\0');
//删除表达式str中的空格
for(int i=0;i<str.length();i++)
{
if(str[i]==' '){
str.erase(i,1);
}
}
cout<<"str:"<<str<<endl;
char stringstr[MAXN]={0};
str.copy(stringstr,MAXN);
//设置字符指针,用来遍历表达式
char* sp = stringstr;
while(!operatorStack.empty())
{
double number;
char op;
if(readNumber(sp,number))
{
char ss[5];
sprintf(ss,"%d",(int)number);
string strnumber = ss;
RPN += strnumber;//数字直接追加到RPN后
}
else
{
op = *sp;
char stackOp = operatorStack.top();
char order = pri[opMap[stackOp]][opMap[op]];
switch(order){
case '<':
operatorStack.push(op);
sp++;
break;
case '>':{
operatorStack.pop();
RPN += stackOp;//运算符该计算的时候追加到RPN后
}
break;
case '=':
operatorStack.pop();
sp++;
break;
default:
break;
}
}
}
return RPN;
}
int main()
{
string str = "( ( 0 + ( 1 + 23 ) /4*5*67-8+9))";
cout<<"逆波兰表达式 RPN:"<<InOrderToRPN(str)<<endl;
return 0;
}
手工转换:
将中缀表达式:(( 0 + (1 + 23)/4 * 5 * 67 - 8 + 9 )) 按照二叉树中序遍历还原原本的二叉树如下图所示:
有了这颗二叉树,我们就可以得到它的后序遍历: 0 1 23 + 4 / 5 * 67 * + 8 - 9 +
这个后序遍历和我们之前的逆波兰表达式是一样的,所以说给一个中序遍历的表达式,求RPN,也就是在求中序遍历对应的二叉树的后序遍历。