一、問題描述
表達式求值是數學中的一個基本問題,也是程序設計中的一個簡單問題。我們所涉及的表達式中包含數字和符號,本實驗中處理的符號包括‘+’、‘-’、‘*’、‘/’、‘(’和‘)’,要求按照我們所習慣的計算順序,正確計算出表達式的值,並輸出至屏幕上。
本實驗采用的數據結構是棧,表達式求值的問題用棧來實現是十分合適的。本實驗用兩個棧分別保存表達式中的數字和符號,以確定每對符號相遇時的優先級來決定當前應該進行什么操作。符號棧的操作分為三種:一是直接入棧;一是直接出棧;一是將當前棧頂符號出棧並計算,然后根據新的棧頂符號與當前符號的優先級關系重復操作類型的判斷。
根據棧的特點以及實驗操作的要求,本實驗采用的是順序棧實現。相比鏈棧來說,順序棧的訪問更方便,但其本質是沒有差別的。
二、數據結構——棧
棧的本質是一個僅在隊尾進行操作的線性表。相比於一般的線性表來說,棧的操作是受限制的。一般的棧僅有四個基本操作:初始化(InitStack())、取棧頂元素(GetTop())、入棧(Push())和出棧(Pop()),本實驗多定義了一個操作函數ChangeTop(),用於更改棧頂元素的值,當然這個操作也等價於先出棧后入棧一個新的值。
棧的順序表示類似於線性表的順序表示,用一段地址連續的區域存儲一個順序棧,用物理存儲位置的連表示棧的邏輯關系。與順序表示的線性表類似,這也要求元素之間維持嚴格的物理位置關系,但正如上文所說,棧的操作都是限制在棧頂元素處,所以修改操作的有限突出了訪問的便捷,因此棧的實現通常使用順序表示而不是鏈式表示。
具體實現時,先申請一段連續的空間,當空間不夠時,需要新申請一段更大的空間,把之前的所有數據移到新的位置。這樣的操作確實十分耗時且無法避免,所以需要在實現之前更好地估計程序所需要的空間大小,減少此類操作的次數,同時最大化地利用空間。
三、算法的設計和實現
1、建立兩個棧:一個是數字棧,一個是算符棧,首先向算符棧內添加一個符號‘#’。對於具體的符號棧的實現,可以通過編號的形式使用數字編號,也可以直接保存char類型的字符。
2、讀取一個字符,自動濾掉空格、換行符等無關字符,當出現非法字符時結束程序並輸出“表達式包含未被識別的符號。”
3、如果當前字符c是數字字符,即c滿足條件“c>=’0’&&c<=’9’”,則繼續讀取,並由一個新的變量Num保存這個數字的真實值,具體實現是Num的初值為0,然后每次執行語句Num=Num*10+c-’0’,直到讀取到非數字字符為止;如果當前字符c不是數字字符,調用函數OptrType(char c)得到該符號的編號。
4、當讀到一個算符,考慮其入棧時,有三種情況:
(1)算符棧的棧頂元素優先級低於當前算符時,當前算符入棧;
(2)當兩者優先級相同時,當前算符不入棧,算符棧的棧頂元素彈出;
(3)算符棧的棧頂元素優先級高於當前算符時,算符棧的棧頂元素彈出,數字棧彈出兩個元素,按照順序進行彈出的符號所對應的運算執行運算操作,將得到的結果壓入數字棧。再將待入棧元素繼續進行步驟4的判斷。
5、當‘#’都彈出之后,計算結束。如果算式沒有錯誤,則數字棧只剩一個元素,該元素就是算式的計算結果,輸出即可。
6、清棧。
注:步驟4中提到的優先級是相對的,本實驗中的實現方式是編號之后使用一個二維數組保存一個序對的關系的。比如Cmp[‘+’][‘-’]=1,而同樣的有Cmp[‘-’][‘+’]=1。這樣的規定方式是為了讓四則運算中同級的運算能夠先出現的先計算,從而避免了錯誤。具體關系見下表,其中值為-2的位置表示表達式有錯誤。
+ | - | * | / | ( | ) | # | |
+ | 1 | 1 | -1 | -1 | -1 | 1 | 1 |
- | 1 | 1 | -1 | -1 | -1 | 1 | 1 |
* | 1 | 1 | 1 | 1 | -1 | 1 | 1 |
/ | 1 | 1 | 1 | 1 | -1 | 1 | 1 |
( | -1 | -1 | -1 | -1 | -1 | 0 | -2 |
) | 1 | 1 | 1 | 1 | -2 | 1 | 1 |
# | -1 | -1 | -1 | -1 | -1 | -2 | 0 |
四、預期結果和實驗中的問題
1、預期結果是程序可以正確計算出一個表達式的值,並判斷出表達式的錯誤,包含“表達式中包含無法識別的字符”和“表達式輸入錯誤”兩種錯誤。比如輸入的表達式為“2*(3+5)”,則能夠得到的輸出為“原式=16”;如果輸入為“2x+3”,則輸出為“表達式包含未被識別的符號”;如果輸入為“1 1+1”,則輸出為“表達式輸出有誤”。
2、實驗中的問題及解決方案:
(1)map或者set解決算符映射問題
對於所出現的算符,我都編了一個數字號碼以便訪問。這樣有一個弊端在於,當符號變多的時候,這樣的沒有特殊意義的數字編碼映射顯得十分累贅且不方便,即使每次訪問一個專門編碼、解碼的函數,也顯得很笨拙。
所以我想到的解決方法是,利用c++的STL標准庫中的map或者set進行映射,這樣在編碼的時候可以直接以char類型的數據作為下標進行訪問,十分方便。map和set這兩個容器的原理都是基於紅黑樹,紅黑樹簡單地說是一種二叉平衡查找樹,具體維護操作這里不再贅述。
(2)template解決通用程序設計問題
本實驗中涉及到的兩個棧,一個是數字棧,一個是算符棧。正如上文所說,我將算符編號了,相當於說算符棧是一個維護int型元素的棧,而且其中的元素需要用作下標訪問Cmp[][]數組以得到兩個算符的優先級;而由於計算的必要,數字棧需要是一個維護double型元素的棧。一種暴力有效的方式是,建立兩個幾乎一模一樣的棧,一個維護int型的元素,另一個維護double型的元素。當然我不願意這樣暴力,所以我使用了c++中的模板的程序設計,也就是template,簡單地說這是c++程序設計語言中采用類型作為參數的程序設計,支持通用程序設計。於是這個問題就完美解決了。
(3)關於鏈棧
關於鏈棧的實現,我認為實現很容易。正如上文所說,棧可以看作一個有特殊操作限制的線性表,所以實現幾乎與鏈棧無異。個人覺得棧這種數據結構沒有必要使用鏈式表示,頻繁的申請空間的時間且多出的鏈域空間都是不必要的浪費。
附:c++源代碼:

1 /* 2 項目:順序棧實現表達式求值 3 作者:張譯尹 4 */ 5 #include <iostream> 6 #include <cstdio> 7 8 using namespace std; 9 10 #define STACK_INIT_SIZE 100 //棧存儲空間的初始分配量 11 #define STACKINCREMENT 10 //棧存儲空間的分配增量 12 13 #define MaxLen 120 //表達式長度 14 15 template <class T> class MyStack 16 { 17 private: 18 T *base; //棧底指針,構造之前和銷毀之后為NULL 19 T *top; //棧頂指針 20 int StackSize; //當前棧分配的空間大小 21 22 public: 23 void InitStack() //構造一個空的棧 24 { 25 base = new T(STACK_INIT_SIZE); 26 top = base; 27 StackSize = STACK_INIT_SIZE; 28 } 29 void DestroyStack() //銷毀棧 30 { 31 delete base; 32 StackSize = 0; 33 } 34 void ClearStack() //清空棧 35 { 36 top = base; 37 } 38 bool StackEmpty() //判斷棧空 39 { 40 return (base == top); 41 } 42 int StackLength() //返回當前棧內元素個數 43 { 44 return (top - base); 45 } 46 bool GetTop(T &Elem) //取棧頂元素,用Elem返回,失敗返回true 47 { 48 if(StackEmpty()) 49 return true; 50 Elem = *(top - 1); 51 return false; 52 } 53 void Push(T Elem) //將Elem加入棧頂 54 { 55 int Len = StackLength(); 56 if(Len + 1 > StackSize) //當前棧的空間不夠 57 { 58 T *NewBase = new T(StackSize + STACKINCREMENT); 59 int i; 60 for(i = 0; i < Len; i++) 61 NewBase[i] = *(base + i); 62 delete base; 63 base = NewBase; 64 StackSize += STACKINCREMENT; 65 } 66 *top = Elem; 67 top++; 68 } 69 bool Pop(T &Elem) //出棧棧頂元素,用Elem返回,失敗時返回true 70 { 71 if(StackEmpty()) 72 return true; 73 top--; 74 Elem = *top; 75 return false; 76 } 77 bool ChangeTop(T Elem) //將棧頂元素的值改為Elem,失敗時返回true 78 { 79 if(StackEmpty()) 80 return true; 81 *(top - 1) = Elem; 82 return false; 83 } 84 }; 85 86 int Cmp[10][10] = {{}, 87 {0, 1, 1, -1, -1, -1, 1, 1}, 88 {0, 1, 1, -1, -1, -1, 1, 1}, 89 {0, 1, 1, 1, 1, -1, 1, 1}, 90 {0, 1, 1, 1, 1, -1, 1, 1}, 91 {0,-1, -1, -1, -1, -1, 0, -2}, 92 {0, 1, 1, 1, 1, -2, 1, 1}, 93 {0,-1, -1, -1, -1, -1, -2, 0}}; 94 //保存運算符之間的優先關系 95 96 void Check() 97 { 98 MyStack<int> s; 99 s.InitStack(); 100 int n; 101 int tmp; 102 scanf("%d", &n); 103 for(int i = 1; i <= n; i++) 104 { 105 scanf("%d", &tmp); 106 s.Push(tmp); 107 } 108 printf("Length = %d\n", s.StackLength()); 109 printf("====================================\n"); 110 while(!s.StackEmpty()) 111 { 112 s.GetTop(tmp); 113 printf("%d\t", tmp); 114 s.Pop(tmp); 115 printf("%d\n", tmp); 116 } 117 printf("====================================\n"); 118 } 119 120 inline int OptrType(char ch) //返回運算符編號 121 { 122 switch(ch) 123 { 124 case ' ': case '\n': return 0; 125 case '+': return 1; 126 case '-': return 2; 127 case '*': return 3; 128 case '/': return 4; 129 case '(': return 5; 130 case ')': return 6; 131 case '#': return 7; 132 case '.': return 8; 133 } 134 return -1; 135 } 136 137 double Cal(double x, double y, int Op) //x Op y 138 { 139 switch(Op) 140 { 141 case 1: return (x + y); 142 case 2: return (x - y); 143 case 3: return (x * y); 144 case 4: return (x / y); 145 } 146 return 0x3f; 147 } 148 149 void EvaluateExpression() 150 { 151 char c; 152 int cnt, t, tmp, pElem; 153 double x, y; //用於計算的數,x符號y 154 double Num; 155 bool OK = false; //表示算式是否完成計算,完成為true 156 157 //+:1 -:2 *:3 /:4 (:5 ):6 #:7 158 MyStack<int> Optr; //運算符棧 159 MyStack<double> Opnd; //運算數棧 160 Optr.InitStack(); 161 Opnd.InitStack(); 162 163 Optr.Push(7); //在起初壓入'#'以便最后的清棧 164 165 printf("請輸入一個算式,以\'#\'結束。\n"); 166 167 while(c = getchar()) 168 { 169 if((c >= '0' && c <= '9') || c == '.') 170 { 171 Num = 0.0; 172 while(c >= '0' && c <= '9') 173 { 174 Num = Num * 10 + c - '0'; 175 c = getchar(); 176 } 177 if(c == '.') 178 { 179 cnt = 0; 180 c = getchar(); 181 while(c >= '0' && c <= '9') 182 { 183 Num = Num * 10 + c - '0'; 184 c = getchar(); 185 cnt++; 186 } 187 while(cnt--) 188 Num /= 10.0; 189 } 190 Opnd.Push(Num); 191 } 192 t = OptrType(c); 193 if(!t) //空格符、換行符 194 continue; 195 if(t == -1) //其他符號 196 { 197 printf("表達式包含未被識別的符號。\n"); 198 return ; 199 } 200 for(;;) 201 { 202 Optr.GetTop(tmp); 203 if(Cmp[tmp][t] == -1) 204 { 205 Optr.Push(t); 206 break; 207 } 208 if(Cmp[tmp][t] == 0) 209 { 210 if(t == 7) //'#'表示結束 211 { 212 Optr.Pop(pElem); 213 OK = true; 214 } 215 else //遇到左右括號配對了 216 { 217 Optr.Pop(pElem); 218 } 219 break; 220 } 221 //Cmp[tmp][t] == 1 222 Optr.Pop(pElem); 223 Opnd.Pop(y); 224 Opnd.GetTop(x); 225 x = Cal(x, y, pElem); 226 Opnd.ChangeTop(x); 227 Optr.GetTop(tmp); 228 } 229 if(OK) 230 break; 231 } 232 tmp = Opnd.StackLength(); 233 if(tmp != 1) 234 { 235 printf("表達式輸入有誤。\n"); 236 return ; 237 } 238 Opnd.GetTop(x); 239 printf("原式 = %lf\n", x); 240 } 241 242 int main() 243 { 244 //Check(); 245 EvaluateExpression(); 246 return 0; 247 }