一、前言
通常我们把栈归为一种基本的数据结构,同时它也是一种线性表结构,也就是说你要自己实现一个栈的数据结构,既可以用数组实现,也可以用链表实现。栈最主要的特点就是“先进后出”,因为栈只有一个入口和出口。
二、实现栈结构
根据栈的先进后出的特点,很容易设置栈结构的接口:入栈、出栈、判空、size()等,熟悉数据库的同学都知道数据库无非就是四种操作:增、删、改、查,其实对于一个数据结构的接口而言,也是这四种操作,就栈而言,入栈即增操作、出栈即删操作、由于栈是线性表结构,所以查和改操作都需要遍历整个栈结构。现在已经知道了栈的接口操作,我们就可以用线性表的方法来实现一个栈结构,其实也就两种,用链表或数组实现栈。
但是,在C++标准库中已经为我们实现了栈结构,而且是按照最高效率、最优的标准实现的,你可以放心的使用C++标准库提供的栈结构,以C++一贯的作风,其实现的栈结构是一个栈类型,定义在<stack>头文件中,使用的时候只需要#include该头文件就行。
根据C++STL的解释,或C++Primer(第五版P329)的解释,都把stack类型称为一个容器适配器(配接器),并没有称其为一个容器,尽管如此,你可以把stack看作是一个特殊的容器,所谓适配器(配接器),指的是一种机制,一个容器适配器使一个容器的行为看起来像另外一个容器,这句话说的是什么意思呢?这是因为C++的容器适配器都是基于基本容器实现的,比如stack就是基于queue实现的(默认,也可以自己显视的指定为vector),这也导致了任何stack的操作接口都是直接调用底层容器的操作来完成的,如stack的push操作(入栈)就是调用queue的push_back操作来完成的。下面给出STL中stack的定义文件:
//模板定义
template<class _Ty,class _Container = deque<_Ty> >
class stack
{
protected:
_Container c; //底层容器对象,_Container是指底层容器类型
public:
typedef stack<_Ty, _Container> _Myt; //类型别名定义
typedef _Container container_type;
typedef typename _Container::value_type value_type;
typedef typename _Container::size_type size_type;
typedef typename _Container::reference reference;
typedef typename _Container::const_reference const_reference;
stack(): c()
{
//默认构造函数,构造空栈,这里是调用其成员容器对象的默认构造函数
}
stack(const _Myt& _Right): c(_Right.c)
{
// construct by copying _Right
}
explicit stack(const _Container& _Cont): c(_Cont)
{
// construct by copying specified container
}
void push(value_type&& _Val)
{
//直接调用底层容器的操作实现stack自身接口
// insert element at beginning
c.push_back(_STD move(_Val));
}
bool empty() const
{
// test if stack is empty
return (c.empty());
}
size_type size() const
{
// test length of stack
return (c.size());
}
reference top()
{
// return last element of mutable stack
return (c.back());
}
const_reference top() const
{
// return last element of nonmutable stack
return (c.back());
}
void push(const value_type& _Val)
{
// insert element at end
c.push_back(_Val);
}
void pop()
{
// erase last element
c.pop_back();
}
};
关于上面的C++STL中stack的定义,你可以不了解,你只需要知道stack提供给你哪些接口,这些接口应该怎么用就行了,至于其内部实现,STL已经为你实现好了,完全不用你担心。
三、栈的应用
1、后缀表达式求值
以人类的思维,中缀表达式是正常的表达式形式,因为我们已经熟悉了各种运算符号的优先级,知道在一个表达式中第一个求哪一部分的值,最常见的就是先求括号内部,然后再求括号外部,但是这种求值顺序在计算机看来是很麻烦的,最好的办法是我们输入给计算机的表达式不需要知道操作符优先级,计算机只管按照我们输入的表达式从左到右求值即可,这就要用后缀表达式来实现。后缀表达式是针对中缀表达式而言的,大致可以理解为操作符在两个操作数之后,并不是像中缀表达式那样每两个操作数之间必须有一个操作符,后缀表达式最大的特点就是没有必要知道任何运算符的优先规则,如下就是一个后缀表达式:
“4.99 1.06 * 5.99 + 6.99 1.06 * + ”
其中缀表达式为:“4.99 * 1.06 + 5.99 + 6.99 * 1.06 ”(关于怎么从中缀表达式转为后缀表达式后面会介绍)
后缀表达式的求值规则为:从左到右扫描后缀表达式,如果遇到一个操作数,将其压入栈中,如果遇到一个操作符,则从栈中弹出两个操作数,计算结果,然后把结果入栈,直到遍历完后缀表达式,则计算完成,此时的栈顶元素即为计算结果,如上的后缀表达式求值过程为:
初始,栈空;步骤(1)
遇到操作数4.99,入栈;步骤(2)
遇到操作数1.06,入栈;步骤(3)
遇到操作符*,弹出栈中两个元素,计算结果入栈;步骤(4)
遇到操作数5.99,入栈;步骤(5)
遇到操作符+,弹出栈中两个元素,计算结果入栈;步骤(6)
遇到操作数6.99,入栈;步骤(7)
遇到操作数1.06,入栈;步骤(8)
遇到操作符*,弹出栈中两个元素,计算结果入栈;步骤(9)
遇到操作符+,弹出栈中两个元素,计算结果入栈;步骤(10)
C++实现代码如下(注意输入的后缀表达式每个元素之后一定要有一个空格,这用于分开不同的元素):
/*********************后缀表达式求值(直接利用C++STL提供的Stack实现)**************************/
double postfixExpression(const string &str)
{
stack<double> mystack; //栈空间
string s = ".0123456789+-*/";
string empty = " ";
string numbers = ".0123456789";
string c = "+-*/";
double firstnum;
double secondnum;
double sum;
for(unsigned int i=0; i<str.size(); )
{
string::size_type start = str.find_first_of(s,i); //查找第一个数字或算术符号
string::size_type end = str.find_first_of(empty,i); //查找第一个空格
string tempstr = str.substr(start, end-start); //取出这一个元素
//判断元素
if(tempstr == "+" || tempstr == "-" || tempstr == "*" || tempstr == "/")
{
secondnum = mystack.top(); //取当前栈顶元素,由于栈的先进后出特性,当前栈顶元素其实是二元操作符中右侧的操作数,如表达式3-2的后缀表达式为“3 2 -”,这里secondnum取得数就是2
mystack.pop();
firstnum = mystack.top();
mystack.pop();
if(tempstr == "+")
{
sum = firstnum + secondnum;
mystack.push(sum);
}
if(tempstr == "-")
{
sum = firstnum - secondnum;
mystack.push(sum);
}
if(tempstr == "*")
{
sum = firstnum * secondnum;
mystack.push(sum);
}
if(tempstr == "/")
{
sum = firstnum / secondnum;
mystack.push(sum);
}
}
else
{
double num = stod(tempstr);
mystack.push(num);
}
//控制迭代
i = end + 1;
}
return mystack.top();
}
代码测试如下:还是以上面那个例子的后缀表达式作为输入,则测试代码如下:
void main()
{
string Postfistr = "4.99 1.06 * 5.99 + 6.99 1.06 * + "; //每个元素后需要有一个空格“ ”字符串
double res = postfixExpression(Postfistr);
cout << res <<endl;
}
上述计算后缀表达式的前提是输入的表达式就是后缀表达式,但是一般我们给出的表达式为中缀表达式,这就需要先把中缀表达式转为后缀表达式。
2、中缀表达式转为后缀表达式
中缀表达式转为后缀表达式也有一定的规则,这个规则是根据操作符的运算优先级来定的,还是上面那个中缀表达式为:“4.99*1.06+5.99+6.99*1.06”,转为后缀表达式的规则为:
(1)这里定义一个操作符栈stack来保存遇到的操作符,还需要定义string作为后缀表达式输出返回;
(2)首先需要对输入的中缀表达式进行“切片”处理,所谓切片,即对所输入的中缀表达式进行元素分割,这里的每个元素要么是一个操作符(“+-*/()”),要么是一个操作数(“.0123456789”),把这些元素存储到一个vector<string>Inputvec中;
(3)然后依次遍历Inputvec中元素,根据其是操作数还是操作符来进行不同的“处理”。这里的处理规则为:
如果是操作数,则直接保存到输出string中;
如果是操作符
如果操作符栈为空,则把操作符入栈;
否则,则比较当前运算符与栈顶操作符优先等级;
如果当前操作符优先等级高,则当前操作符入栈;
否则,弹出栈顶操作符到输出string中;
中缀表达式转后缀表达式C++实现代码如下:
//设置操作符优先级,这里考虑到括号("("、")")匹配,定义设置左括号"("的优先级最高,且只有在遇到右括号时才弹出左括号
int priority(const string str)
{
const char *op = str.c_str();
switch(*op)
{
case ')':
return 0;
case '+':
case '-':
return 1;
case '*':
case '/':
return 2;
case '(':
return 3;
default :
return -1;
}
}
/*********************中缀表达式转为后缀表达式**************************/
string InfixToPostfi(const string &str)
{
string operatorstr = "*-/+()"; //用于string搜索
string numbers = "0123456789.";
//对输入的中缀表达式中每个元素进行切片,每个元素存储到vector<string>Inputstr
vector<string> Inputvec; //存储切片结果
for(unsigned int i=0; i<str.size(); )
{
string::size_type operatorindex = str.find_first_of(operatorstr,i); //搜索str中从i开始的第一个操作符
if(operatorindex != string::npos)
{
//如果从i开始搜索到了操作符
if(operatorindex == i)
{
//如果是两个连续的操作符,即这种形式的表达式 a*(b+c)+d;
string tempstr = str.substr(operatorindex,1);
Inputvec.push_back(tempstr);
i = i+1;
}
else
{
Inputvec.push_back(str.substr(i,operatorindex-i));
Inputvec.push_back(str.substr(operatorindex,1));
i = operatorindex+1;
}
}
else
{
//如果从i开始搜索到了操作符,即输入的中缀表达式以操作数结尾,不是以操作符结尾(其实一个表达式以操作符结尾的情况只可能是以右括号")"结尾,这里就是为防止这种特殊情况)
Inputvec.push_back(str.substr(i,str.size()-i));
i = str.size();
}
}
//遍历切片结果vector中每个元素
stack<string> operatorstack; //创建空栈,用来存储操作符
vector<string> PostfiOutvec; //存储中缀输出,这里是存储到vector
for(int i=0; i<Inputvec.size(); i++)
{
//如果当前元素是操作符
if(Inputvec[i].find_first_of(operatorstr) != string::npos)
{
if(operatorstack.empty())
{
operatorstack.push(Inputvec[i]); //如果操作符栈空,则直接入栈
}
else
{
if(Inputvec[i] == ")") //如果当前操作符是右括号
{
while(operatorstack.top() != "(")
{
PostfiOutvec.push_back(operatorstack.top()); //将栈顶操作符输出
operatorstack.pop(); //删除栈顶元素
}
operatorstack.pop(); //删除栈顶元素(这里是删除左括号"(")
}
else
{
int curpri = priority(Inputvec[i]); //获取操作符的优先级
//比较当前操作符与栈顶元素优先级,如果小于或等于栈顶元素优先级则弹出栈顶元素,否则当前操作符入栈
while(!operatorstack.empty())
{
string top = operatorstack.top(); //返回栈顶元素
int toppor = priority(top); //栈顶元素优先级
if((curpri <= toppor) && top!="(") //左括号优先级最大,但是它只有遇到右括号才输出
{
PostfiOutvec.push_back(top);
operatorstack.pop(); //删除栈顶元素
}
else
break;
}
operatorstack.push(Inputvec[i]);
}
}
}
//如果当前元素是操作数,直接输出
else
{
PostfiOutvec.push_back(Inputvec[i]);
}
}
while(!operatorstack.empty())
{
PostfiOutvec.push_back(operatorstack.top()); //输出操作符栈中的其他操作符
operatorstack.pop();
}
//在输出中插入空格
vector<string>::const_iterator itr=PostfiOutvec.begin()+1;
while(itr!=PostfiOutvec.end())
{
itr = PostfiOutvec.insert(itr," "); //这里一定要返回insert之后的指针,因为改变容器的操作会使迭代器失效
itr+=2;
}
PostfiOutvec.push_back(" "); //添加最后一个空格
//vector输出为string,作为后缀表达式结果返回
string result;
for(int i=0; i<PostfiOutvec.size(); i++)
{
result.append(PostfiOutvec[i]);
}
return result;
}
测试代码如下:
void main()
{
string Infixstr1 = "4.99*1.06+5.99+6.99*1.06"; //没有括号
string Infixstr2 = "4.99*1.06+5.99+(6.99*1.06)"; //中缀表达式以操作符结尾(这种情况只能是以右括号结尾)
string Infixstr3 = "4.99*(1.06+5.99)+6.99*1.06"; //括号在中间
string Infixstr4 = "4.99*1.06+5.99+()6.99*1.06"; //插入括号,其内没有表达式
string Postfistr1 = InfixToPostfi(Infixstr1);
string Postfistr2 = InfixToPostfi(Infixstr2);
string Postfistr3 = InfixToPostfi(Infixstr3);
string Postfistr4 = InfixToPostfi(Infixstr4);
double res1 = postfixExpression(Postfistr1);
cout << "res1=" << res1 <<endl;
double res2 = postfixExpression(Postfistr2);
cout << "res2=" << res2 <<endl;
double res3 = postfixExpression(Postfistr3);
cout << "res3=" << res3 <<endl;
double res4 = postfixExpression(Postfistr4);
cout << "res4=" << res4 <<endl;
}
以上测试代码中,测试了4中不同的中缀表达式形式,运行结果如下: