《计算机程序设计艺术》数据结构之树

个人掘金专栏算法

上节是线性结构,这节的树是「非线性」结构。bash

树的定义

  1. 有一个特别指定的节点 T,记为 root(T),它叫作树的根。
  2. 剩余的节点被分划成 m>=0 个「不相交」的集合,T1,...,Tm,每一个集合也是一个树,称为子树。

这个定义很奇怪,它是一个递归定义,由于用树来定义了树。post

但这并无什么问题,由于让 m = 0,那么一个节点就是一棵树。而后咱们能够用若干个这样的树组成更大的树。spa

树也有非递归形式的定义,但递归定义彷佛是最合适的。由于天然界中的树也是由幼树的树芽逐渐长出有本身的树芽的子树芽。3d

术语

  • 度:一个节点的子树的个数,叫作该节点的度(是枝开二度类比来的吗?)
  • 叶子节点:度数为零(没开过)的节点叫作叶子节点
  • 分支节点:非叶子节点
  • 级:root(T) 的级为 0,它包含的子树的根节点都比 root(T) 的级别大 1,以此类推。

  • 有序树:若是子树 T1,...,Tm 的相对顺序是重要的话,咱们就说该树是有序树。也有人称之为「平面树」,由于它与在一个平面中嵌入树的方式有关。T1 叫第一棵子树,T2 叫第二棵子树等等。
  • 有向的树:若是不把仅仅是节点子树的相对次序不一样的两个树看成不一样的树,就说这样的树是有向的。由于考虑的仅仅是节点相对的方向,而不是它们的次序。(这个定义很晦涩,学了图以后有更简单的定义)。

从如今开始,咱们假定全部的树都是有序的,除非另有说明。指针

好比下面两棵树是不一样的树:code

  • 森林:零个或多个不相交的树的集合。也能够说树除了根以外的全部节点造成一个森林。
  • 二叉树:该树的每一个节点至多有 2 个子树。当只有一个子树时,咱们会区分左子树和右子树。

注意:二叉树不是树的特殊状况。二叉树彻底是另外一个概念,虽然它跟树有不少联系。例以下图:cdn

对于二叉树,这是两棵不一样的树;对于树,这两个是相同的树。(在这一点上我我的表示不太理解,对于树来讲它们能够是不一样的树哇)。blog

  • 0-2树:每一个节点恰有0个或2个子树的树。

树的画法

画树的时候,能够把根画在下面,也能够把根画在上面,甚至左边。继承

最推荐的画法是把根画在上面,缘由以下:

  1. 符合阅读习惯,大部分人都是从上往下阅读
  2. 符合更多术语的意义,如 subtree、supertree 等。
  3. 方便使用深浅来描述节点。深的在下面,浅的在上面。

还能够用不少其余方式来表示树。

上图能够表示为下面三种不一样的形式:

一个代数公式也定义了一个隐含的树,a - b * (c/d + e/f) 表示:

遍历二叉树

有三个主要的方法遍历二叉树:

  1. 先根序
  2. 中根序(也叫对称序)
  3. 后根序

中根序遍历

设 T 存储了二叉树的节点的地址(头指针),A 是一个栈,做为辅助。步骤以下:

  • 第一步,置栈 A 为空,置 P <- T
  • 第二步,若是 P 为空,则跳到第四步;若是 P 不为空,则跳到第三步。
  • 第三步,置 A <= P(把 P 压入 A),而后置 P <- LeftLink(P),并返回至第二步。
  • 第四步,若是 A 不为空,则置 P <= A(弹出栈顶内存放入 P);若是 A 为空,则算法结束
  • 第五步,访问 Node(P),而后置 P <- RightLink(P),并返回至第二步。

用人类的话来讲,就是

  1. 把全部最左边的节点依次压入 A 栈,直到没有最左的节点。
  2. A 栈依次弹栈入 P,打出 P 节点的值,再看 P 有没有右子树,若是有,就让右子树经历一下第 1 步。没有右子树就算了。
  3. A 栈为空则说明打印完毕。

新手乍看会以为奇怪,这只是先遍历了左边,再遍历了右边而已啊,哪有在中间访问父节点?

这个就须要新手本身动手模拟一遍整个过程了,确实有在「中间」访问父节点哦。

能够用几乎相同的算法来描述先根序遍历,后根序稍微难一点,因此通常来讲咱们优先使用前两种遍历方式。

前驱与后继

前驱就是上一个节点(能够是子节点也能够是父节点),后继就是下一个节点。因为我认为这两个术语十分无聊,因此下文统一以大白话来叙述。

假设 P 指向一个二叉树,那么

  • P$ 表示在中根遍历时节点 Node(P) 的下一个节点的地址
  • $P 表示在中根遍历时节点 Node(P) 的上一个节点的地址

为何用 $ 呢?由于 $ 是对称的,因此能够表示对称序(中根序)。

  • P* 表示在先根遍历时节点 Node(P) 的下一个节点的地址
  • *P 表示在先根遍历时节点 Node(P) 的上一个节点的地址

后根就不讲了,省得记混。

内存表示

咱们能够把二叉树中的每一个节点表示为 [LeftLink, Node, RightLink],那么树

就能够表示为:

这种表示方法会出现不少空连接,空连接甚至比有用的连接还要多,怎么解决这个问题呢?

穿线的树

看图:

  1. 每一个节点都有两条指向其余地方的线
    1. 可能两条线都是实线,指向本身的子树,好比 C
    2. 可能两条线一实一虚,一个是子树,一个是比它高的节点(规则不明),好比 B 和 E
    3. 可能两条线都是虚线,好比 D、G、H、J
  2. 只有 D 和 J 的虚线比较特殊,分别指向「最左」和「最右」,后面再说。

那么虚线具体指向哪个比它高的节点呢?答案是

  • 左虚线指向中根遍历时的上一个节点
  • 右虚线指向中根遍历时的下一个节点

从新开图,能够得出中根中序遍历的顺序是:

D B A E G C H F J

好了,咱们知道怎么画穿线了,那么怎么用内存表示呢?

给每一个节点再加两个 bit:[LeftTag, LeftLink, Node, RightLink, RightTag]

LeftTag 和 RightTag 的值只能是 0 或 1,0 表示没有虚线(有实线),1 表示有虚线。

由于咱们能够经过中根遍历算法肯定具体的位置,因此就不须要再存一个地址了。

穿线的存在使得遍历二叉树不须要额外的辅助栈。

类似和等价

  • 类似的树:若是二叉树 T 和 T' 结构相同,则它们是类似的
  • 等价的树:若是二叉树 T 和 T' 类似,且对应的节点包含相同的信息,则说它们是等价的。

书中对如何证实两个数类似和等价给出了形式化的步骤,有兴趣能够自行翻开 311 页查看。

树的二叉树表示

树和二叉树的区别咱们复习一下:

  1. 树老是有一个根节点;树的每一个节点能够有 0,1,2,3,4,5,6,... 个子树。
  2. 二叉树能够为空;它的每一个节点能够有 0,1,2 个子二叉树;它区分左儿子和右儿子。

这一节研究「咱们是否能把一棵树表示为一棵二叉树」。

考虑有以下两棵树

咱们把每棵树的连接删掉,而后把全部拥有同一个父亲的节点横向连接(哥哥指向弟弟),而后让每一个父亲只连接它的大儿子(爸爸指向大儿子),再把两个根节点连起来,就获得了这样的图:

接下来就神奇了,把上图顺时针转 45 度,而后微调一下,就获得了二叉树!

这个过程是可逆的,因此任何二叉树都对应于惟一片森林。

为何会这样呢?由于每一个节点至多有一个大儿子,至多有一个弟弟呀,因此每一个节点的度至多为 2,这知足二叉树的定义。

经过数学概括饭,很容易证实三棵树的森林,也能够转化成二叉树。以此类推。

这个转换过程叫作「森林和二叉树之间的天然对应」。

形式化的描述以下:

对于具备 n+1 个节点的树 T,和具备 n 个节点的二叉树,令 F = (T1, T2, ..., Tn) 是 T 的子树组成的森林,二叉树 B(F) 的定义以下:

  1. 若是 n = 0,则 B(F) 为空
  2. 若是 n > 0,则 B(F) 的根是 root(T1);B(F) 的左子树是 B(T11,T12,...,T1m),其中 T11,T12,...,T1m 是 T1 的子树;B(F) 的右子树是 B(T2,T3,...,Tn),这里又是一个递归定义。

从新理解遍历

天然对应转行过程其实就是对树进行先根遍历。

咱们把上面的两棵树用另外一种形式从新表示一下:

(A(B,C(K)), D(E(H),F(J),G))

对其进行先根遍历,获得 A B C K D E H F J G,竟然就是把括号去掉而已!

这就是为何把这种遍历叫作「天然对应」了,由于真的很天然。

再举个例子:

对书的章节号组成的树进行先根遍历,获得的结果是

1
1.1
1.1.1
1.1.2
1.2
1.2.1
1.3
1.3.1
1.3.2
复制代码

够不够天然?

再举个例子,欧洲的王位继承。

  1. 国王死了,传位给大儿子,大儿子死了就传位给大儿子的大儿子。
  2. 若是很不幸,大儿子全家都死了,就传给其余儿子中最大的一个。以此类推。

因此,只须要把皇室的族谱先根遍历一下,就能够获得王位继承顺序了。

够不够天然?

因此先根序是列出树中节点最天然的方法。

接下来是后根序。

后根序

(A(B,C(K)), D(E(H),F(J),G))

的先根序是

A B C K D E H F J G

后根序则是

B K C A H E J F G D

这其实就是把原树 (A(B,C(K)), D(E(H),F(J),G)) 的书写方式改一下,把父节点写在括号右边,而不是左边:

((B, (K)C)A, ((H)E,(J)F,G)D)

经过把对应的定义作对比,能够观察出:

  1. 先根遍历森林与先根遍历对应二叉树彻底相同
  2. 后根遍历森林与中根遍历对应二叉树彻底相同

树的基本数学性质

扩充的二叉树

在二叉树全部的空节点的地方加上特殊节点,就获得了一个扩充的二叉树。

假设有 n 个圆圈节点,s 个方形节点,那么边的数量就是 n + s - 1

因为每一个圆圈对应两条边,因此边的数量也能够表示成 2n

因而咱们获得 n + s - 1 = 2n,由此推出 s = n + 1

s = n + 1 意味着,特殊的方形节点老是比圆形节点多一个。

即便 n = 0,这个公式也成立。

通路长度

图:通常把图定义为点(叫作顶点) + 链接某些不一样顶点的线(叫作边)的集合。简而言之,图 = 顶点 + 边。一对顶点之间最多有一条边。若是两个顶点被一条边链接,则称这两个顶点是「相邻」的。

通路:若是 VV' 是两个顶点,V_0 = VV_k 相邻与 V_{k+1}V_n = V',那么咱们就说 (V_0, V_1, ..., V_n) 是从 V 到 V' 的一条通路,长度为 n。

简单通路:若是 V_0, V_1, ..., V_{n-1} 都不相同,并且 V_1, V_1, ..., V_n 也都不相同,则这条通路的简单的。这句话的意思是 V_0V_n 能够相同,只要中途的点都不相同便可。

扩充二叉树的外部通路长度 E 的定义是「从根到每一个方形节点的通路长度之和」。

扩充二叉树的内部通路长度 I 的定义是「从根到每一个圆形节点的通路长度之和」。

上图中的 E = 3 + 3 + 2 + 3 + 4 + 4 + 3 + 3 = 25I = 2 + 1 + 0 + 2 + 3 + 1 + 2 = 11

而且能够证实,E = I + 2n

彻底二叉树

图中从左到右给全部节点编了号。你很容易发现

  • 节点 k 的父亲是 k/2
  • 节点 k 的儿子是 2k 和 2k+1
  • n 是圆形节点个数,那么方形节点的编号就是 n + 1 到 2n + 1

所以,彻底二叉树能够顺序存储,且结构隐含在节点的下标中。

带权通路

假设有 m 个实数 w_1 w_2 ... w_m,把这些实数做为扩充二叉树的方形节点,能够画出不一样的扩充二叉树。求如何找出「带权通路长度」最小的二叉树(其中带权通路长度 \Sigma w_jl_j 是指 w_j 乘以其到根节点的通路长度 l_j 的积的累加之和)。

假设这 m 个数字是 2 3 4 11,那么就有三种扩充二叉树:

三者的带权通路长度为别是 4*2+2*3+3*3+11*1=343*2+4*3+11*3+2*1=532*2+11*2+3*2+4*2=40。最小的是第一个。

求最小带权通路长度的树的漂亮算法是由 D.Huffman(哈夫曼)发现的。

  1. 首先求最小的两个值 w_iw_j 组成的二叉树(显然一个放左边一个放右边长度最小)
  2. 而后把这个数看作一个总体,值为 w_i + w_j,与剩下的数字放一块儿,递归地带入第 1 步便可,直到只剩两个

举例:求 2,3,4,7,11,13,17,19,23,29,31,41 的最优树

相关文章
相关标签/搜索