|
| 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 | +道友們可能在煉氣時就學過或自己想到過該如何巧用棧,從左到右掃過一個算式就能求其值。該算法名為[調車場算法](https://en.wikipedia.org/wiki/Shunting_yard_algorithm),而其遞迴版本(遞迴跟棧關係密切啊!)有另一個名字,叫[優先級爬升算法](https://en.wikipedia.org/wiki/Operator-precedence_parser)。 |
| 51 | + |
| 52 | +算法並不難,首先來觀察一個算式 |
| 53 | +```c |
| 54 | +1 == 5 - 3 % 2 * 4 - 1 |
| 55 | +``` |
| 56 | +從左到右來讀取,當讀取到 |
| 57 | +```c |
| 58 | +1 == 5 |
| 59 | +``` |
| 60 | +時,能確定 1 與 5 以 == 結合嗎?不能,若 5 的右側算子優先級比 == 還大,那 == 不會結合。 |
| 61 | + |
| 62 | +再來往右讀取 - 3 |
| 63 | +```c |
| 64 | +1 == 5 - 3 |
| 65 | +``` |
| 66 | +時,能確定 5 與 3 以 - 結合嗎?不能,若 3 的右側算子優先級比 - 還大,那 - 不會結合。 |
| 67 | + |
| 68 | +同理,一直向右讀取到 |
| 69 | +```c |
| 70 | +1 == 5 - 3 % 2 * 4 |
| 71 | +``` |
| 72 | +時,都無法確定任何一個算子結合,注意到,無法結合是因為,**至今遇到的所有算子中右側總是比左側優先級高,故始終無法結合**。(遇到這種右側總比左側高的結構,往往意味這能用棧來解題。) |
| 73 | + |
| 74 | +但再往右讀取一個算子就不一樣了 |
| 75 | +```c |
| 76 | +1 == 5 - 3 % 2 * 4 - 1 |
| 77 | +``` |
| 78 | +\- 的優先級小於 2 * 4 ,故 2 * 4 必先結合。結合後密不可分,僅以代數 x 表示,可視為 |
| 79 | +```c |
| 80 | +1 == 5 - 3 % x - 1 |
| 81 | +``` |
| 82 | +\- 比較的對象變為 % ,% 仍比 - 優先級高,故 3 % x 先結合,現在式子為 |
| 83 | +```c |
| 84 | +1 == 5 - x - 1 |
| 85 | +``` |
| 86 | +x 兩側的 - 優先級相等,但 - 是左結合,優先往左側結合,故 5 - x 先結合 |
| 87 | +```c |
| 88 | +1 == x - 1 |
| 89 | +``` |
| 90 | +此時 - 的優先級大於 == ,如果右側還有算子的話,倒是無法確定 x - 1 會結合,但現在右側已經沒東西了,故 x - 1 結合,最後輪到 == 。 |
| 91 | + |
| 92 | +用虛擬碼來表述該過程: |
| 93 | +``` |
| 94 | +每讀取一個新算子,拿其與當下算式最右側的算子做比較 |
| 95 | +
|
| 96 | +若新算子優先級較低: |
| 97 | + 則最右側算子可結合,此時再與結合後算式中的最右側算子比較。 |
| 98 | +若新算子優先級較高: |
| 99 | + 無法確定優先級,將新算子丟入算式 |
| 100 | +
|
| 101 | +當算子讀取完畢,從右往左結合。 |
| 102 | +``` |
| 103 | +該算法稍作加強,也能處理括號,基本想法是每當遇到右括號時,就當做算子已經暫時讀取完畢,算子算式由右一直向左結合直到碰到左括號。 |
| 104 | + |
| 105 | +#### 以棧表示該過程 |
| 106 | +此過程能以棧輕易模擬,具體作法請見下方範例。 |
| 107 | + |
| 108 | +#### 遞迴做法 |
| 109 | +TODO: |
| 110 | + |
| 111 | + |
| 112 | +### 混用回溯剖析與優先級爬升 |
| 113 | + |
| 114 | +那吾人不妨在剖析器中將算子、算元與括號順序保留,再由此優先級爬升算法來處理優先級。 |
| 115 | + |
| 116 | +```語法 |
| 117 | +算式 = 原子式・(算子・原子式)* |
| 118 | +
|
| 119 | +原子式 = 數字 |
| 120 | + | 變數 |
| 121 | + | "("・算式・")" |
| 122 | +``` |
| 123 | + |
| 124 | +由於括號已經由原子式處理,在剖析算式中不需要處理括號,應用前述的簡單優先級處理算法即可。 |
| 125 | + |
| 126 | +```rust |
| 127 | +fn 優先級(運算子: O運算子) -> u64 { |
| 128 | + match 運算子 { |
| 129 | + O運算子::乘 => 4, |
| 130 | + O運算子::除 => 4, |
| 131 | + |
| 132 | + O運算子::餘 => 3, |
| 133 | + |
| 134 | + O運算子::加 => 2, |
| 135 | + O運算子::減 => 2, |
| 136 | + |
| 137 | + O運算子::等於 => 1, |
| 138 | + O運算子::異於 => 1, |
| 139 | + O運算子::小於 => 1, |
| 140 | + O運算子::小於等於 => 1, |
| 141 | + O運算子::大於 => 1, |
| 142 | + O運算子::大於等於 => 1, |
| 143 | + } |
| 144 | +} |
| 145 | +fn 剖析算式(&self, 游標: usize) -> Option<(O算式, usize)> { |
| 146 | + let (原子式, mut 游標) = self.剖析原子式(游標)?; |
| 147 | + |
| 148 | + // TODO: 將算子棧算元棧包裝到一個 struct 裡 |
| 149 | + let mut 算元棧 = VecDeque::<O算式>::new(); |
| 150 | + 算元棧.push_back(原子式); |
| 151 | + |
| 152 | + let mut 算子棧 = VecDeque::<O運算子>::new(); |
| 153 | + |
| 154 | + while let Some((新算子, 新游標)) = self.消耗運算子(游標) { |
| 155 | + let (新算元, 新游標) = self.剖析原子式(新游標)?; |
| 156 | + |
| 157 | + // 讀取到新算子,進行棧操作 |
| 158 | + while !算子棧.is_empty() && 優先級(算子棧.back().unwrap()) >= 優先級(&新算子) |
| 159 | + { |
| 160 | + // 新算子優先級較低,代表棧中的算子算元可以先結合了。 |
| 161 | + let 右算元 = 算元棧.pop_back().unwrap(); |
| 162 | + let 左算元 = 算元棧.pop_back().unwrap(); |
| 163 | + let 運算子 = 算子棧.pop_back().unwrap(); |
| 164 | + 算元棧.push_back(O算式::二元運算(O二元運算 { |
| 165 | + 運算子, |
| 166 | + 左: Box::new(左算元), |
| 167 | + 右: Box::new(右算元), |
| 168 | + })); |
| 169 | + } |
| 170 | + |
| 171 | + // 原式中能決定結合的算子跟算元都決定了,推入新算子跟算元 |
| 172 | + 算子棧.push_back(新算子); |
| 173 | + 算元棧.push_back(新算元); |
| 174 | + |
| 175 | + 游標 = 新游標 |
| 176 | + } |
| 177 | + |
| 178 | + while !算子棧.is_empty() { |
| 179 | + // 無新算子,棧中的算子算元最右向左依次結合 |
| 180 | + // TODO: 封裝此二相同 while 內容 |
| 181 | + let 右算元 = 算元棧.pop_back().unwrap(); |
| 182 | + let 左算元 = 算元棧.pop_back().unwrap(); |
| 183 | + let 運算子 = 算子棧.pop_back().unwrap(); |
| 184 | + 算元棧.push_back(O算式::二元運算(O二元運算 { |
| 185 | + 運算子, |
| 186 | + 左: Box::new(左算元), |
| 187 | + 右: Box::new(右算元), |
| 188 | + })); |
| 189 | + } |
| 190 | + |
| 191 | + assert_eq!(算元棧.len(), 1); |
| 192 | + |
| 193 | + Some((算元棧.pop_back().unwrap(), 游標)) |
| 194 | +} |
| 195 | +``` |
0 commit comments