java API包羅萬象,細節層出不窮,要在一篇筆記裏總結好實在是很有挑戰性。筆者思前想後,決定還是用畢老師視頻中講到的API知識實現一個小項目,並把思考和編碼過程記錄在下面,在這個過程中練習API的使用。這個項目是實現一個算術表達式解析器。網上有很多針對此項目的程序,不過大多要用到中綴表達式的轉換算法和棧結構。筆者在查閱這些資料的時候感到因爲不符合手算的經驗,這些代碼都難以理解。能不能用小學生算術課上的分步化簡的辦法來完成呢?也就是說解析器在運算過程中操作數和操作符不會變換相對位置,僅僅用計算、替換、刪除來實現化簡,最終表達式被化簡爲一個操作數,即答案。
筆者用兩天時間完成了這個程序。用到的API主要有包裝類,String類和ArrayList框架類。
1. 概述
String expr = "1 + 2 * 4 - 3";
這個表達式的值應當是6。考慮更復雜的表達式:
String expr = "( - 2)*(2 - 2*(3 + 4) + 9*2)";
計算結果是 - 12。2. 需求
一個字符串,該字符串表示一個完整正確的算術表達式
該表達式可以包括括號,包括正負零三種範圍的整數或小數操作數,包括加減乘除四種運算
輸出:
該表達式的double類型計算結果
其他要求:不使用棧,不對操作符進行重新排序,仿照手算的順序按照計算、替換、刪除的方式不斷化簡直到得出答案,代碼儘可能精簡
3. 設計
3.1 選擇一種框架類來存儲表達式
( - 2)*(2 - 2*(3 + 4) + 9*2)
= ( - 2)*(2 - 2*(7) + 9*2)
= ( - 2)*(2 - 2*7 + 9*2)
= ( - 2)*(2 - 14 + 9*2)
= ( - 2)*(2 - 14 + 18)
= ( - 2)*( - 12 + 18)
= ( - 2)*(6)
= ( - 2)*6
= - 12
我們要設計的程序每一步消去一個操作符。因爲操作數的個數始終比操作符多一個,到最後會剩下一個操作數就是答案。要模仿這個分步計算的過程,這個框架類作爲表達式的容器,應當滿足:
爲了方便定位操作符和操作數,能夠按索引查找元素
爲了方便替換和刪除操作符和操作數,能夠在遍歷中按索引更改刪除元素
爲了刪除功能,容器的元素總數可以變化
按索引查找,可以用List類的容器實現。按索引改刪,可以用ArrayList。
3.2 決定框架類中存儲什麼類型的元素
= = = 元素怎樣表示操作符和操作數
需求要求我們精簡代碼,所以應該將操作符和操作數存儲在同一個ArrayList中。一個操作數可以存成一個double型變量,可是加減乘除和括號這樣的操作符怎樣用double型變量表示,同時不會引起歧義呢?因爲字符串表達式中很難出現高精度無理數,所以可以選一個作爲表示操作符的常量。筆者選的是:
//用來標識操作符的常數
interface OperatorTag
{
double ADD = Math.PI; //加
double SUB = Math.PI + 1; //減
double MUL = Math.PI + 2; //乘
double DIV = Math.PI + 3; //除
double LBR = Math.PI + 4; //左括號
double RBR = Math.PI + 5; //右括號
}
如果arraylist中的某個元素是上述常數值,則它應當是個操作符。3.3 按什麼順序存儲元素
ArrayList[0] : LBR
ArrayList[1] : SUB
ArrayList[2] : 2.0
ArrayList[3] : RBR
ArrayList[4] : MUL
ArrayList[5] : LBR
ArrayList[6] : 2.0
ArrayList[7] : SUB
ArrayList[8] : 2.0
ArrayList[9] : MUL
ArrayList[10]: LBR
ArrayList[11]: 3.0
ArrayList[12]: ADD
ArrayList[13]: 4.0
ArrayList[14]: RBR
ArrayList[15]: ADD
ArrayList[16]: 9.0
ArrayList[17]: MUL
ArrayList[18]: 2.0
ArrayList[19]: RBR
爲此我們需要從字符串表達式中提取操作數和操作符,並存到ArrayList中去。3.4 將字符串表達式轉換爲ArrayList中的一系列元素
//提取字符串expr中的操作符和操作數並存儲到ArrayList<Double> al中
public static void parse(String expr)
{
//去掉所有空格
String str = expr.replace(" ", "");
//標記單目操作符負號位置
int minus_pos = 0;
//將表達式中的所有單目運算符負號轉化成雙目運算符減號,補充上左操作數0
while( (minus_pos = str.indexOf("(-")) != - 1)
{
str = str.substring(0, minus_pos + 1) + "0" + str.substring(minus_pos + 1);
}
//一個操作數、操作符的開始位置
int pos_start = 0;
//一個操作數、操作符的結束位置
int pos_end = 0;
//將字符串轉換爲字符數組
char[] arr = str.toCharArray();
//逐個提取表達式中的操作數和操作符
while(pos_start != arr.length )
{
pos_end = pos_start;
while( Character.isDigit(arr[pos_start]) == Character.isDigit(arr[pos_end]) || arr[pos_end] == '.')
{
pos_end++;
if(pos_end == arr.length)
{
break;
}
}
//提取到的操作符或操作數字符串
String tmp = new String(arr, pos_start, pos_end - pos_start);
if(Character.isDigit(arr[pos_start]))
{
//將提取出的操作數放入arraylist
al.add(Double.parseDouble(tmp));
}
//將提取出的操作符放入arraylist
else
{
char[] op = tmp.toCharArray();
for(char c : op)
{
if(c == '+')
al.add(ADD);
else if(c == '-')
al.add(SUB);
else if(c == '*')
al.add(MUL);
else if(c == '/')
al.add(DIV);
else if(c == '(')
al.add(LBR);
else if(c == ')')
al.add(RBR);
else
throw new RuntimeException("Operator not allowd : " + tmp);
}
}
pos_start = pos_end;
}
}
這一步中使用了String對象的拼接來補全操作數,包裝類Double將字符串轉換爲它所表示的雙精度值,字符數組來定位操作數或操作符的開始結束位置。我們還要能反向將ArrayList表達式轉換回字符串表達式,這樣可以方便顯示每一步運算結果:
//顯示化簡到當前一步的, 以arraylist形式存儲的表達式al
public static void display()
{
//輸出提取到的所有元素‘
for(int i = 0; i < al.size(); i++)
{
if(al.get(i) == ADD)
System.out.print(" + ");
else if(al.get(i) == SUB)
System.out.print(" - ");
else if(al.get(i) == MUL)
System.out.print(" * ");
else if(al.get(i) == DIV)
System.out.print(" / ");
else if(al.get(i) == LBR)
System.out.print("( ");
else if(al.get(i) == RBR)
System.out.print(" )");
else
System.out.print(al.get(i));
}
System.out.println("\n");
}
3.5 化簡無括號的表達式
//計算不含括號的表達式的值, 該表達式表示爲al中從start到end的一段元素
public static void eval(int start, int end)
{
//元素在當前化簡表達式中的位置
int i = start;
//化簡的停止位置
int stop = end;
//從左向右計算乘除法,每消去一個操作符則更新al
while(i <= stop)
{
double element = al.get(i);
//臨時存放結果
double rslt = 0;
//如果是乘除操作符,則計算該符結果並更新表達式爲化簡後的
if(element == MUL)
{
rslt = al.get(i - 1) * al.get(i + 1);
//刪除操作符和右操作數,左操作數用本步計算結果代替
al.remove(i + 1);
al.remove(i);
al.set(i - 1, rslt);
stop = stop - 2;
display();
}
else if(element == DIV)
{
rslt = al.get(i - 1) / al.get(i + 1);
al.remove(i + 1);
al.remove(i);
al.set(i - 1, rslt);
stop = stop - 2;
display();
}
else
{
i++;
}
}
i = start;
//從左向右計算加減法,每消去一個操作符則更新al
while(i <= stop)
{
double element = al.get(i);
//臨時存放結果
double rslt = 0;
//如果是加減操作符,則計算該符結果並更新表達式爲化簡後的
if(element == ADD)
{ rslt = al.get(i - 1) + al.get(i + 1);
al.remove(i + 1);
al.remove(i);
al.set(i - 1, rslt);
stop = stop - 2;
display();
}
else if(element == SUB)
{
rslt = al.get(i - 1) - al.get(i + 1);
al.remove(i + 1);
al.remove(i);
al.set(i - 1, rslt);
stop = stop - 2;
display();
}
else
{
i++;
}
}
}
這個功能用到了arraylist的remove操作。要特別注意刪除arraylist元素會改變arraylist長度,而且索引對應的元素也會有變動。刪除元素的順序不同,索引變動也會不同。如果要將索引變動範圍限制在remove掉的元素後面而不影響前面,應當從較大的索引開始刪除,再刪較小的索引。上面的程序在一遍遍歷中計算所有乘除法並更新,然後在另一遍遍歷中計算所有加減法並更新,到此無括號表達式化簡完畢,得到答案。3.6 化簡有括號的表達式
括號內部表達式的起止位置作爲參數傳給無括號表達式的化簡方法,就可以實現有括號方法對無括號方法的調用了:
//左右括號位置
int lbr_pos = 0;
int rbr_pos = 0;
//臨時存放括號對內表達式計算結果
double rslt = 0;
//如果表達式中還有括號的話,就從右向左繼續計算各個括號對裏表達式的值,並替換化簡
while(al.contains(LBR))
{
//最右邊的左括號位置
lbr_pos = al.lastIndexOf(LBR);
//其對應的右括號
rbr_pos = lbr_pos + 1;
while(al.get(rbr_pos) != RBR) rbr_pos++;
//計算該括號對內表達式的值, 並替換化簡
eval(lbr_pos + 1, rbr_pos - 1);
//刪除括號對
al.remove(lbr_pos + 2);
al.remove(lbr_pos);
display();
}
//計算所有括號化簡掉後的表達式的值
eval(0, al.size() - 1);
//化簡到最後剩下的唯一一個操作數就是答案
return al.get(0);
這段代碼不難理解,就是不斷尋找下一步要計算的括號,調用無括號方法計算完畢用結果替換後,再尋找更新的表達式中下一步要算的括號。4. 算術表達式解析器的完整代碼和輸出
import java.util.*;
import java.math.*;
//用來標識操作符的常數
interface OperatorTag
{
double ADD = Math.PI; //加
double SUB = Math.PI + 1; //減
double MUL = Math.PI + 2; //乘
double DIV = Math.PI + 3; //除
double LBR = Math.PI + 4; //左括號
double RBR = Math.PI + 5; //右括號
}
public class Test implements OperatorTag
{
//按表達式中出現的次序存儲提取出的操作數和操作符
static ArrayList<Double> al = new ArrayList<Double>();
public static void main(String[] args)
{
//要計算的字符串表達式
String expr = "( - 2.1)*(2 - 2*(3.1 + 4) + 9*2)";
double rslt = evalbr(expr);
System.out.println(rslt);
}
//計算含括號的表達式的值
public static double evalbr(String expr)
{
//解析這個expr表達式,結果存入arraylist
parse(expr);
//顯示解析後的arraylist中的表達式
display();
//左右括號位置
int lbr_pos = 0;
int rbr_pos = 0;
//臨時存放括號對內表達式計算結果
double rslt = 0;
//如果表達式中還有括號的話,就從右向左繼續計算各個括號對裏表達式的值,並替換化簡
while(al.contains(LBR))
{
//最右邊的左括號位置
lbr_pos = al.lastIndexOf(LBR);
//其對應的右括號
rbr_pos = lbr_pos + 1;
while(al.get(rbr_pos) != RBR) rbr_pos++;
//計算該括號對內表達式的值, 並替換化簡
eval(lbr_pos + 1, rbr_pos - 1);
//刪除括號對
al.remove(lbr_pos + 2);
al.remove(lbr_pos);
display();
}
//計算所有括號化簡掉後的表達式的值
eval(0, al.size() - 1);
//化簡到最後剩下的唯一一個操作數就是答案
return al.get(0);
}
//計算不含括號的表達式的值, 該表達式表示爲al中從start到end的一段元素
public static void eval(int start, int end)
{
//元素在當前化簡表達式中的位置
int i = start;
//化簡的停止位置
int stop = end;
//從左向右計算乘除法,每消去一個操作符則更新al
while(i <= stop)
{
double element = al.get(i);
//臨時存放結果
double rslt = 0;
//如果是乘除操作符,則計算該符結果並更新表達式爲化簡後的
if(element == MUL)
{
rslt = al.get(i - 1) * al.get(i + 1);
//刪除操作符和右操作數,左操作數用本步計算結果代替
al.remove(i + 1);
al.remove(i);
al.set(i - 1, rslt);
stop = stop - 2;
display();
}
else if(element == DIV)
{
rslt = al.get(i - 1) / al.get(i + 1);
al.remove(i + 1);
al.remove(i);
al.set(i - 1, rslt);
stop = stop - 2;
display();
}
else
{
i++;
}
}
i = start;
//從左向右計算加減法,每消去一個操作符則更新al
while(i <= stop)
{
double element = al.get(i);
//臨時存放結果
double rslt = 0;
//如果是乘除操作符,則計算該符結果並更新表達式爲化簡後的
if(element == ADD)
{ rslt = al.get(i - 1) + al.get(i + 1);
al.remove(i + 1);
al.remove(i);
al.set(i - 1, rslt);
stop = stop - 2;
display();
}
else if(element == SUB)
{
rslt = al.get(i - 1) - al.get(i + 1);
al.remove(i + 1);
al.remove(i);
al.set(i - 1, rslt);
stop = stop - 2;
display();
}
else
{
i++;
}
}
}
//提取字符串中的操作符和操作數並存儲到al中
public static void parse(String expr)
{
//去掉所有空格
String str = expr.replace(" ", "");
//標記單目操作符負號位置
int minus_pos = 0;
//將表達式中的所有單目運算符負號轉化成雙目運算符減號,補充上左操作數0
while( (minus_pos = str.indexOf("(-")) != - 1)
{
str = str.substring(0, minus_pos + 1) + "0" + str.substring(minus_pos + 1);
}
//一個操作數、操作符的開始位置
int pos_start = 0;
//一個操作數、操作符的結束位置
int pos_end = 0;
//將字符串轉換爲字符數組
char[] arr = str.toCharArray();
//逐個提取表達式中的操作數和操作符
while(pos_start != arr.length )
{
pos_end = pos_start;
while( Character.isDigit(arr[pos_start]) == Character.isDigit(arr[pos_end]) || arr[pos_end] == '.')
{
pos_end++;
if(pos_end == arr.length)
{
break;
}
}
//提取到的操作符或操作數字符串
String tmp = new String(arr, pos_start, pos_end - pos_start);
if(Character.isDigit(arr[pos_start]))
{
//將提取出的操作數放入arraylist
al.add(Double.parseDouble(tmp));
}
//將提取出的操作符放入arraylist
else
{
char[] op = tmp.toCharArray();
for(char c : op)
{
if(c == '+')
al.add(ADD);
else if(c == '-')
al.add(SUB);
else if(c == '*')
al.add(MUL);
else if(c == '/')
al.add(DIV);
else if(c == '(')
al.add(LBR);
else if(c == ')')
al.add(RBR);
else
throw new RuntimeException("Operator not allowd : " + tmp);
}
}
pos_start = pos_end;
}
}
//顯示化簡到當前一步的表達式
public static void display()
{
//輸出提取到的所有元素‘
for(int i = 0; i < al.size(); i++)
{
if(al.get(i) == ADD)
System.out.print("+");
else if(al.get(i) == SUB)
System.out.print("-");
else if(al.get(i) == MUL)
System.out.print("*");
else if(al.get(i) == DIV)
System.out.print("/");
else if(al.get(i) == LBR)
System.out.print("(");
else if(al.get(i) == RBR)
System.out.print(")");
else
System.out.print(al.get(i));
}
System.out.println("\n");
}
}
輸出:(-2.1)*(2-2*(3.1+4)+9*2)
(0.0-2.1)*(2.0-2.0*(3.1+4.0)+9.0*2.0)
(0.0-2.1)*(2.0-2.0*(7.1)+9.0*2.0)
(0.0-2.1)*(2.0-2.0*7.1+9.0*2.0)
(0.0-2.1)*(2.0-14.2+9.0*2.0)
(0.0-2.1)*(2.0-14.2+18.0)
(0.0-2.1)*(-12.2+18.0)
(0.0-2.1)*(5.800000000001)
(0.0-2.1)*5.800000000001
(-2.1)*5.800000000001
-2.1*5.800000000001
-12.1800000000001
5. 總結
從這個小項目可見,利用arraylist的可變容量和索引查找這兩個優勢,用它來表示表達式大大簡化了編碼和理解的難度。Java API是一本百科全書,限於能力筆者選擇arraylist爲例來做練習。