Skip to content

Commit 229241d

Browse files
committed
修訂調車場算法
1 parent 43f005f commit 229241d

1 file changed

Lines changed: 84 additions & 66 deletions

File tree

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

Lines changed: 84 additions & 66 deletions
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,7 @@
4747

4848
### 決定優先級的算法
4949

50-
道友們可能在煉氣時就學過或自己想到過該如何巧用棧,從左到右掃過一個算式就能求其值。該算法名為[調車場算法](https://en.wikipedia.org/wiki/Shunting_yard_algorithm),而其遞迴版本(遞迴跟棧關係密切啊!)有另一個名字,叫[優先級爬升算法](https://en.wikipedia.org/wiki/Operator-precedence_parser)
50+
道友們可能在煉氣時就學過或自己想到過該如何巧用棧,從左到右掃過一個算式求其值。該算法名為[調車場算法](https://en.wikipedia.org/wiki/Shunting_yard_algorithm),而其遞迴版本(遞迴跟棧關係密切啊!)有另一個名字,叫[優先級爬升算法](https://en.wikipedia.org/wiki/Operator-precedence_parser)
5151

5252
算法並不難,首先來觀察一個算式
5353
```c
@@ -69,7 +69,7 @@
6969
```c
7070
1 == 5 - 3 % 2 * 4
7171
```
72-
時,都無法確定任何一個算子結合,注意到,無法結合是因為,**至今遇到的所有算子中右側總是比左側優先級高,故始終無法結合**。(遇到這種右側總比左側高的結構,往往意味這能用棧來解題。)
72+
時,都無法確定任何一個算子結合,注意到,無法結合是因為,**至今遇到的所有算子中,新算子總是比舊算子優先級高**。(發現維護的序列具有單調特徵時,往往意味能用棧來解題。)
7373

7474
但再往右讀取一個算子就不一樣了
7575
```c
@@ -90,41 +90,27 @@ x 兩側的 - 優先級相等,但 - 是左結合,優先往左側結合,故
9090
此時 - 的優先級大於 == ,如果右側還有算子的話,倒是無法確定 x - 1 會結合,但現在右側已經沒東西了,故 x - 1 結合,最後輪到 == 。
9191

9292
用虛擬碼來表述該過程:
93-
```
94-
每讀取一個新算子,拿其與當下算式最右側的算子做比較
9593

96-
若新算子優先級較低:
97-
則最右側算子可結合,此時再與結合後算式中的最右側算子比較。
98-
若新算子優先級較高:
99-
無法確定優先級,將新算子丟入算式
94+
```虛擬
95+
想像吾人先蓋住整個算式,再從左到右慢慢展露整個算式。
96+
97+
持續向右讀取新算子:
98+
將新算子與已知算式最右側的算子做比較
99+
若新算子優先級較低:
100+
則最右側已知算子可結合,此時再與結合後算式中的最右側算子比較。
101+
重複此動作直到已知算子優先級低於新算子,此時沒有一個算子的優先級能確認,將新算子丟回已知算式
102+
若新算子優先級較高:
103+
沒有一個算子的優先級能確認,將新算子丟進已知算式
100104
101105
當算子讀取完畢,從右往左結合。
102106
```
103107
該算法稍作加強,也能處理括號,基本想法是每當遇到右括號時,就當做算子已經暫時讀取完畢,算子算式由右一直向左結合直到碰到左括號。
104108

105-
#### 以棧表示該過程
106-
此過程能以棧輕易模擬,具體作法請見下方範例。
107-
108-
#### 遞迴做法
109-
TODO:
110-
111-
112-
### 混用回溯剖析與優先級爬升
113-
114-
那吾人不妨在剖析器中將算子、算元與括號順序保留,再由此優先級爬升算法來處理優先級。
115-
116-
```語法
117-
算式 = 原子式・(算子・原子式)*
118-
119-
原子式 = 數字
120-
| 變數
121-
| "("・算式・")"
122-
```
123-
124-
由於括號已經由原子式處理,在剖析算式中不需要處理括號,應用前述的簡單優先級處理算法即可。
109+
#### 以棧實作:調車場算法
110+
此過程能以棧輕易模擬,具體作法可參照下方源碼:
125111

126112
```rust
127-
fn 優先級(運算子: O運算子) -> u64 {
113+
fn 優先級(運算子: &O運算子) -> u64 {
128114
match 運算子 {
129115
O運算子::=> 4,
130116
O運算子::=> 4,
@@ -142,54 +128,86 @@ fn 優先級(運算子: O運算子) -> u64 {
142128
O運算子::大於等於 => 1,
143129
}
144130
}
145-
fn 剖析算式(&self, 游標: usize) -> Option<(O算式, usize)> {
146-
let (原子式, mut 游標) = self.剖析原子式(游標)?;
147-
148-
// TODO: 將算子棧算元棧包裝到一個 struct 裡
149-
let mut 算元棧 = VecDeque::<O算式>::new();
150-
算元棧.push_back(原子式);
151131

152-
let mut 算子棧 = VecDeque::<O運算子>::new();
153-
154-
while let Some((新算子, 新游標)) = self.消耗運算子(游標) {
155-
let (新算元, 新游標) = self.剖析原子式(新游標)?;
132+
struct 調車場 {
133+
算元棧: VecDeque<O算式>,
134+
算子棧: VecDeque<O運算子>,
135+
}
156136

137+
impl 調車場 {
138+
fn new(首個算元: O算式) -> Self {
139+
Self {
140+
算元棧: vec![首個算元].into(),
141+
算子棧: vec![].into(),
142+
}
143+
}
144+
fn 結合棧中算子(&mut self) {
145+
let 右算元 = self.算元棧.pop_back().unwrap();
146+
let 左算元 = self.算元棧.pop_back().unwrap();
147+
let 運算子 = self.算子棧.pop_back().unwrap();
148+
self.算元棧.push_back(O算式::二元運算(O二元運算 {
149+
運算子,
150+
: Box::new(左算元),
151+
: Box::new(右算元),
152+
}));
153+
}
154+
fn 讀取(&mut self, 新算子: O運算子, 新算元: O算式) {
157155
// 讀取到新算子,進行棧操作
158-
while !算子棧.is_empty() && 優先級(算子棧.back().unwrap()) >= 優先級(&新算子)
156+
while !self.算子棧.is_empty() && 優先級(self.算子棧.back().unwrap()) >= 優先級(&新算子)
159157
{
160158
// 新算子優先級較低,代表棧中的算子算元可以先結合了。
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-
}));
159+
self.結合棧中算子();
169160
}
161+
// 棧中能結合的算子跟算元都結合了,推入新算子跟算元
162+
self.算子棧.push_back(新算子);
163+
self.算元棧.push_back(新算元);
164+
}
170165

171-
// 原式中能決定結合的算子跟算元都決定了,推入新算子跟算元
172-
算子棧.push_back(新算子);
173-
算元棧.push_back(新算元);
166+
fn 結束(&mut self) -> O算式 {
167+
while !self.算子棧.is_empty() {
168+
self.結合棧中算子();
169+
}
170+
assert_eq!(self.算子棧.len(), 0);
171+
assert_eq!(self.算元棧.len(), 1);
174172

175-
游標 = 新游標
173+
self.算元棧.pop_back().unwrap()
176174
}
175+
}
177176

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-
}
177+
```
178+
179+
#### 遞迴實作:優先級爬升算法
180+
TODO:
190181

191-
assert_eq!(算元棧.len(), 1);
192182

193-
Some((算元棧.pop_back().unwrap(), 游標))
183+
### 混用回溯剖析與優先級決定算法
184+
185+
那吾人不妨在剖析器中將算子、算元與括號順序保留,再由此優先級爬升算法來處理優先級。
186+
187+
```語法
188+
算式 = 原子式・(算子・原子式)*
189+
190+
原子式 = 數字
191+
| 變數
192+
| "("・算式・")"
193+
```
194+
195+
由於括號已經由原子式處理,在剖析算式中不需要處理括號,應用前述的調車場算法即可。
196+
197+
```rust
198+
fn 剖析算式(&self, 游標: usize) -> Option<(O算式, usize)> {
199+
let (原子式, mut 游標) = self.剖析原子式(游標)?;
200+
201+
let mut 調車場 = 調車場::new(原子式);
202+
203+
while let Some((新算子, 新游標)) = self.消耗運算子(游標) {
204+
let (新算元, 新游標) = self.剖析原子式(新游標)?;
205+
206+
調車場.讀取(新算子, 新算元);
207+
208+
游標 = 新游標
209+
}
210+
211+
Some((調車場.結束(), 游標))
194212
}
195213
```

0 commit comments

Comments
 (0)