1. 学习目标
你好,我是悦创。
- 理解树这种数据结构及其用法。
- 了解如何用树实现映射。
- 用列表实现树。
- 用类和引用实现树。
- 将树实现为递归数据结构。
- 用堆实现优先级队列。
2. 示例
我们已经学习了栈和队列等线性数据结构,并对递归有了一定的了解,现在来学习树这种常见的数据结构。树广泛应用于计算机科学的多个领域,从操作系统、图形学、数据库到计算机网络。
作为数据结构的树和现实世界中的树有很多共同之处,二者皆有根、枝、叶。不同之处在于,前者的根在顶部,而叶在底部。
在研究树之前,先来看一些例子。
第一个例子是生物学中的分类树。从生物分类学的角度给出了某些动物的类别,从这个简单的例子中,我们可以了解树的一些属性。
2.1 层次性
第一个属性是层次性,即树是按层级构建的,越笼统就越靠近顶部,越具体则越靠近底部。
在图中,顶层是界,下一层(上一层的“子节点”)是门,然后是纲,依此类推。但不管这棵分类树往下长多深,所有的节点都仍然表示动物。
可以从树的顶部开始,沿着由椭圆和箭头构成的路径,一直到底部。
在树的每一层,我们都可以提出一个问题,然后根据答案选择路径。
比如,我们可以问:
- “这个动物是属于脊索动物门还是节肢动物门?”如果答案是“脊索动物门”,就选脊索动物门那条路径;
- 再问:“是哺乳纲吗?”如果不是,就被卡住了(当然仅限于在这个简单的例子中)。
- 继续发问:“这个哺乳动物是属于灵长目还是食肉目?”
就这样,我们可以沿着路径直达树的底部,找到常见的动物名。
2.2 无关性
树的第二个属性是,一个节点的所有子节点都与另一个节点的所有子节点无关。
比如,猫属的子节点有家猫(英文名为 Domestica)和狮。家蝇属的子节点是家蝇,其英文名也是Domestica,但此 Domestica 非彼 Domestica。这意味着可以变更家蝇属的这个子节点,而不会影响猫属的子节点。
2.3 独一无二
第三个属性是,叶子节点都是独一无二的。
在本例中,每一个物种都对应唯一的一条从树根到树叶的路径,比如:动物界→脊索动物门→哺乳纲→食肉目→猫科→猫属→家猫。
另一个常见的树状结构是文件系统。在文件系统中,目录或文件夹呈树状结构。下图展示了 Unix 文件系统的一小部分。
Windows 其实也有,就是查看不方便:
clela@AIYC D:\Github_pages\QuicksandTeam.github.io\docs# tree文件夹 PATH 列表卷序列号为 C0000100 0BF4:1B94D:.├─.vuepress│ ├─dist│ │ ├─AboutTeam│ │ ├─article│ │ ├─assets│ │ │ ├─css│ │ │ ├─fonts│ │ │ ├─icon│ │ │ ├─img│ │ │ └─js│ │ ├─category│ │ │ └─Markdown 基础│ │ ├─ColumnImages│ │ │ ├─jysfimg│ │ │ └─MarkdownBase│ │ │ └─01│ │ ├─en│ │ │ └─home│ │ ├─encrypt│ │ ├─images│ │ │ └─公众号│ │ ├─slide│ │ ├─star│ │ ├─tag│ │ │ └─Markdown│ │ ├─timeline│ │ ├─WebSiteData│ │ └─zh│ │ ├─column│ │ │ ├─aiycsf│ │ │ ├─jysf│ │ │ └─md│ │ │ ├─EightMarkdownform│ │ │ ├─FiveMarkdowncode│ │ │ ├─fourmarkdownblock│ │ │ ├─nineMarkdownskills│ │ │ ├─oneMarkdowntitle│ │ │ ├─SevenMarkdownpictures│ │ │ ├─SixMarkdownlink│ │ │ ├─threeMarkdown_list│ │ │ └─twoMarkdown_paragraph│ │ ├─guide│ │ ├─home│ │ └─slides│ ├─public│ │ ├─assets│ │ │ └─icon│ │ ├─ColumnImages│ │ │ ├─aiycsf│ │ │ ├─jysfimg│ │ │ └─MarkdownBase│ │ │ └─01│ │ └─images│ │ └─公众号│ └─styles├─en└─zh├─column│ ├─aiycsf│ ├─jysf│ └─md└─guide
文件系统树与生物分类树有很多共同点。在文件系统树中,可以沿着一条路径从根直达任何目录。这条路径能唯一标识子目录以及其中的所有文件。树的层次性衍生出另一个重要属性,即

可以将树的某个部分(称作子树)整体移到另一个位置,而不影响下面的层。比如,可以将从etc/ 起的全部子树挪到 usr/ 下。
这会将到达 httpd/ 的路径从 /etc/httpd/ 变成 /usr/etc/httpd/,但不会影响 httpd 目录下的内容或子节点。
关于树的最后一个例子是网页。以下是一个简单网页的HTML代码。下图展示了该网页用到的 HTML 标签所对应的树。
<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en"><head><meta http-equiv="Content-Type"content="text/html; charset=utf-8"/><title>simple</title></head><body><h1>A simple web page</h1><ul><li>List item one</li><li>List item two</li></ul><h2><a href="http://www.aiyc.top">CS</a></h2></body></html>

HTML 源代码与对应的树展示了另一种层级关系。树的每一层对应 HTML 标签的每一层嵌套。在源代码中,第一个标签是,最后一个是。其余的标签都在这一对标签之内。检查一遍会发现,树的每一层都具备这种嵌套属性。
3. 术语及定义
3.1 节点
节点是树的基础部分。它可以有自己的名字,我们称作“键”。
节点也可以带有附加信息,我们称作“有效载荷”。有效载荷信息对于很多树算法来说不是重点,但它常常在使用树的应用中很重要。
3.2 边

边是树的另一个基础部分。两个节点通过一条边相连,表示它们之间存在关系。除了根节点
以外,其他每个节点都仅有一条入边,出边则可能有多条。
3.3 根节点

根节点是树中唯一没有入边的节点。在上图中,/ 就是根节点。
3.4 路径
路径:是由边连接的有序节点列表。比如,哺乳纲→食肉目→猫科→猫属→家猫就是一条路径。
3.5 子节点

一个节点通过出边与子节点相连。在上图中,log/、spool/ 和 yp/ 都是 var/ 的子节点。
3.6 父节点

一个节点是其所有子节点的父节点。在上图中,var/ 是 log/、spool/ 和 yp/ 的父节点。
3.7 兄弟节点

具有同一父节点的节点互称为兄弟节点。文件系统树中的 etc/ 和 usr/ 就是兄弟节点。
3.8 子树
一个父节点及其所有后代的节点和边构成一棵子树。
3.9 叶子节点
叶子节点没有子节点。比如,上图中的人和黑猩猩都是叶子节点。
3.10 层数

节点 n 的层数是从根节点到 n 的唯一路径长度。在上图中,猫属的层数是 5。由定义可知,根节点的层数是 0。
3.11 高度

树的高度是:其中节点层数的最大值。上图中的树高度为 2。
定义基本术语后,就可以进一步给出树的正式定义。实际上,悦创将提供两种定义:
- 其中一种涉及节点和边;
- 另一种涉及递归。你在后面会看到,递归定义很有用。
4. 定义一:树由节点及连接节点的边构成。
树有以下属性:
- 有一个根节点;
- 除根节点外,其他每个节点都与其唯一的父节点相连;
- 从根节点到其他每个节点都有且仅有一条路径;
- 如果每个节点最多有两个子节点,我们就称这样的树为二叉树。
5. 定义二:一棵树要么为空,要么由一个根节点和零棵或多棵子树构成,子树本身也是一棵树。
每棵子树的根节点通过一条边连到父树的根节点。下图展示了树的递归定义。
从树的递归定义可知,图中的树至少有 4 个节点,因为三角形代表的子树必定有一个根节点。这棵树或许有更多的节点,但必须更深入地查看子树后才能确定。
6. 实现
根据6.3节给出的定义,可以使用以下函数创建并操作二叉树。
BinaryTree():创建一个二叉树实例。getLeftChild():返回当前节点的左子节点所对应的二叉树。getRightChild():返回当前节点的右子节点所对应的二叉树。setRootVal(val):在当前节点中存储参数 val 中的对象。getRootVal():返回当前节点存储的对象。insertLeft(val):新建一棵二叉树,并将其作为当前节点的左子节点。insertRight(val):新建一棵二叉树,并将其作为当前节点的右子节点。
实现树的关键在于选择一个好的内部存储技巧。Python 提供两种有意思的方式,我们在选择前会仔细了解这两种方式。第一种称作“列表之列表”,第二种称作“节点与引用”。

注意,可以通过标准的列表切片操作访问子树。
- 树的根节点是:
myTree[0]; - 左子树是:
myTree[1]; - 右子树是:
myTree[2]
以下会话展示了如何使用列表创建树。一旦创建完成,就可以访问它的根节点、左子树和右子树。
“列表之列表”表示法有个很好的性质,那就是表示子树的列表结构符合树的定义,这样的结构是递归的!
由一个根节点和两个空列表构成的子树是一个叶子节点。还有一个很好的性质,那就是这种表示法可以推广到有很多子树的情况。如果树不是二叉树,则多一棵子树只是多一个列表。
myTree = ['a',
['b', ['d', [], []], ['e', [], []]],
['c', ['f', [], []], []]
]
print(myTree)
In [4]: myTree = ['a',
...: ['b', ['d', [], []], ['e', [], []]],
...: ['c', ['f', [], []], []]
...: ]
In [5]: myTree
Out[5]: ['a', ['b', ['d', [], []], ['e', [], []]], ['c', ['f', [], []], []]]
In [6]: myTree[0]
Out[6]: 'a'
In [7]: myTree[1]
Out[7]: ['b', ['d', [], []], ['e', [], []]]
In [8]: myTree[2]
Out[8]: ['c', ['f', [], []], []]
