数据结构 -- 树

来源:互联网 发布:coreldraw x6软件下载 编辑:程序博客网 时间:2024/05/21 01:42

数据结构 -- 树

树ADT

对于大量的输入数据,链表的线性访问时间太慢,不宜使用。我们介绍一种简单的数据结构,其大部分操作的运行时间平均为O(log N)。涉及到这种数据结构叫做二叉查找树,在计算机科学中树(tree)是非常有用的抽象概念。

预备知识

树可以用几种方式定义。一种定义树的自然的方式是递归的方法。一棵树是一些结点的集合。这个集合可以是空集;若非空,则一棵树由称作根(root)的节点r,以及0个或多个非空的子树T1,T2,T3,...,Tk组成,这些子树中每一棵的根都被来自根r的一条有向边(edge)所连接。

每一棵子树的根叫做根r的儿子(child),而r是每一棵子树的根(root)的根的父亲(parent)。一棵树是N个节点和N-1条边的集合,其中的一个节点叫做根。每条边都将某个节点连接到它的父亲,而除去根节点外每一个节点都有一个父亲。每一个节点都可以有任意个儿子,也可能有零个儿子。没有儿子的结点叫做树叶(leaf);具有相同父亲的结点叫做兄弟(sibling)。用类似方法可以定义祖父(grandparent)和孙子(grandchild)关系。

从节点n1到nk的路径(path)定义为结点n1,n2,n3,...,nk的一个序列,使得对于1<=i<=k,节点n(i)是n(i+1)的父亲。这条路径的长(length)为该路径上的边的条数,即k-1。对任意节点n(i)的深度为从根到n(j)的惟一路径的长。因此,根的深度为0。n(i)的高是从n(i)到一片树叶的最长路径的长,因此所有树叶的高为0。一棵树的高等于它的根的高,一棵树的深度等于它的最深的树叶的深度,该深度总是等于这棵树的高。

树的实现

实现树的一种方法可是实在每一个节点出数据外还要有一些指针,使得该节点的每一个儿子都有一个指针指向它。将每个节点的所有儿子都放在树节点的链表中。

树的节点声明

typedef struct TreeNode *PtrToNode;struct TreeNode{ElementType Element;PtrToNode FirstChild;ptrToNode NextSibling;};

树的遍历和声明


树有许多应用。流行的用法之一是包含UNIX, VAX/VMS和DOS在内的许多常用操纵系统的目录结构。例如,UNIX目录的根是/usr,/usr有三个儿子:mark、alex和bill,它们自己也是目录。这种分级文件系统非常流行,因为他能够使得用户逻辑地组织数据。在不同目录下的两个文件还可以享有相同的名字,因为它们必然有从根开始的不同的路径从而具有不同的路径名。
我们想要列出目录中所有文件的名字,我们的输出格式为:深度为d(i)的文件的名字将被d(i)次跳格(tab)缩进后打印出来。
算法如下:(列出分级文件系统中目录的例程)

static void ListDir(DirectoryOrFile D, int Depth){if(D is a legitimate entry){PrintName(D, Depth);if(D is a directory)for each child, C, of DListDir(C, Depth + 1);}}void ListDirectory(DirectoryOfFile D){ListDir(D, 0);}

算法的核心为递归过程ListDir,为了显示根时不在进行缩进,该例程需要从目录名和深度为0开始。这种遍历的的策略叫做先序遍历,在先序遍历中,对节点的处理工作是在它的诸儿子的节点被处理之前(pre)进行的。还有一种遍历树的方法是后序遍历。在后序遍历中,在一个节点处的工作实在它的诸儿子节点被计算后(post)进行的。

二叉树

概念

二叉树的每个节点都不能有多于两个的儿子。二叉树的一个性质是平均二叉树的深度要比N小得多,这个平均深度为O(根号N)。而二叉查找树,其深度的平均值是O(log N).不幸的是,这个深度可以大到N - 1的。

实现

因为一棵二叉树最多有两个儿子,所以我们可以用指针直接指向它们。树节点的声明在结构上类似于双链表的声明。一个节点可以在调用free删除后被释放。
二叉树的节点声明:

typedef struct TreeNode *PtrToNode;typedef struct PtrToNode Tree;struct TreeNode{ElementType Element;Tree Left;Tree Right;};

表达式树

表达式树的树叶是操作数,比如常数或变量,而其他的节点为操作符。由于这里所有的操作都是二元的,因此这棵树正好是二叉树。我们可以通过递归产生一个带括号的左表达式,然后打印出在根处的运算符,最后再递归地产生一个带括号的右表达式而得到一个(对两个括号整体进行运算的)中缀表达式,这种一般的方法(左,节点,右)称为中序遍历。

另一种遍历策略是递归打印出左子树,右子树,然后再打印运算符。比如: a b c * + d e * f + g * +   ,这就是后缀表达式,这种遍历叫做后序遍历。
第三种遍历策略是先打印出运算符,然后递归地打印出左子树和右子树,其结果为: + + a * b c * + * d e f g   ,是不太常用的前缀记法,这种便利策略为先序遍历。