数据结构 ☞ 树

术语及性质

尽管树不是线性结构,但是得益于其层次化的特点,我们还是可以说“树的某一元素是另一元 素‘直接上邻’”,也可以说“某一元素是另一元素的‘直接下邻’之一”。这些关系可以形象地用“父亲”、 “孩子”、“祖先”和“后代”等术语来描述,下面我们首先对此做一介绍。

树ADT

树抽象类型要支持以下的基本方法:

tree ADT

不同的应用问题会要求树结构提供不同方法。这方面的差异太大,我 们无法在树 ADT 中定义出通用的更新操作。在后续章节的讨论中,我们将结合各种应用问题,陆续 给出一些具体的更新操作的实现。

树的Java接口

原始代码所在目录请点击这里arrow-up-right

基于链表实现树

树的遍历

前序后序遍历

所谓树的遍历(Traversal),就是按照某种次序访问树中的节点,且每个节点恰好访问一次。 也就是说,按照被访问的次序,可以得到由树中所有节点排成的一个序列。

我们将分别介绍两种最基本的树遍历算法⎯⎯前序遍历(Preoder trave rsal)和后 序遍历(Postorder traver sal)。这两种遍历算法都是递归定义的,只是其中对“次序”的定义略有不同。

对任一(子)树的前序遍历,将首先访问其根节点,然后再递归地对其下的各棵子树进行前序 遍历。对于同一根节点下的各棵子树,遍历的次序通常是任意的;但若换成有序树,则可以按照兄 弟间相应的次序对它们实施遍历。由前序遍历生成的节点序列,称作前序遍历序列。

  • 前序遍历的过程可以描述为

tree PreorderTraversal

对称地,对任一(子)树的后序遍历将首先递归地对根节点下的各棵子树进行后序遍历,最后 才访问根节点。由后序遍历生成的节点序列,称作后序遍历序列。

  • 后序遍历过程可以描述为

tree PostorderTraversal

层次遍历

除了上述两种最常见的遍历算法,还有其它一些遍历算法,层次遍历(Traversal by level )算 法就是其中的一种。在这种遍历中,各节点被访问的次序取决于它们各自的深度,其策略可以总结 为“深度小的节点优先访问”。对于同一深度的节点,访问的次序可以是随机的,通常取决于它们的 存储次序,即首先访问由firstChild指定的长子,然后根据nextSibling确定后续节点的次序。当然, 若是有序树(定义四.13,第 106 页),则同深度节点的访问次序将与有序树确定的次序一致。

层次遍历过程可以借助一个队列来实现:

tree LevelorderTraversal

树迭代器

二叉树抽象数据类型及其实现

在算法领域,二叉树的重要地位是其它结构无法替代的。每个节点均不超过 2 度的有序树,称作二叉树(Binary tree), 因此每个节点的孩子(如果存在的话)可以左、右区分,分别称作左孩子和右孩子⎯⎯之所以如此命名, 是因为在画出二叉树时,通常都将左、右孩子分别画在其父节点的左下方、右下方。

在几乎所有的应用问题中,都可以看到二叉树的身影。而且令我们感到惊讶的是,尽管二叉树 只是树的一种特例,但无论是从对应用问题的描述与刻画能力,还是从计算能力的角度来看,二叉 树并不逊色于一般意义上的树。反过来,正是得益于其定义的简洁性以及结构的规范性,基于二叉 树的算法往往可以更好地得到描述,其实现也更加简捷。

二叉树ADT

二叉树抽象类型需要支持以下的基本方法:

二叉树ADT

与普通的树一样,不同的应用问题可能会在上述操作之外要求二叉树结构提供更多的方法。这 些操作多属更新类操作,而且具体功能差异极大。在此,我们只能在二叉树 ADT 中定义相对通用的 那些更新操作。在后续的讨论中,我们将结合各种具体的应用问题,相应地定义并实现一些其它的 更新操作。

二叉树类的Java接口

BinTreePosition接口

除了继承Position接口的getElem()和setElem()方法外,这里还针对二叉树节点的操作要求,定 义了一系列的方法。其中有些是辅助性的,旨在提高代码的简洁性和可读性。其中还定义了诸如 secede()、attachL()和attachR()等方法,这是为后面实现查找树的相关操作而埋下的伏 笔。另外,这里还定义了对当前节点所有后代的若干遍历方法,与普通树的遍历方法 一样,它们也符合迭代器规范,可以生成相应的节点遍历序列。当然,鉴于二叉树的特点,这里增 加了一种遍历算法⎯⎯中序遍历。

二叉树类的实现

基于链表实现二叉树

二叉树的基本算法

  • getSize()、getHeight()和 getDepth()

这三个方法的功能是,分别返回二叉子树的规模、树根节点的高度和深度。 这里为每个节点设置了三个变量size、height和depth,分别对应于这三个指标,这样,只需在 O(1)时间内返回相应的变量,即可实现相应的功能。

  • updateSize()

若当前节点的孩子发生变化,比如原有的某个孩子被删除或者有新的孩子插入,就需要更新当 前节点及其祖先的规模记录,以便后续的查询,updateSize()方法的功能正在于此。请注意,在这里, 我们允许直接对任何节点执行这一操作。

updateSize

若节点 v 的左、右孩子分别为 lc 和 rc,则 size(v) = 1 + size(lc) + size(rc)

因此,一旦其左、右子树的规模都已确定,我们就可以在O(1)时间内得到以节点v为根的子树规 模。当然,此后还需要从v出发沿parent引用逆行向上,依次更新各个祖先的规模记录。这可以描述为

若节点 v 的深度为 depth(v),则总共需要修改 depth(v)+1 个节点的规模。为了更新一个节点的 规模记录,只需执行两次 getSize()操作并做两次加法,故 updateSize()算法的总体运行时间为O(depth(v)+1)

二叉树遍历与查找方式演示

Last updated

Was this helpful?