先來看一個粗糙的簡單計算器的實現。他只支援加減乘除,並且一次只能對兩個小於10的正整數做一次運算
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 |
#include<stdio.h> int char2number(char c){ switch(c){ case '0':return 0; case '1':return 1; case '2':return 2; case '3':return 3; case '4':return 4; case '5':return 5; case '6':return 6; case '7':return 7; case '8':return 8; case '9':return 9; } } int is_operator(char c){ return (c=='*' || c=='+' || c=='-' ||c=='/'); } int count(int a,int b,char opt){ switch(opt){ case '*':return a*b; case '+':return a+b; case '-':return a-b; case '/':return a/b; } } int main(void){ int number[2]; //存放運算數 char str[10]; ///表示式 char opt; //操作符 while(1){ fgets(str,10,stdin); int i=0; int j=0; int res; //相當於一個簡單的字串分析,從中提取出數字和運算子 while(str[i] && str[i]!='\n'){ if(!is_operator(str[i])){ number[i-j] =char2number(str[i]); }else{ opt =str[i]; j++; //碰到運算子,需要記錄。因為運算子並不儲存在number陣列中, //比如 3*5 分析到字元5時,i為2但是number陣列中只記錄了3即number[0]所以5應該在number[1], //所以上面使用number[i-j]來記錄數字 } i++; } res=count(number[0],number[1],opt); printf("=%d\n",res); } return 0; } |
這是一個粗糙的實現,但是但是仍舊有一些要點體現在上面,比如對 表示式的分析中使用了while(str[i] && str[i]!=’\n’)我們首先要判斷是不是到表示式字串 的結尾了,如果不是還要判斷是不是’\n’字元。因為呼叫fgets輸入表示式時Enter鍵也被儲存了。
上面的程式只能進行一次 兩個小於十的正整數 的加減乘除運算。如下
當然,這連個簡單的計算器都算不上。
1: 如果我要計算 2+3+4怎麼辦。也就是說上面的不支援大於兩個運算元的運算
2 :如果我要 計算2的立方怎麼辦 。也就是說上面支援的運算太少了,這個其實問題不大。因為我們通過加單的新增就可以了。所以後面的正真實現中我們還是隻實現了加減乘除四個典型運算以及帶括號的運算
3 :如果我要計算 2+3*4怎麼辦?有的人可能會問,這有什麼怎麼辦了。計算就好了。但是我們知道應該先算 3*4 然後再 +2
但是計算器不知道啊。也就是說上面的並未支援優先順序。這在windows自帶的計算機上有體現在檢視裡面將計算器切換到標準型
再切換到科學型。
很顯然,標準型並未支援優先順序運算,他只是簡單的從左往右運算
4 :如果我要計算 20+10怎麼辦,也就是說運算數可以是任意的並不固定為小於十的正整數,因為我們輸入的一個連續的 表示式字串。
那麼就需要更復雜一點的語句分析才能提取出正真的數字。當然後面的正真實現中,也只是實現了 整形所能表示的正整數的相加,並未實現小數,負數之類。當然他們原理都是相同的,只是在表示式的分析上覆雜一些而已。
所以,即使是一個簡單的計算器,也應該上面的四個要求。
1,2, 4,的要求主要在表示式字串的分析上,也就是從裡面分析出 字元 和數字。無非是語法分析變複雜一點。
問題核心是在 如何運算子的優先順序上。
這就需要了解 字尾(逆波蘭)表示式。
定義:不包含括號,運算子放在兩個運算物件的後面,所有的計算按運算子出現的順序,
嚴格從左向右進行(不再考慮運算子的優先規則,如:(2 + 1) * 3 , 即2 1 + 3 *
我們知道計算機並不知道什麼優先順序。最簡單的它只會從左到有讀取然後運算。
那麼根據上面 字尾表示式的定義。如果能將表達是轉換成字尾表示式,那麼運算就簡單了。可以使用一個棧來實現。
比如:3+12*2-(6+1)*2;
轉換成字尾表示式 3 12 2 * + 6 1 + 2 * –
使用一個棧,然後從左到右遍歷,遇到數字我們就其入棧,遇到運算子,就出棧兩個數字然後做運算,並將運算結構入棧。這樣一直下去到最後。棧中存放的就為最終的結果
也就是說如果得到了字尾表示式那麼就可以用上面的方法來從左到右計算了。而不存在優先順序的問題了
那麼,現在問題就變成了怎麼將表示式變成計算機易於計算的字尾表示式,這裡同樣是用到棧來實現的。
下面我們通過對 + *以及( )這幾個典型的運算子的處理 來闡述轉換成字尾表示式的一般原理。
我們假設表示式是合法的。當讀到一個運算元的時候,立即把他放到輸出中。
遇到其他運算子時”+” ,”*” , “(” 那麼久從棧中彈出元素直到當前棧頂元素的優先順序比當前遇到的運算子低,然後再將該運算子入棧需要注意
的一點是 除非是在處理一個右括號’)’否則絕不從棧中移走左括號'(‘。
如果遇到右括號’)’,那麼就將棧元素彈出並寫到輸出,直到遇到一個對應的左括號。注意:這時候右括號不入棧,彈出的左括號也不輸出。而是僅僅丟棄他們
最後當我們讀到表示式的結尾時,再將棧中的元素依次彈出寫到輸出中直到棧空變得到了字尾表示式
比如 a+b*c+(d*e+f)*g
字尾表示式為: a b c * + d e * f + g * +
轉換過程如下
得到字尾表示式後一切就簡單了,計算機只需想上面說的那樣從左到右簡單計算就行了。
下面我們對 計算器編寫過程中的幾個核心部分的程式碼詳細說明
整體思路是 從鍵盤得到一個字串形式的表示式,然後從左到右對其進行分析,依次分離出數字和運算子然並建立對應節點,
然後像上面介紹的放法一樣,如果是數字就 插入到一個連結串列中(這裡並不輸出而是放到連結串列中供後續使用),如果是運算子,就放入棧中。
最終當分析表示式結尾時,彈出棧中所有元素並依此插入連結串列中,最終連結串列就是我們需要的字尾表示式。
然後對連結串列從左到右遍歷並使用之前所說的對字尾表示式的計算方法。計算出最終結果
需要注意的是,我們沒有做錯誤檢查,所以輸入的表示式必須是合法的
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
enum PRIORITY{level1,level2,level3,level4}; struct node{ long key; //運算子或數字 PRIORITY priority; //優先順序 數字預設設定為1 加減為2 乘除為3 括號為4 TYPE type; //區分當前節點中的值是 數字還是 運算子 NODE next; }; struct expression{ NODE exp; NODE last_node; //因為使用連結串列作為字尾表示式的存放,每次插入元素都是從尾部插入,為避免每次都要從頭遍歷到尾部, //我們設定一個尾部指標,從而使插入操作能立刻完成 }; |
求字尾表示式的程式碼註釋
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 |
EXP to_postfix(char *exp){ //求字尾表示式 EXP list; EXP stack; init_expression(&list); //初始化用來存放字尾表示式的連結串列 init_expression(&stack); //初始化用來得到字尾表示式而是用的棧 NODE temp; NODE new_node; NODE top_node; long key=0; while(*exp && (*exp)!='\n'){ //開始遍歷表示式 if('0' <=*exp && *exp<='9'){ //這段程式碼用來提取出像 200 這樣不是以為字元表示的數字 while('0' <=*(exp+1) && *(exp+1)<='9'){ // key =key*10+tonumber(*exp); // exp++; // } // key =key*10+tonumber(*exp); // new_node=create_node(key,IS_NUMBER); //建立一個 數字型別的節點 插入連結串列 insert_key(list,new_node); key=0; //重新置0,記錄後面的數字 } else{ //不是數字則是運算子 if(*exp == ')'){ //如果碰到的是右括號 NODE local; //則彈出棧元素 直到遇到左括號 while((char)(local=pop(stack))->key !='(' ) // 左右括號丟棄,其他運算子插入連結串列中(字尾表示式中) insert_key(list,local); // }else{ new_node=create_node(*exp,IS_OPERATOR); //不是右括號,則為一般的運算子 top_node=get_top(stack); //我們嘗試或得棧頂元素(不出棧)看優先順序是不是小於當前遇到的 if((!top_node) || (top_node->priority < new_node->priority)){ //如果棧空,或者棧頂運算子優先順序小於當前運算子 push(stack,new_node); //則當前運算子直接入棧 }else{ // while(get_top(stack) && get_top(stack)->priority != level4 // && (get_top(stack))->priority >= new_node->priority){ // temp=pop(stack); //否則彈出棧頂元素,並插入連結串列中(字尾表示式存放的地方) insert_key(list,temp); //直到棧頂元素優先順序小於當前遇到的運算子優先順序 } //注意,除非遇到右括號,否則不能彈出左括號 push(stack,new_node); //最後入棧當前遇到的運算子 } } } exp++; } while(get_top(stack)){ //表示式遍歷完畢後,出棧所有元素 insert_key(list,pop(stack)); //並插入存放字尾表示式的連結串列中 } free(stack); //釋放棧空間 return list; //返回字尾表示式 } |
根據字尾表達是求運算結果的註釋:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 |
int result(EXP postfix){ EXP stack; int result=0; NODE temp = postfix->exp; //或得表示式 NODE new_node; NODE number1,number2; init_expression(&stack); //初始化用來求值時使用的棧 while(temp){ //對連結串列從頭到尾行遍歷(遍歷字尾表示式) if(temp->type == IS_NUMBER){ //如果是數字,則建立節點併入棧 new_node = create_node(temp->key,temp->type); // push(stack,new_node); // }else{ // 否則為運算子 number1=pop(stack); // 棧中出棧兩個運算數 number2=pop(stack); // result = count(number1->key,number2->key,temp->key); // 進行相應運算 push(stack,create_node(result,IS_NUMBER)); // 運算結果入棧 } temp = temp->next; } return pop(stack)->key; //最終棧中存放的即為最終運算結果 } |
測試結果如下
第二行給出了字尾表示式
下面是所有程式碼實現
head.h
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 |
#ifndef HEAD_H_ #define HEAD_H_ typedef int TYPE ; struct node; //運算子或數字 struct expression; //表示式 typedef struct node * NODE; typedef struct expression *EXP; void init_expression(EXP *expression); //初始化表示式 void insert_key(EXP expression,NODE new_node); //連結串列操作 NODE create_node(int key,TYPE type); //建立一個節點,裡面儲存的伙食運算子或是數字 void push(EXP expression,NODE node); //入棧一個運算子或是數字 NODE get_top(EXP expression); //或得棧頂元素,不彈出。 NODE pop(EXP expression); //或得棧頂元素,彈出 EXP to_postfix(char *exp);//將普通表示式轉換成字尾表示式 int result(EXP postfix); void print_list(EXP list); //除錯例程 #endif |
head.c
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 |
#include<stdio.h> #include<stdlib.h> #include"head.h" #define IS_NUMBER 0 #define IS_OPERATOR 1 enum PRIORITY{level1,level2,level3,level4}; struct node{ long key; //運算子或數字 PRIORITY priority; //優先順序 數字預設設定為1 加減為2 乘除為3 括號為4 TYPE type; //區分當前節點中的值是 數字還是 運算子 NODE next; }; struct expression{ NODE exp; NODE last_node; //因為使用連結串列作為字尾表示式的存放,每次插入元素都是從尾部插入,為避免每次都要從頭遍歷到尾部, //我們設定一個尾部指標,從而使插入操作能立刻完成 }; static int count(int number1,int number2,int ope); //根據運算子運算 static void set_priority(NODE node); // 設定優先順序 static int is_empty(EXP expression); //判斷是否為空 static int tonumber(char c); //字元到數字的轉換 static int is_empty(EXP expression){ return expression->exp==NULL; } static void set_priority(NODE node){ switch((char)node->key){ case '(': case ')': node->priority = level4;break; case '*': case '/': node->priority = level3;break; case '+': case '-': node->priority = level2;break; default: printf("input error! \n");exit(1);break; } } NODE create_node(int key,TYPE type){ //type 區分建立的是一個數字節點還是操作符節點 NODE node = (NODE)malloc(sizeof(struct node)); if(node){ node->key=key; node->priority=level1; //預設先設定為1 if(type == IS_OPERATOR) set_priority(node); node->next = NULL; node->type = type; return node; }else{ printf("malloc error(in create_node)\n"); exit(1); } } void init_expression(EXP *expression){ *expression = (EXP)malloc(sizeof(struct expression)); if(expression == NULL){ printf("initialization expression error!\n"); exit(1); } (*expression)->exp = NULL; (*expression)->last_node = NULL; //為連結串列設定的表尾指標 } void insert_key(EXP expression ,NODE new_node){ //插入節點到連結串列,尾部插入 if(!is_empty(expression)){ expression->last_node->next = new_node; expression->last_node = new_node; }else{ expression->exp = new_node; expression->last_node = new_node; } } void push(EXP expression, NODE new_node){ //入棧 new_node->next = expression->exp; expression->exp = new_node; } NODE get_top(EXP expression){ return expression->exp; } NODE pop(EXP expression){ //出棧 NODE temp; if(!is_empty(expression)){ temp = expression->exp; expression->exp = temp->next; }else{ return NULL; } return temp; } EXP to_postfix(char *exp){ //求字尾表示式 EXP list; EXP stack; init_expression(&list); //初始化用來存放字尾表示式的連結串列 init_expression(&stack); //初始化用來得到字尾表示式而是用的棧 NODE temp; NODE new_node; NODE top_node; long key=0; while(*exp && (*exp)!='\n'){ //開始遍歷表示式 if('0' <=*exp && *exp<='9'){ //這段程式碼用來提取出像 200 這樣不是以為字元表示的數字 while('0' <=*(exp+1) && *(exp+1)<='9'){ // key =key*10+tonumber(*exp); // exp++; // } // key =key*10+tonumber(*exp); // new_node=create_node(key,IS_NUMBER); //建立一個 數字型別的節點 插入連結串列 insert_key(list,new_node); key=0; //重新置0,記錄後面的數字 } else{ //不是數字則是運算子 if(*exp == ')'){ //如果碰到的是右括號 NODE local; //則彈出棧元素 直到遇到左括號 while((char)(local=pop(stack))->key !='(' ) // 左右括號丟棄,其他運算子插入連結串列中(字尾表示式中) insert_key(list,local); // }else{ new_node=create_node(*exp,IS_OPERATOR); //不是右括號,則為一般的運算子 top_node=get_top(stack); //我們嘗試或得棧頂元素(不出棧)看優先順序是不是小於當前遇到的 if((!top_node) || (top_node->priority < new_node->priority)){ //如果棧空,或者棧頂運算子優先順序小於當前運算子 push(stack,new_node); //則當前運算子直接入棧 }else{ // while(get_top(stack) && get_top(stack)->priority != level4 // && (get_top(stack))->priority >= new_node->priority){ // temp=pop(stack); //否則彈出棧頂元素,並插入連結串列中(字尾表示式存放的地方) insert_key(list,temp); //直到棧頂元素優先順序小於當前遇到的運算子優先順序 } //注意,除非遇到右括號,否則不能彈出左括號 push(stack,new_node); //最後入棧當前遇到的運算子 } } } exp++; } while(get_top(stack)){ //表示式遍歷完畢後,出棧所有元素 insert_key(list,pop(stack)); //並插入存放字尾表示式的連結串列中 } free(stack); //釋放棧空間 return list; //返回字尾表示式 } static int count(int number1,int number2,int ope){ switch((char)ope){ case '+': return number1+number2; case '-': return number2-number1; case '*': return number1*number2; case '/': return number1/number2; } } int result(EXP postfix){ EXP stack; int result=0; NODE temp = postfix->exp; //或得表示式 NODE new_node; NODE number1,number2; init_expression(&stack); //初始化用來求值時使用的棧 while(temp){ //對連結串列從頭到尾行遍歷(遍歷字尾表示式) if(temp->type == IS_NUMBER){ //如果是數字,則建立節點併入棧 new_node = create_node(temp->key,temp->type); // push(stack,new_node); // }else{ // 否則為運算子 number1=pop(stack); // 棧中出棧兩個運算數 number2=pop(stack); // result = count(number1->key,number2->key,temp->key); // 進行相應運算 push(stack,create_node(result,IS_NUMBER)); // 運算結果入棧 } temp = temp->next; } return pop(stack)->key; //最終棧中存放的即為最終運算結果 } void print_list(EXP list){ NODE temp = list->exp; while(temp){ if(temp->type==IS_NUMBER) printf("%d ",temp->key); else printf("%c ",temp->key); temp = temp->next; } printf("\n"); } int tonumber(char c){ switch(c){ case '0':return 0; case '1':return 1; case '2':return 2; case '3':return 3; case '4':return 4; case '5':return 5; case '6':return 6; case '7':return 7; case '8':return 8; case '9':return 9; } } |
測試程式碼
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
#include<stdio.h> #include"head.h" int main(void ){ char s[30]; EXP postfix; fgets(s,sizeof(s),stdin); char *exp=s; postfix=to_postfix(exp); //得到字尾表示式 print_list(postfix); //列印字尾表示式 int res=result(postfix); //根據字尾表示式求結果 printf("%d\n",res); return 0; } |