当你编码时你在做什么:谈编程的本质(二)可爱的树

来源:互联网 发布:临沂蓝狐网络怎么样 编辑:程序博客网 时间:2024/04/28 10:56

憋了好久的一篇,主题有点大一直没有写完,中间隔了很长时间现在已经有点捡不起来了,索性先发出来吧。至少个人觉得,完成的部分还是总结了一些有用的东西。关于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.”

阅读全文
1 0