与队列和堆栈一样,树也是人为构造的一种数据存储逻辑。
首先我们来看一下树的课本定义。
树(Tree),是元素的集合。
假设我们有这样一组数据,{ 6,3,5,1,8,7,9 },我们使用树的形式来存放他们,得到了这样一棵树。
(在一棵有实际意义的树中,节点之间其实是有一定关联的。我们在这里仅仅将数据随意存放,来展示一下树的概念。)
图-一棵仅用作示例,没有什么实际意义和用途的树
树有多个节点(node),用来存储元素。图中的每个数字所在的圆形就是一个节点。
每个节点之间用一根线相连,这些线,称为边(edge)。在一条边连接的两个节点中,靠近上层的节点称为父节点(parent),处在下层的节点称为子节点(children)。而处在最顶层,没有父节点的节点被称为根节点(root);没有子节点的节点称为叶节点(leaf)。同一个父节点下的同一层级的子节点相互称为兄弟节点(sibling)。很显然,这个图中的“6”就是根节点,也是“3”“5”的父节点。“5”是一个叶节点,也是“6”的子节点。“3”是“6”的子节点,但同时也是“1”“8”“7”的父节点。“1”“8”都是“3”的子节点,同时他们也是叶节点。“7”是“3”的子节点,是“9”的父节点。“9”是“7”的子节点,也是一个叶节点。“1”“8”“7”互为兄弟节点,“3”“5”互为兄弟节点。
树的层次被称为深度(depth)。可以看出上图中,“6”位于第一层,“3”和“5”位于第二层,“1”“8”“7”位于第三层,“9”位于第四层。这一棵树的深度是4。
然后我们再看一下树的课本定义:
1. 树是元素的集合。
2. 该集合可以为空。这时树中没有元素,我们称树为空树 (empty tree)。
3. 如果该集合不为空,那么该集合有一个根节点,以及0个或者多个子树。根节点与它的子树的根节点用一个边(edge)相连。
*这里的第三点使用了递归的思想来定义一棵树。
树的实现
以C语言为例,当我们要实现一棵树的构造时,我们往往需要构造一种结构体来作为节点。但是一个父节点下的子节点个数是不同的,可能它有很多个子节点,也可能它只有一个子节点。如果我们在结构体中,定义了多个子节点指针的变量,会造成内存的浪费。于是我们有了以下这种经典的实现方式(依然以上文那棵只有展示意义的树为例):
图-树的一种经典内存实现方式
在这种实现方式中,一个节点有两个指针,一个指向它的第一个子节点,一个指向它的下一个兄弟节点。这样,我们依然可以从根节点开始,遍历整棵树。
当然在实际操作中,为了方便,我们也可以让节点中增加一个指针去指向他的父节点。
树的应用
在了解了树的定义和构造之后,我们可以看看树的实际应用了。
表达式树
一个表达式,或者一个算式,都可以存成树的形式:
实际上你也可以感觉得出来,非常多的操作系统中,文件夹的目录是很类似树的结构的。
我们以某本书上的一个UNIX文件系统中的一个目录作为例子。
图-一个UNIX文件系统的目录
我们可以看到,最原始的根节点是 /usr 。
如何打印出这个目录下所有的文件和路径呢?我们需要对这棵树进行遍历。
树的遍历
先序遍历:从根节点开始,对每一棵树进行遍历,每处理一棵树/子树的时候,先处理这棵树的根节点。
按照先序遍历,以一定格式可以把上图的目录打印出来:
图-先序遍历打印出的目录
后序遍历:从根节点开始,处理完以这个节点为根节点的树/子树的所有子节点之后,再处理根节点。
按照后序遍历,把上图的目录以一定格式再次打印出来:
图-后序遍历打印出的目录