Skip to content

Commit 43f005f

Browse files
committed
再遇剖析:優先級特殊處理
1 parent 4581405 commit 43f005f

2 files changed

Lines changed: 198 additions & 3 deletions

File tree

book/零.一版/剖析(語法分析).md

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -231,9 +231,9 @@
231231
232232
算式 = 乘除式・重複乘除式
233233
234-
重複乘除式 = +・重複乘除式
235-
| −・重複乘除式
236-
| e
234+
重複乘除式 = +・乘除式・重複乘除式
235+
| −・乘除式・重複乘除式
236+
| e
237237
238238
乘除式 = 原子式・重複原子式
239239

book/零.二版/再遇剖析.md

Lines changed: 195 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,195 @@
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

Comments
 (0)