通過后綴表達式求表達式的值
知識點:樹的前中后序遍歷(可以參考AK寶典),后綴表達式(逆波蘭式),中綴轉后綴,后綴表達式求值
引言:
對於一個數學表達式,比如說 1-(2+3/4)*5=?可以很容易地人工計算出結果。然而如果想要用計算機求這樣表達式的值似乎有一點麻煩,因為計算機不太方便處理運算符的優先級,尤其是括號對優先級的影響。因此,想要用計算機求表達式的值,最好要找到一種方法可以排除括號,運算符優先級的干擾。
表達式與樹:
事實上,任何一個數學表達式都可唯一轉化為一棵樹,比如說:1-(2+3/4)*5=?可以轉化為如下一棵二叉樹:
樹的特性:
可以看出,這棵二叉樹的每一個葉子節點都是一個數,而每兩個節點的父節點都是一個運算符,比如最右邊子樹3,4,/,它代表3/4
如果能拿到這樣的一顆二叉樹,求上述表達式的值就很簡單了,我們只需要先算出子樹3/4=0.75,再計算子樹2+0.75=2.75,再計算子樹5*2.75=13.75,最后計算根節點處1-13.75=-12.75即可求出表達式的值
不難發現,這顆二叉樹的中序遍歷結果為:1-2+3/4*5
,也就是原表達式去掉括號的樣子。因此,原數學表達式也被稱為中綴表達式,對這棵樹后序遍歷得到的結果稱為后綴表達式
樹的求法:
而這樣的樹的求法也很很簡單,我們只需要找出原表達式中最先計算的運算( 3/4
),把它作為一顆子樹,然后我們就可以把這個運算(3/4
)視為一個整體,找出原式中其次優先的運算(2+3/4
)和之前的子樹組成一顆更大的樹,直至把所有運算都處理完,就可以得出一整棵樹
不過,對於計算機而言,無論是求樹的過程,還是通過樹求值的過程都還是有些復雜,需要進一步簡化
使用后綴表達式求值:
首先,這顆樹對應的后綴表達式為 1 2 3 4 / + 5 * -,不難看出,后綴表達式不含括號,數字的排列順序與原式相同,原式中優先級越高的運算符在該后綴表達式中越先出現。
因此,一定存在某種方案能夠使得計算機在處理一個后綴表達式時能夠先把運算優先級高的運算計算出來,使其能夠用於后續的低優先級運算。
而對於中綴表達式,由於運算級高的運算不一定排在前面,因此計算機必須反復掃描中綴表達式,先算優先級高的,再算優先級低的運算,這就是后綴表達式更加易於求值的原因。
具體而言:
我們依次處理后綴表達式中的每一個數字或符號a[i]:
- 如果a[i]是一個數字,就直接將其壓入棧s中
- 如果a[i]是一個運算符,則將棧s頂端的第一個元素a,第二個元素b分別從棧中取出並清除,計算 c=b運算a (比如
c=b-a
),然后再將c壓入棧中,進行后續操作
對於后綴表達式1 2 3 4 / + 5 * -,運算過程如下:
后綴表達式 | 棧(從底到頂) | 運算過程 |
1 2 3 4 / + 5 * - | 1 | 1入棧 |
1 2 3 4 / + 5 * - | 1 2 | 2入棧 |
1 2 3 4 / + 5 * - | 1 2 3 | 3入棧 |
1 2 3 4 / + 5 * - | 1 2 3 4 | 3入棧 |
1 2 3 4 / + 5 * - | 1 2 0.75 | 3,4出棧 3/4=0.75入棧 |
1 2 3 4 / + 5 * - | 1 2.75 | 2,0.75出棧 2+0.75=2.75入棧 |
1 2 3 4 / + 5 * - | 1 2.75 5 | 5入棧 |
1 2 3 4 / + 5 * - | 1 13.75 | 2.75,5出棧 2.75*5=13.75入棧 |
1 2 3 4 / + 5 * - | -12.75(答案) | 1 13.75出棧 1-13.75=-12.75入棧 |
不難看出,使用后綴表達式求值的過程其實本質上與通過子樹逐步求值的過程相同
那么,要如何求后綴表達式呢?
中綴表達式轉后綴表達式:
中綴表達式轉后綴表達式可以借助兩個棧來完成,設兩個棧分別為s1,s2,我們從左到右遍歷原本的中綴表達式,設當前遍歷到的數字或符號為a[i]
- 如果a[i]為
數字
,那么直接壓入s2中 - 如果a[i]為
運算符
,那么需比較a[i]和s1棧頂符號ch的優先級- 如果s1為空或ch為'(',則直接將a[i]壓入棧s1中
- 如果a[i]優先級 > ch優先級,則直接將a[i]壓入棧s1中
- 如果a[i]優先級 < ch優先級,則將ch壓入s2中,並從s1中刪除,再比較新的s1棧頂元素ch'與a[i]的優先級
- 如果a[i]優先級 = ch優先級,若ch與a[i]為左結合,則認為ch優先級 > a[i]優先級;若ch與a[i]為右結合,則認為ch優先級 < a[i]優先級
- 如果a[i]為左括號
'('
,那么直接壓入s1中 - 如果a[i]為右括號
')'
,那么將s1中第一個左括號'('前的所有運算符依次壓到到s2中,並從s1中刪除,最后將s1中的這個左括號'('刪除 - 如果a[i]為結束符
'='
,那么s1中剩余的全部運算符依次移到s2中,轉換結束
完成后s2所存結果就是原式的后綴表達式,但是由於棧式先進后出的,所以s2棧頂元素依次輸出所得其實式逆序的后綴表達式,需要進行反向處理
注:典型的左結合運算符有+-*/
,特點為a+b+c=(a+b)+c
典型的右結合運算符有^
(冪運算),特點為a^b^c=a^(b^c)
對於1-(2+3/4)*5=,運算過程如下:
原式 | 棧s1 | 棧s2 | 運算過程 |
1-(2+3/4)*5= | 1 | 1 入棧 | |
1-(2+3/4)*5= | - | 1 | - 入棧 |
1-(2+3/4)*5= | - ( | 1 | ( 入棧 |
1-(2+3/4)*5= | - ( | 1 2 | 2 入棧 |
1-(2+3/4)*5= | - ( + | 1 2 | + 入棧 |
1-(2+3/4)*5= | - ( + | 1 2 3 | 3 入棧 |
1-(2+3/4)*5= | - ( + / | 1 2 3 | / 入棧 |
1-(2+3/4)*5= | - ( + / | 1 2 3 4 | 4 入棧 |
1-(2+3/4)*5= | - | 1 2 3 4 / + | / + 移入s2,( 清除 |
1-(2+3/4)*5= | - * | 1 2 3 4 / + | * 入棧 |
1-(2+3/4)*5= | - * | 1 2 3 4 / + 5 | 5 入棧 |
1-(2+3/4)*5= | 1 2 3 4 / + 5 * - | * - 依次移入s2,結束 |
依次輸出s2棧頂元素 -*5+/4321 ,是后綴表達式的逆序
完整代碼如下:
#include <stdio.h>
//使用后綴表達式(逆波蘭式)求表達式的值,表達式只含+-*/()與整數
//樣例 : 1-(2+3/4)*5=
double s3[5005]; //棧s3,結果為浮點數
int size; // s3中的元素數
struct stack //棧
{
int size; //棧中元素數量
int flag[5005]; // flag[i]=0表示s[i]是數字, flag[i]=1表示s[i]是符號
int s[5005]; //既可以存數字,也可以存運算符
} s1, s2; //棧s1,s2
typedef struct stack stack;
int top(stack *a) //取出棧頂元素
{
return a->s[a->size];
}
int flg(stack *a) //判斷棧頂元素的類型(字符或數字)
{
return a->flag[a->size];
}
void push(stack *a, int flag, int x) //向棧頂壓入一個元素
{
a->size++;
a->flag[a->size] = flag; //標記元素的類型(運算符或數字)
a->s[a->size] = x; //存入元素
}
void pop(stack *a) //刪除棧頂元素
{
a->size--;
}
int pr(char op) //求判斷運算符優先級
{
if (op == '*' || op == '/')
return 1;
else if (op == '+' || op == '-')
return 0;
else //此處視括號優先級最低
return -1;
}
void read() //讀取中綴表達式
{
char ch = getchar();
while (1)
{
if (ch == '=') //若為 = ,將s1中所有元素移入s2中,並結束函數
{
while (s1.size > 0)
{
int tmp = top(&s1);
pop(&s1);
push(&s2, 1, tmp);
}
return;
}
else if (ch >= '0' && ch <= '9') //若為數字,求出整個數,並放入s2中
{
int x = 0;
while (ch >= '0' && ch <= '9') //思想與快讀相同
{
x = x * 10 + ch - '0';
ch = getchar();
}
push(&s2, 0, x);
continue;
}
else if (ch == '+' || ch == '-' || ch == '/' || ch == '*') //若為運算符
{
while (s1.size > 0 && pr(top(&s1)) >= pr(ch)) //當ch的優先級<s1棧頂元素優先級時
{
int tmp = top(&s1); //將s1棧頂元素移到s2並清除
pop(&s1); //清除該元素
push(&s2, 1, tmp);
}
push(&s1, 1, ch); //將運算符ch壓入棧s1
}
else if (ch == '(') //左括號直接壓入s1
{
push(&s1, 1, ch);
}
else if (ch == ')') //右括號
{
while (top(&s1) != '(') //將左右括號之間的運算符全部移到s2
{
int tmp = top(&s1);
pop(&s1);
push(&s2, 1, tmp);
}
pop(&s1); //清除s1中左括號
}
ch = getchar();
}
}
void cal()
{
while (s1.size > 0) //后綴表達式求值
{
if (flg(&s1) == 0) //如果棧頂為數字,直接壓入棧s3中
{
int tmp = top(&s1);
s3[++size] = tmp;
}
else if (flg(&s1) == 1) //如果棧頂為運算符
{
double a = s3[size--]; //取出s3的兩個棧頂元素相運算
double b = s3[size--];
if (top(&s1) == '+')
{
s3[++size] = b + a;
}
else if (top(&s1) == '-')
{
s3[++size] = b - a; //后減前,並將結果壓入棧s3
}
else if (top(&s1) == '*')
{
s3[++size] = b * a;
}
else if (top(&s1) == '/')
{
s3[++size] = b / a; //后者除以前者,並將結果壓入棧s3
}
}
pop(&s1); //將s1棧頂元素清除
}
}
int main()
{
read();
while (s2.size > 0) // s2中結果依次輸出為一個逆序的后綴表達式
{
int tmp = top(&s2);
int flag = flg(&s2);
pop(&s2);
push(&s1, flag, tmp);
} //需將其移入s1中使其反向為一個正序的后綴表達式
// while(s1.size>0)//調試,輸出后綴表達式
// {
// if(s1.flag[s1.size])
// printf("%c ",top(&s1));
// else printf("%d ",top(&s1));
// pop(&s1);
// }
cal();
printf("%.2lf\n", s3[size]); //結果為s3中剩下的一個元素
return 0;
}