目录
基本术语
树的数据结构
普通树就写这些,太多内容暂时理解不了,这篇文章不涉及二叉树
在很多情况下,信息会具有家谱或组织图中那样的分层结构或嵌套结构。为分层结构建模的抽象被称为树,而且这种数据结构是计算机科学领域中最为基础的内容之一。它是包括Lisp在内的数种程序设计语言的底层模型。
基本术语
树是被称为节点的点与被称为边的线的集合。一条边连接着两个不同的节点,要形成树,这一系列的节点和边必须满足某些属性,
(1) 在树中,有一个节点是与众不同的,它被称为根。树的根通常画在其顶端。
(2) 除根之外的每个节点c都由一条边连接到某个称为c的父节点的节点p。我们也将节点c称为p的子节点。节点的父节点要画在该节点的上方。
(3) 如果从除根之外的任一节点n开始,移动到n的父节点,再到n的父节点的父节点,以此类推,最终到达树的根节点,就说树是连通的。
利用由较小树构成较大树的归纳定义,还可以递归地定义树。
依据。单个节点n就是一棵树,我们说n就是这棵单节点树的根。
归纳。设r是一个新节点,并设T1、T2、…、Tk分别是根为c1、c2、…、ck的树。这里要求任何节点在Ti中的出现次数都不会超过一次,而且r是个“新”节点,一定不会在这些树中出现。可以按照以下规则用r和T1、T2、…、Tk组成新的树T。
(a) 用r作为树T的根。
(b) 从r添加连接到c1、c2、…、ck的边,使得这些节点都成为根节点r的子节点。还可以将本步骤视为让r成为T1、T2、…、Tk些树的根节点的父节点。
这种父子关系可以自然而然地扩展为祖先和子孙的关系。粗略地讲,节点的祖先就是从节点到其父节点,再到其父节点的父节点,以此类推,顺着这样一条唯一路径找到的那些节点。严格地讲,节点本身也是其自身的祖先节点。而子孙关系则是祖先关系的反向关系,像父子关系这样就是互为反向关系。
假设m1、m2、…、mk是树中的一系列节点,其中m1是m2的父节点,m2是m3的父节点,以此类推,直到mk1是mk的父节点。那么m1、m2、…、mk就是该树中从m1到mk的一条路径。路径的长度为 k 1,比路径上的节点数小1。请注意,路径可能是由单个节点构成的,这种情况下路径的长度就为0。
如果该路径的长度不小于1,那么m1就是mk的真祖先,而mk则是m1的真子孙。还要记住,路径长度可能为0,在这种情况下,我们就可以得出m1是其自身的祖先也是其自身的子孙这一结论,虽然它不是自己的真祖先或真子孙。树的根节点是树中每个节点的祖先,而树中每个节点都是根节点的子孙。
具有相同父节点的节点有时也称为兄弟节点。
在树T中,某个节点n,与其所有真子孙(若存在的话),就构成了T的子树。而节点n就是该子树的根节点。请注意,子树要满足3个条件才能构成树:它有根节点;子树中其他节点在该子树中都有唯一的父节点;此外,沿着该子树中任意节点的父节点回溯,最终都能达到该子树的根节点。
树中没有子节点的节点就是叶子节点。内部节点是指至少有一个子节点的节点。因此,树中的每个节点要么是叶子节点,要么是内部节点,但不可能同时是这两者。树的根节点通常是内部节点,不过,如果该树只由一个节点组成,那么该节点就既是根节点也是叶子节点。
在树中,节点n的高度是指从n到叶子节点最长路径的长度,树的高度就是根节点的高度。而节点n的深度,或者说等级,就是从根节点到n的路径的长度。
另外,可以为任意节点的子节点指定从左到右的顺序。例如,n1的子节点最左边是n2,然后是n3,再然后是n4。这一从左到右的排列方式可以扩展到为树中的所有节点排列顺序。如果m和n是兄弟节点,而m在n的左边,那么m的子孙全都在n的子孙的左边。
在树中选择任意两个互不存在祖先关系的节点x和y。由于有着“在左边”的定义,x和y中其中有一个是在另一个的左边。要分辨哪个在哪个左边,就要从x和y开始沿着路径向根节点回溯。而在某一点,可能是根节点,也可能是较低的点,这两条路径会在某个如图5-2所示的节点z相遇。从x和y到z的路径分别要经过两个不同的节点m和n,可能有 m=x 且(或) n =y,但一定有 m≠ n,否则这两条路径在z以下的某个位置就融合了。
假设m在n的左边。那么因为x在根为m的子树中,而y在根为n的子树中,所以有x在y的左边。同样,如果m在n的右边,那么x也就在y的右边。
因为叶子节点不可能是其他叶子节点的子孙节点,所以所有叶子节点的顺序都遵循“从左起”的原则。
标号树(labelled tree)是指树中每个节点都有与之关联的标号或值的树。我们可将标号视为与给定节点相关联的信息。标号可以是很简单的内容,比如一个整数;也可以是复杂的内容,比如整个文档中的文本。我们可以改变节点的标号,但不能改变节点的名称。如果节点的名称不重要,就可以用节点的标号来表示它。不过,标号并不总是能为节点提供唯一名称,因为若干个节点可能有着同一标号。因此,很多时候在绘制节点时既会标上其标号,也会标上其名称。下面的一些图展示了标号树的概念,并提供了一些示例。
算术表达式可以用标号树表示,而且将表达式直观表示为树往往是非常有意义的。其实,表达式树顾名思义就是以统一的方式指定了表达式的操作数与操作符之间的关联,不论这种关联是表达式中括号的放置需要的,还是所涉及运算符的优先级和结合规则需要的。
通过对表达式递归定义的模拟,就可以递归地定义对应的标号树。大致的概念就是通过将运算符应用到较小表达式上以构成更大的表达式,我们会创建标号为该运算符的新节点。该新节点就成为了表示较大表达式的树的根节点,而它的子节点就是表示较小表达式的树的根节点。
例如,可以按照如下方式,定义表示使用二元运算符 、、×、/ 及一元运算符的算术表达式的标号树。
依据。单个原子操作数(比如一个变量、一个整数或一个实数,如2.6节介绍的)是表达式,它的树就是标号为该操作数的一个节点
归纳。如果E1和E2这两个表达式分别是由树T1和T2表示的,那么表达式(E1+E2)就是由图所示的树表示的,其根节点标号为+。该根节点有两个子节点,依次分别是树T1和T2的根节点。同样,表达式(E1-E2)、(E1×E2)和(E1/E2)分别有着根节点标号-、×和/,且子树均为T1和T2的表达式树。最后,可以将一元减号运算符应用到表达式E1上。这里引入了标号为-的根节点,而其唯一的子节点是T1的根节点,表示(-E1)的树如图所示
树的数据结构
很多数据结构可用来表示树。应该选用哪种数据结构,取决于想要执行的特定操作。举个简单的例子,如果想要做的只是定位节点的父节点,那么可以用由标号组成的结构体,加上指向表示节点之父节点的结构体的指针来表示每个节点。
作为一般规则,树的节点可以用结构体表示,这些结构体中的字段以某种方式将节点链接在一起,这种链接方式与节点在抽象的树中的连接方式类似,而树本身则可由指向表示根节点的结构体的指针来表示。因此,在谈到对树的表示时,我们主要是对节点的表示方式感兴趣。
表示方式的一种差异体现在表示节点的结构体在计算机内存中的位置上。在C语言中,可以使用标准库stdlib.h中的malloc函数为表示节点的结构体创建空间,这种情况下,节点都“漂泊”在内存中,并只能通过指针来访问。此外,可以创建由结构体组成的数组,并用数组中的元素来表示节点。这里节点还是根据它们在树中的位置链接起来的,不过也可以沿着数组的顺序来访问这些节点。因此,可以不沿着树中的路径来访问节点。基于数组的表示方式的弊端,在于没办法创建一棵节点数超过数组所含元素数量的树。接着,我们会假设这些节点都是由malloc创建的,虽然在树的大小有限制的情况下,由相同类型的结构体组成数组是可行的,而且有可能是首选方案。
表示树的最简单方式之一就是为每个节点使用一个由表示节点标号的字段组成的结构体,后面再跟上指向该节点子节点的指针组成的数组。下图就表示了这样的结构。常量bf是该指针数组的大小,它表示节点可以具有的最大子节点数,这个量就是分支系数(branching factor)。某节点对应数组的第i个位置含有指向该节点第i个子节点的指针,不存在的子节点可用NULL指针表示。
typedef struct NODE* pNODE;
struct NODE
{
int info;
pNODE children[BF];
};
在这里,字段info表示构成节点标号的信息,而BF则表示分支系数的常量。在本章中我们还将看到该声明的多个变种。
在这种表示树的数据结构和多数表示树的数据结构中,都将树表示为指向根节点的指针。因此,pNODE还是树的类型。其实,可以在pNODE的位置使用类型TREE。不过,现在还是要使用pNODE这个名称代表“指向节点的指针”类型,因为在某些数据结构中,指向节点的指针除了表示树之外还用于其他用途。
指针数组表示使我们能在O(1) 的时间内访问任意节点的第i个子节点。然而,当树中只有少量节点有很多子节点时,这种表示会非常浪费空间。在这种情况下,数组中的多数指针都是NULL。
树可以用来表示一系列单词,其表示方式可以使检查给定字符序列是否为存在的单词变得非常有效率。在这类称为单词查找树的树中,除了根节点之外,每个节点都有与之相关联的字母。由某个节点n表示的字符串,就是从根节点到n的路径上的字母序列。给定一组单词,单词查找树的节点就是那些表示该集合中某个单词的前缀的字符串。节点的标号是由表示该节点的字母,以及表明从根节点到该节点的字母串能否构成完整单词的布尔值组成的。如果能,就用布尔值1表示,如果不能,就用0表示。
单词查找树中众节点的分支系数就等于构成这些单词的字母表中不同字符的数目。例如,如果不区分大小写字母,而且单词中不含撇号这样的特殊字符,那么分支系数就等于26。包含两个标号字段的节点的类型可以按照如下所示的方式定义。在数组children中,可以假设字母a是用下标0表示的,而下标1表示字母b,以此类推。
typedef struct NODE* pNODE;
struct NODE
{
char letter;
int isWord;
pNODE children[BF];
};
使用指针数组表示节点的空间利用率可能很低,因为通常情况下,绝大多数指针都会是NULL。事实上,如果想想这种情况,就会发现,在任何基于26个字母的字母表的单词查找树中,指针的数量都会是表示节点的指针的数量的26倍。因为没有哪个节点会有两个父节点,而且根节点是没有父节点的,所以N个节点中只有N-1个非NULL的指针,也就是说,每26个指针中只有不到1个是有用的。
要克服树的指针数组表示空间利用率低的问题,方法之一就是使用链表来表示节点的子节点。节点对应链表所占据的空间是与该节点子节点的数量成正比的。不过,这种表示方式在时间上要付出代价,访问第i个子节点所需时间为O ( i) ,因为在到达第i个节点之前必须遍历长度为i -1的链表。与之相比,使用指针数组表示子节点的话,就可以在O(1) 时间内到达第i个子节点,跟i完全没有关系。
在树的这种最左子节点右兄弟节点(leftmost-child-right-sibling)表示中,要为每个节点放入一个指向其最左子节点的指针,而节点没有指向它其他子节点的指针。要找到节点n的第二个及后续的子节点,可以为这些节点创建一个链表,其中每个子节点c都指向n的子节点中紧挨在c右侧的那个,该节点称为c的右兄弟节点。
在树的最左子节点右兄弟节点表示中,节点是按照如下方式定义的。
typedef struct NODE* pNODE;
struct NODE
{
int info;
pNODE leftmostChild,rightSibling;
};
info字段存放着与节点相关联的标号,而且可以是任一类型的。字段leftmostChild和rightSibling指向最左子节点以及相应的右兄弟节点。请注意,尽管leftmostChild给出了有关该节点自身的信息,但节点的rightSibling字段才是真正表示该节点父节点全部子节点的链表的那一部分。
有些时候,在表示节点结构体中包含指向其父节点的指针是很有用的,而根节点的父指针为NULL。
typedef struct NODE* pNODE;
struct NODE
{
int info;
pNODE leftmostChild,rightSibling,parent;
};