一、前言
通常我們把棧歸爲一種基本的數據結構,同時它也是一種線性表結構,也就是說你要自己實現一個棧的數據結構,既可以用數組實現,也可以用鏈表實現。棧最主要的特點就是“先進後出”,因爲棧只有一個入口和出口。
二、實現棧結構
根據棧的先進後出的特點,很容易設置棧結構的接口:入棧、出棧、判空、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中不同的中綴表達式形式,運行結果如下: