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
6969``` c
70701 == 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