解密 LL與LR解析 1(譯)

2021-06-16 22:40:32 字數 4038 閱讀 8824

解密:ll與lr解析 1

翻譯:楊貴福

由於gfw,我無法聯絡到作者,所以沒有授權,瞎翻譯的。原文在這裡[

2023年7月22日

我最初解析理論的經歷來自大學時自學程式語言的時候。當我學到像ll,lr還有它們的變型 (比如strong-ll, slr, lalr等等)的時候,我迷惑了。我覺得正注視著的是艱深而強大的咒語,它的重要意義我尚不能領會,但是我確信,總有一天,像"從左至右匯出""最右匯出"這些術語會融匯貫通,於是我繼續努力期待明白的一天。

現在我可以說,經過10年的時間再加上看了一整架解析類的書以後,我把這些演算法理解得不錯了。但是我看待它們的角度和我看過的文獻都非常不同。我更多地從實現的角度,而不是數學的角度,數學的角度也起了一些作用 (楊注:瞎翻譯的)。無論如何,我想解釋一下我是如何看待這些演算法的,希望有人也像我一樣覺得這個角度更直觀。

1. 解析 與 波蘭表式法

如果你在大學學習電腦科學,或者甚至你要是有個惠普的計算器 (楊注:我從來沒見過逆波蘭的hp計算器,而且,空格在那上面如何表示啊?) ,你就見過波蘭和逆波蘭表示法。它們能不用符號,也不用四則運算順序規則,就能寫出數**算表示式。我們習慣於把表示式寫作中綴形式,在這種形式下,操作符置於運算元二者之間:

1 + 2 * 3

在這種形式下,你如何知道計算的優先順序呢?你不得不按約定的規則 (四則混合運算的法則)。你如何想按不同的次鄧,就必須用括號了,像這樣:

1 (1 + 2) * 3

在波蘭和逆波蘭表示法中,你不必關心四則運算的優先順序,也不必加括號,同樣可以避免二義性。這是通過把操作符放在運算元之前(波蘭表示法)或之後 (逆波蘭表示法)實現的。它們也分別被稱為字首和字尾表示法。

// 第乙個例子: 1 + 2 * 3 // 中綴+ 1 * 2 3 // 波蘭表示法 (字首) 1 2 3 * + // 逆波蘭表示法 (字尾)

// 第二個例子: (1 + 2) * 3 // 中綴* + 1 2 3 // 波蘭表示法 (字首) 1 2 + 3 * // 逆波蘭表示法 (字尾)

除了不需要括號,也不需要運算次序的約定以外,波蘭和逆波蘭表示法在寫運算器 (求值)的時候也容易很多 (也許hp計算器的設計師用逆波蘭表示法,就是為了能去巴哈馬群島度一周假) 。下面是乙個python實現的逆波蘭的簡單求值器。

1 # 函式定義了操作符,及如何依據操作符求值

2 # 本例假設操作符都是二值的,不過容易擴充套件為多值。

3 ops =

7   

8 def eval(tokens):

9   stack =

10   

11   for token in tokens:

12     if token in ops:

13       arg2 = stack.pop()

14       arg1 = stack.pop()

15       result = ops[token](arg1, arg2)

17     else:

19   

20   return stack.pop()

21   

22 print "result:",  eval("7 2 3 + -".split())

波蘭和逆波蘭表示法,確實如通常所說的,需要事先知道所有操作符的引數數量。這裡的引數數量,指的是操作符所作用的運算元的數量。這意味著,單值操作符負號和二值操作符減法,是兩個不同的操作符。否則,我們在遇到操作符的時候,就不知道從棧中彈出多少個運算元。

一種避免了這個問題的類似表達方法,是lisp語言的s-表示式。s-表示式 (還有類似的編碼形式,比如xml)避免了固定操作符引數個數的需要,實現這一效果的方法是明確標記每個表示式的開始和結束之處。

1 ; lisp風格的字首表示式; 

2 ; 同乙個操作符可以有不同的引數數量

3 (+ 1 2) 

4 (+ 1 2 3 4 5) 

5 6 ; 我們前兩個例子在lisp中的等價表達方式

7 ; 字首: + 1 * 2 3 

8 (+ 1 (* 2 3)) 

9 10 ; 字首: * + 1 2 3 

11 (* (+ 1 2) 3)

lisp這一表達法有不同於前述方法的妥協 (前面的方法中要使用固定數量的引數,lisp需要括號),但是它們底層的解析/處理演算法是非常類似的,因此通常我們把它們視為略有不同的字首表示式。

看起來我好像有點跑題了,不過,其實我一直在偷偷地討論ll和lr。按我的觀點,ll和lr解析正分別與波蘭和逆波蘭表示法直接相關。不過為了完整地探索這個想法,我們需要先描述一下我們需要解析器輸出什麼。

作為乙個有趣的練習,請嘗試實現乙個演算法,用於把波蘭表示式轉化為逆波蘭表示式。看看你是否可以不需要先把整個表式式轉化為為一棵樹;你可以只用乙個棧實現這個效果。現在,比如你又要實現相反的過程 (從逆波蘭到波蘭)--你只需在輸入上執行同乙個演算法,這回轉換的方向就相反了。當然,你也可以構造一棵中間的樹,但是這導致 o(輸入長度) 的空間,而單使用乙個棧的解決方案只需要 o(樹的深度) 的空間。如何從中綴到字尾呢?有乙個非常聰明和高效的演算法,稱為 排程場演算法[

2. 解析器及輸出

我們一致認可解析器的輸入是token的乙個流 (這個流極可能來自乙個詞法分析器,不過我們可以以後再討論這一部分)。不過解析器的輸出是什麼?你可能傾向於說"一棵解析樹"。當然你可以用解析器構造出一棵解析樹,不過也可能不是這樣,而是一種完全不構造解析樹的輸出。比如,這個bison的例子[ ,在解析的同時求值了算術表示式。每次當子表示式被識別出來,它立即被求值,直到最終的結果是乙個單獨的數。從來沒有解析樹顯式地構造出來。

因此,說解析器的輸出是一棵解析樹不具有足夠的一般性。相反地,我斷言:解析器的輸出,至少我們今天討論的ll和lr的輸出,是解析樹的 *遍歷*。

如果觸動了哪位真理潔癖的神經,我在此道歉。我可以聽到有人**道,樹的遍歷是一種演算法,是你施加於一棵樹上的操作。我怎麼能說解析器輸出了一棵樹的遍歷呢?答案在於,請回想一下剛才的波蘭和逆波蘭表式法。它們通常只是一種數學算式的表示法,不過我們也可以更一般性地把它們視為 對樹的遍歷的扁平和線性的 (序列化的)編碼方式。

回想 下我們的第乙個例子 1 + 2 * 3。下面是這個表示式的樹形的寫法:

+

/ \1   *

/ \2   3

有三種方法遍歷這個二叉樹,如在維基百科上所給出的:中序遍歷 (in-order) ,先序遍歷 (pre-order) ,後序遍歷 (post-order)。它們的不同只在於你訪問父節點的時機,是在訪問子節點之前 (先序),之後 (後序),或者左右子樹之間(中序)。這三者正與中綴、波蘭、逆波蘭表示法對應。

1 + 2 * 3 // 中綴表示式,中序遍歷+ 1 * 2 3 // 波蘭 (字首)表示式,先序遍歷1 2 3 * + // 逆波蘭 (字尾)表示式,後序遍歷

所以,波蘭和逆波蘭表示法 完全地編碼了一棵樹結構,並且規定了你遍歷它的步驟。在這些編碼方法與一棵實際的解析樹之間的主要區別,在於 波蘭和逆波蘭表示法 編碼的訪問並非隨機的。對於一棵真實的樹 (楊注:計算機裡的真實,不是現實的真實,哈哈,所謂真實),你可以跟隨乙個內部節點到它的右子樹,或者它的左子樹,或者甚至 (對於許多樹而言)它的父節點。在這些線性的編碼方案中,就沒有這種靈活性:你只能採用它已經這樣編碼了的那種遍歷方法。

但是,好的一方面是,它使用解析樹的輸出是乙個流,這個流是在解析行為發生的時候產生的。這也是bison的那個例子,它如何在沒有實現構造一棵樹的情況下,就能夠求值算術表示式。如果真的需要一棵不是扁平編碼的樹的話,從線性的樹遍歷中很容易就能構造出一棵來。不過,當不需要這棵真的樹的話,構造它的代價就完全可以避免。

這就引出了關鍵點:

ll和lr解析器操作之主要不同在於,ll解析器輸出解析樹的先序遍歷,而lr解析器輸出後序遍歷。

這等價於那些更傳統,但是 (按我的觀點)更易令人迷惑和不那麼直觀的關於區別的解釋:

* "ll解析器產生乙個最左匯出,而lr解析器產生乙個逆轉最右匯出。"

* "ll解析器自頂向下把樹構造出來,而lr解析器自底向上構造。"

* ll解析器通常稱為"帶**的解析器"(楊注:原文predictive parsers,這是不是有約定的翻譯啊),而lr解析器稱為歸約解析器 (楊注:原文shift-reduce )。

今天先翻譯到這裡,原文後面還有。

LL(1)文法解析

一 問題描述 給定上下文無關文法,對其進行解析,得出first集和follow集。在有能力的情況下,可以求出ll 1 分析表。二 演算法設計 本程式主要分成三塊內容,第一是文法的讀入解析,第二是first集的求解,第三為follow集的求解。文法的解析需要根據整行讀入的文法,生成rulelist 表...

譯 用 Rust 實現 csv 解析 part1

隨著 csv 1.0 剛剛發布,關於使用 rust 讀取和寫入 csv 資料的教程是時候發布了。本教程針對的是剛入門的 rust 程式設計師,因此有相當多的示例,並花了一些時間在基礎的概念解釋上。你可能會發現,其中部分內容很有用,但對有經驗的 rust 程式設計師來說,快速瀏覽會更有利。有關 rus...

加密與解密函式authcode解析

iscuz的 authcode 函式可以說對中國的php界作出了重大貢獻。包括康盛自己的產品,以及大部分中國使用php的公司都用這個函式進行加密,authcode 是使用異或運算進行加密和解密。原理如下,假如 加密明文 1010 1001 密匙 1110 0011 密文 0100 1010 得出密文...