「树」是一类重要的非线性数据结构。本文介绍了树的基本概念、术语、性质。
一、树的基本概念和术语
树在生活中随处可见
树有许多特点:有一个树根、树根可以分出树干,树干可以分出许多树枝、树枝上有许多叶子。
树作为一种数据结构,也有相似的特点
-
树是一个有限集合
-
有且仅有一个特定的「根节点」(Root),如上图中的结点A
-
一颗树的根可以有许多「子树」,如根节点A有3颗子树(T1、T2、T3),这些子树本身也是一棵树,比如结点B又是树T1的根节点。
-
树的结点包含「数据元素」和「若干指向其子树的分支」
-
结点的度(Degree):结点拥有的子树数。如结点A的度为3,结点C的度为1,结点G的度为0
-
叶子(Leaf)或 终端结点:度为0的结点。如K
-
非终端结点 或 分支节点:度不为0的结点。如结点A、E、C。
-
内部结点:除根结点之外的分支节点,即在树内部的结点。如结点B、C、E、H。
-
孩子、双亲、兄弟、堂兄弟、祖先、子孙这些概念和族谱上的相同。
如:A的孩子是B、C、D,A是B、C、D的双亲,B、C、D是兄弟(同一双亲),F、G、H是堂兄弟(同一层但不同双亲),K的祖先是E、B、A,B的子孙是B、E、F、K、L。
-
层次:层次从根开始,根为第一层,根的孩子为第二层……
-
深度(Depth)或 高度:树中结点的最大层次。如树T的深度为4。
-
森林:即n(n≥0)颗互不相交的树的集合
由上述特点我们可以看出,树的结构是递归的。树T是一颗树、其根结点A的子树也是一棵树、A的子树的根节点的子树也是一棵树、A的子树的根节点的子树的根节点的子树也是一棵树……
二、二叉树
1. 二叉树的基本结构
二叉树是一种特殊的树,特点是每个结点至多只有两颗子树,所以二叉树中不存在度大于2的结点。二叉树的子树有左右之分,次序不能任意颠倒。
二叉树的5种基本形态:
- 空二叉树
- 仅有根结点的二叉树
- 右子树为空的二叉树
- 左子树为空的二叉树
- 左右子树都不为空的二叉树
二叉树的结构是递归的,一个二叉树或者为空,或者由一个根结点加上两颗左右子树构成的(这两颗子树也是二叉树)。
满二叉树和完全二叉树是两种特殊的二叉树。
满二叉树的特点在于「满」,即每层的结点数都是最大结点数。如下图:
完全二叉树的「完全」是相对于满二叉树来说的,下图是一个完全二叉树:
对一颗满二叉树和一颗完全二叉树按「自上向下,自左向右」的顺序进行编号,如上面两个图。完全二叉树中的所有结点(1~12结点)必须和满二叉树中的1~12结点在位置上一一对应。
如下左图为满二叉树,右图为非完全二叉树,因为其所有结点(1~6结点)和其对应的满二叉树中的1~6结点在位置上并不一一对应。
2. 二叉树的遍历
搞清了二叉树的结构,那么如何遍历它?
二叉树的结构是递归的,由根节点、左子树、右子树组成,所以我们只需递归地遍历这三个部分即可。根据遍历这三个部分的顺序的不同,有三种遍历方式:
- 先序遍历
- 中(根)序遍历
- 后(根)序遍历
(1) 先序遍历
基本步骤:
-
二叉树不为空时,
- 访问根结点
- 先序遍历左子树
- 先序遍历右子树
-
二叉树为空时,做空操作。
步骤描述:
- 访问根结点A
- A有左孩子B,B是A的左子树的根结点,访问结点B
- B有左孩子D,D是B的左子树的根结点,访问根结点D
- D没有左孩子
- D没有右孩子
- 返回到B
- B有右孩子E,E是B的右子树的根结点,访问根结点E
- E有左孩子G,G是E的左子树的根结点,访问根结点G
- G没有左孩子
- G没有右孩子
- 返回到E
- E没有右孩子
- 返回到A
- A有右孩子C,C是A的右子树的根结点,访问根结点C
- C没有左孩子
- C有右孩子F,F是C的右子树的根结点,访问根结点F
- F有左孩子H,H是F的左子树的根结点,访问根结点H
- H没有左孩子
- H有右孩子I,I是H的右子树的根结点,访问根结点I
- I没有左子树
- I没有右子树
- 返回到F
- F没有右子树
- 遍历结束
所以与其说是在遍历结点,不如说是在遍历「根结点」,我们只是在递归地把「所有根结点」找出来并输出而已。(因为每个结点都可以看做是根结点)
(2) 中序遍历
基本步骤:
-
二叉树不为空时,
- 中序遍历左子树
- 访问根结点
- 中序遍历右子树
-
二叉树为空时,做空操作。
(3) 后序遍历
基本步骤:
-
二叉树不为空时,
- 后序遍历左子树
- 后序遍历右子树
- 访问根结点
-
二叉树为空时,做空操作。
3. 二叉树、树、森林
(1) 树的遍历
先根遍历:先访问树的根结点,然后依次先根遍历根的每颗子树
后根遍历:先依次后根遍历每颗子树,然后访问根结点
(2) 森林的遍历
先序遍历:
若森林非空,则:
- 访问森立中第一棵树的根结点
- 先序遍历第一棵树的「根结点的子树构成的森林」
- 先序遍历除去第一棵树之后「剩余的树构成的森林」
说白了就是,依次先根遍历森林中的每棵树。
中序遍历:
若森林非空,则:
- 中序遍历森林中第一棵树的「根结点的子树构成的森林」
- 访问第一棵树的根结点
- 中序遍历除去第一棵树之后「剩余的树构成的森林」
说白了就是,依次后根遍历森林中的每颗树。
森林的先序遍历和中序遍历即为其对应二叉树的先序和中序遍历。
(3) 二叉树、树、森林的转换
树和二叉树的转换:
给定一棵树,可以找到惟一的一颗二叉树与之对应。
转换方法:
- 按照「先根遍历的次序」来转化每个结点
- 如果该结点是根结点,则作为二叉树的根结点
- 如果该结点是「第一个孩子」,则作为「上一个结点的左孩子」
- 如果该结点「非第一个孩子」,则作为「上一个兄弟结点的右孩子」
说明:
- 孩子的次序通常自左向右排序,如A的孩子BCD的次序为第一个、第二个、第三个
- 「上一个结点」是指在先根遍历次序中的上一个结点。
- 「上一个兄弟结点」是指在原树中的左面的兄弟结点,如E、F、G为兄弟,F的上一个兄弟结点为E,G的兄弟结点为F。
观察树对应的二叉树,我们发现:
- 「在二叉树中,某个结点的左孩子」对应「在原树中,该结点的第一个孩子」。
- 「在二叉树中,某个结点的右孩子」对应「在原树中,该结点的兄弟节点」。
根据以上两个特点,我们可以将二叉树转化为树。
森林和二叉树的转换:
「森林和二叉树的转换」与「树和二叉树的转换」规则类似,我们只需将森林中的每棵树的根结点看做是兄弟结点,即将森林看做一棵树,然后按照树和二叉树的转换规则即可。
三、Huffman树
-
路径:从树中一个结点到另一个结点之间的分支构成这两个结点之间的路径
-
路径长度:路径上的分支数目。
如结点d的路径长度为2
-
树的路径长度:从树根到每一个结点的路径长度之和。
上图中树的路径长度为0+1+1+2+2+3+3=12
-
结点的带权路径长度:从该结点到树根之间的路径长度与结点上的权的乘积。
结点d的带权路径长度为2×4=8
-
树的带权路径长度:树中所有叶子结点的带权路径长度之和。
上图树的带权路径长度为2×4+3×7+3×5+1×2=46
1. 最优二叉树
树的带权路径长度最小的二叉树为最优二叉树或Huffman(赫夫曼)树。
上图中的二叉树并不是最优二叉树。那么我们如何构造最优二叉树?
- 给定一个集合,该集合中有n颗二叉树,每颗二叉树都只有一个带权的根结点,其左右子树为空
- 选取两颗根结点的权值最小的树作为左右子树构造一颗新的二叉树,新二叉树的根结点的权值为其左右子树上根结点的权值之和
- 在集合中删除这两棵树,同时将新构造的二叉树加入集合中
- 重复步骤2、3,直到集合中只含一棵树为止,该树便是赫夫曼树。
2. Huffman编码
如果将电文中的字符A、B、C、D分别编码为0、00、1、01,当发送方发送000011010时,接收方并不能准确翻译。因为0000可能翻译为AAAA或BB或AAB或BAA。
所以如果要设计长短不一的编码,必须满足任一个字符的编码都不是另一个字符的编码的前缀,这种编码叫做前缀编码。
我们可以利用二叉树来设计前缀编码:
4个叶子结点表示ABCD,约定左分支表示0,右分支表示1。从根节点到叶子结点的路径上分支组成该叶子结点字符的编码。A:0,B:10,C:110,D:1111。按这种方式得到的编码编译的电文并一定不是最短的。
如何得到使电文总长最短的二进制前缀编码呢?
将字符出现的频率作为权,设计一颗赫夫曼树,由该赫夫曼树得到的前缀编码编译的电文是最短的,这种编码称为赫夫曼编码。
如有错误,还请指正
文章首发于公众号『行人观学』