1. 程式人生 > >當你編碼時你在做什麼:談程式設計的本質(二)可愛的樹

當你編碼時你在做什麼:談程式設計的本質(二)可愛的樹

憋了好久的一篇,主題有點大一直沒有寫完,中間隔了很長時間現在已經有點撿不起來了,索性先發出來吧。至少個人覺得,完成的部分還是總結了一些有用的東西。關於Tree之上的屬性、遞迴演算法等,只能等狀態回來了再補充了。

I think that I shall never see
A poem lovely as a tree.
Poems are made by fools like me,
But only God can make a tree.
– Trees, Joyce Kilmer, 1886–1918

曾經畫過資料結構之間關係的圖,就像古代的哲學家思考萬物之源一樣,當時的自己也在苦苦思考一個問題:究竟哪個資料結構是本源?聽起來有些鑽牛角尖,的確大可不必一定排出個坐次。但如果讓我選出最重要的一個,我可以毫不猶豫地說是:Tree。如果再想一個能與之並駕齊驅的,我想應該是Hashing吧,畢竟《The Algorithm Design Manual》裡說道:

“I once heard Udi Manber - then Chief Scientist at Yahoo -talk about the algorithms employed at his company. The three most important algorithms at Yahoo, he said, were hashing, hashing, and hashing.”

但我們本節的主角是Tree,所以關於Hashing的各種妙用還是且聽下次分解吧。但為什麼不是Graph呢?Graph是更加general的概念,我們可以稱Tree是無環連通圖(Acyclic Connected Graph),反之我們也可以稱Graph是可以有環的森林(Cyclic Forest)。所以其實兩者沒有本質區別,選Tree的原因從下文就可以看出,因為Tree可以準確地表達出程式碼的執行,而Graph則可以表示更多的東西(有環、不連通)。

下面就先來看一看Tree都有哪些用處,以下每一種應用都很重要,但我們還是分個主次,按照與程式設計本質的關係緊密程度排列:

  • Configuration Graph:Tree最核心的一點就是它可以表示程式的執行路徑,即前一部分我們提到過的State Tree。在計算理論中,Configuration就是圖靈機的一個狀態,Non-deterministic圖靈機在每一步都可以有多種遷移方式,所以Configuration的遷移變化形成了Graph。如果是Deterministic的也就是每步都沒有選擇,在遷移表中都只有固定一個選擇,那就形成了List。所以,Tree在List和Graph之間承上啟下。
  • Analysis of Algorithm:有了前面的鋪墊,很自然地通過分析Tree的特點,我們就可能知道程式的執行情況。比如:
    • 遞迴執行情況:CLRS中通過Tree分析Recursion的時間複雜度(所有Node的總個數,每條Path都會執行)
    • 各種輸入下的最壞情況:還有用Decision Tree分析了Comparsion Sorting在所有可能Input下的時間複雜度下界(最長Simple Path就是Worst-case的執行時間,Configuration Graph也是這樣分析時間複雜度的)
    • 遞迴執行的特例:用DAG表示Dynamic Programming問題的公共Subproblem間的關係等。
  • Searching:用Tree代表數學意義上的集合Set來實現搜尋功能,同時相對於Hashing只能判斷是否存在,Tree通過維護了元素之間的關係進行增強
    • 同屬關係-Disjoint Set:稍微特殊一點的Tree,一般我們都是從Root自頂向下查詢,可Disjoint Set卻是反過來從Leaf找到Root,以Root結點作為當前Tree所代表的Set的Representive。如果兩個元素最終找到的Root不同那就Union成一顆Tree,所以這也叫Union-Find演算法。同一Tree內結點間沒什麼關係,只是表示屬於同一集合。
    • 大小關係-BST/B-Family:這是我們最熟悉的Tree的應用,巧妙地利用Tree對資料進行”索引“,典型的資料結構有:Binary Search Tree(BST)、B/B+ Tree Family以及更高階的對寫優化的Buffer Tree/B-epsilon Tree等。這其中有很大一部分其實都是最經典的Binary Search的延伸,《Programming Pearls》中對Binary Search細緻入微的講解是有其用意的。另外,BST的性質使其可以輕鬆地實現Binary Heap/Priority Queue,其實Heap也可以算作是一種大小關係的Tree結構,只不過只限定了父結點與孩子結點的大小關係,而未限定左右孩子之間的關係。
    • 順序關係-Trie:按照Prefix/Suffix對資料進行編碼壓縮,具體例子有經典的Trie、Huffman編碼、邏輯表示式的BDD(Binary Decision Diagram,本質上是一個Binary Trie)、Suffix Trie(回答關於substring的查詢)等。
    • 包含/累積關係-Sum/Interval:父結點上的衛星資料與子節點是包含或者累積關係,具體來說只要是associative的操作如sum、max、min等都可以,具體例子有Interval Tree、Segment Tree、Binary Indexed Tree(BIT)、Hash Tree(Merkle Tree)。
  • Parsing
    • Evaluation:例如我們可以將逆波蘭表示式構造成樹,然後求值。
    • Generation/Translation:編譯原理中,通過解析原始碼生成Abstract Syntax Tree(AST),然後在AST上做變換、優化等操作,最後生成更底層程式碼或其他語言的程式碼,從而實現編譯或翻譯功能。類似的,也可以將巢狀資料結構展開成一維平面結構。

其實前面這些應用可以分為兩類:用Tree表示程式本身的執行從而進行演算法設計和分析;用Tree作為詞典進行搜尋或進行語法解析等。這兩者一抽象一具體,一狀態機本身一狀態機之上(程式碼之中),這正如Sedgewick在《Algorithms: fundamentals, data structures, sorting and searching》中所說的:

“Trees are a mathematical abstraction that play a central role in the design and analysis of algorithms because:
1) We use trees to describe dynamic properties of algorithms.
2) We build and use explicit data structures that are concrete realizations of trees.”