560 likes | 735 Views
第 6 章 树和二叉树. 【 本章教学目的、要求 】 1 、理解树的定义和基本操作; 2 、理解二叉树和哈夫曼树的定义及其特点; 3 、理解常见实例的编程思想。. 主要内容. 6.0 前言 6.1 树的定义和基本操作 6.2 二叉树 6.3 哈夫曼树和判定树 6.4 应用举例及分析 习题. 6.0 前言. 树形结构中结点之间有分支关系,又具有层次关系,它非常类似于自然界中的树。树形结构在现实世界中广泛存在: 例如家谱、各单位的行政组织机构等都可用树来表示。 树在计算机领域中也有着广泛的应用:
E N D
第6章 树和二叉树 • 【本章教学目的、要求】 • 1、理解树的定义和基本操作; • 2、理解二叉树和哈夫曼树的定义及其特点; • 3、理解常见实例的编程思想。
主要内容 • 6.0 前言 • 6.1 树的定义和基本操作 • 6.2 二叉树 • 6.3 哈夫曼树和判定树 • 6.4 应用举例及分析 • 习题
6.0 前言 • 树形结构中结点之间有分支关系,又具有层次关系,它非常类似于自然界中的树。树形结构在现实世界中广泛存在: • 例如家谱、各单位的行政组织机构等都可用树来表示。 • 树在计算机领域中也有着广泛的应用: • DOS和Windows操作系统中对磁盘文件的管理就采用树形目录结构。 • 在数据库中,树结构也是数据的重要组织形式之一。本章重点讨论二叉树的存储结构及其各种操作。 Return
6.1 树的定义和基本操作 • 6.1.1 树的定义 • 6.1.2 基本术语 • 6.1.3 树的基本操作 Return
6.1.1 树的定义(1) • 树(tree)是n (n ≥ 0)个结点的有限集合。当n = 0时,集合为空集,称为空树。在任意一棵非空树T中: • ① 有且仅有一个特定的,称为根(root)的结点。 • ② 当n > 1时,除根结点以外的其余结点可分成m (m > 0)个互不相交的有限集合T1,T2,…,Tm。其中每一个集合本身又是一棵树,并称其为根的子树(subtree)。 • 例如,图6.1 (a)是只有一个根结点的树,(b)是有13个结点的树,其中A是根结点,其余结点分成三个互不相交的子集: T1 = {B, E, F, K, L},T2 = {C, G},T3 = {D, H, I, J, M}。T1, T2, T3都是根的子树,它们本身也是一棵树。
6.1.1 树的定义(2) • 树的定义具有递归性,递归定义描述了树的递归特性: • 即一棵树是由根及若干棵子树构成的,而子树又可由更小的子树构成。 • 在树中,只有根结点没有直接前驱,而根结点以外的其余结点均有一个且只有一个直接前驱。 • 基于树的这一特点,我们在画树的示意图时一般都将根结点画在最上面,而结点之间的分支箭头就都省略了,前面图6.1(b)中的树通常画成图6.1 (c)。 Return
图6.1 树的示例 Return
6.1.2 基本术语(1) • 树的结点:包括一个数据元素及若干指向其子树的分支。 • 结点的度(degree):结点拥有的子树个数称为。 • 树中所有结点的度的最大值为该树的度。 • 度为0的结点称为叶子结点(leaf)或终端结点。 • 度不为0的结点称为非叶子结点或非终端结点。 • 孩子结点(child):结点的子树的根;该结点是其孩子结点的双亲结点(parent)。 • 具有同一双亲结点的孩子结点之间互称为兄弟(sibling)。 • 某结点的祖先是指从根结点到该结点所经分支上的所有结点。
6.1.2 基本术语(2) • 以某结点为根的子树中的所有结点都称为该结点的子孙。 • 结点的层次(level)从根开始算起,根为第一层,根的孩子为第二层,某结点所在的层从根开始向下计算。 • 在树的同一层上而双亲结点不同的结点互为堂兄弟。 • 树中结点的最大层次称为树的深度(depth)或高度。
6.1.2 基本术语(3) • 例如,图6.1(c)中,A是根结点,A的度为3,E的度为2,L的度为0。结点K,L,F,G,M,I,J都是叶子结点,其他都是非叶子结点。A有三棵子树,这三棵子树的根B,C,D是A的孩子结点,A是B,C,D的双亲结点。H,I,J的双亲是D,所以H,I,J三个结点互为兄弟结点。H和G都在树的同一层上,但它们的双亲不是同一个结点,所以它们是堂兄弟关系。树的最大层次共四层,因此该树的深度为4。
6.1.2 基本术语(4) • 如果将树中结点的各个子树看成从左至右是有序的(即不能互换),则称该树为有序树,否则称为无序树。 • 当用树来描述家谱时,应将树看成是有序树,有序树中某结点最左边子树的根称为该结点的第一个孩子,最右边子树的根称为最后一个孩子。当用树来描述某单位的行政组织结构时,可将树看成是无序树。 • 森林(forest)是m(m≥0) 棵互不相交的树的集合,树和森林的概念很密切,删去一棵树的根,就得到一个森林。图6.3就是一个森林的例图。 Return
图6.3 森林示例 Return
6.1.3 树的基本操作 (1) TREEEMPTY(T)判树空。若T为空树,返回TRUE,否则返回FALSE。 (2) ROOT(T) 求根。返回树T的根结点地址。若T为空树,返回一特殊值。 (3) TREEDEPTH(T) 求树的深度。返回树T的深度。 (4)VALUE(T, e) 求结点。e是树T中的结点,函数返回e结点地址。 (5) PARENT(T, e) 求结点的双亲。e是树T中的结点,当e是非根结点时,函数返回e结点的双亲结点地址。 (6)CHILD(T, e, i) 求结点的孩子。e是树T中的结点,函数返回e结点的第i个孩子结点的地址。 (7) CREATE_TREE(T, T1, T2, …, Tk)建树操作。当k≥1,建立一棵以T为根,以T1, T2, …, Tk为第1, 2, …, k棵子树的树。 Return
6.2 二叉树 • 二叉树是树的一个重要类型,许多实际问题抽象出来的数据结构往往是二叉树的形式,一般的树也可转换为二叉树,而且二叉树的存储结构及其操作都较为简单,因此二叉树显得特别重要。 • 6.2.1 二叉树的定义和基本操作 • 6.2.2 二叉树的性质 • 6.2.3 二叉树的存储结构 • 6.2.4 遍历二叉树 Return
6.2.1 二叉树的定义和基本操作(1) • 二叉树(binary tree)是n (n ≥ 0) 个结点的有限集合,n = 0时为空集,称为空二叉树;n≠0时,二叉树由一个根结点及两棵互不相交、分别称为左子树和右子树的二叉树组成。 • 二叉树和树在概念上相同的是都有一个且仅有一个根结点,根结点无前驱结点,叶子结点无后继结点。 • 不相同的是二叉树中每个结点的度小于等于2,而且二叉树的子树有左右子树之分。 • 二叉树的定义也是一个递归定义,表明二叉树或为空,或是由一个根结点加上两棵分别称为左子树和右子树的二叉树组成。由此,二叉树可以有五种基本形态,如图6.4所示。
6.2.1 二叉树的定义和基本操作(2) • 二叉树的基本操作和树的基本操作大致相同,主要的基本操作如下: (1) TREEEMPTY(BT)判二叉树空。若BT为空树,返回TRUE,否则,返回FALSE。 (2) ROOT(BT) 求根。返回二叉树BT的根结点地址。若BT为空树,返回一特殊值。 (3) TREEDEPTH(BT) 求二叉树的深度。返回二叉树BT的深度。 (4)VALUE(BT, e) 求结点。e是二叉树BT中的结点,函数返回e结点地址。 (5) PARENT(BT, e) 求结点的双亲。e是二叉树BT中的结点,当e是非根结点时,函数返回e结点的双亲结点地址。
6.2.1 二叉树的定义和基本操作(3) (6) LCHILD(BT, e) 求结点的左孩子。e是二叉树BT中的结点,函数返回e结点的左孩子结点的地址。 (7)RCHILD(BT, e) 求结点的右孩子。e是二叉树BT中的结点,函数返回e结点的右孩子结点的地址。 (8)CREATE_BT(BT, LBT, RBT)建二叉树操作。建立一棵以BT为根,以二叉树LBT、RBT分别为BT的左、右子树的二叉树。 Return
图6.4 二叉树的五种基本形态 Return
6.2.2 二叉树的性质(1) • 性质1 二叉树第i层上的结点数目至多为2i-1(i≥1)。 • 例如,一棵二叉树第4层上的结点数至多是24-1 = 23 = 8个。可用数学归纳法证明此性质: • i = 1时,只有一个根结点。显然2i-1 = 20 = 1,命题成立。 • 假设对所有的j(1≤j<i),命题成立,即第j层上至多有2j-1个结点。可以证明j = i时,命题也成立。 • 根据归纳假设,第i-1层上至多有2i-2个结点,由于二叉树的每一个结点的度最大为2,故在第i层上的结点数,至多是第i-1层上最大结点数的2倍,即2×2i-2 = 2i-1。 • 性质2 深度为k的二叉树至多有2k-1个结点(k≥1)。
6.2.2 二叉树的性质(2) • 性质3 在任何一棵二叉树中,如果其终端结点数为n0,度为2的结点数为n2,则n0 = n2 + 1。 • 介绍两种特殊形态的二叉树:完全二叉树(comlpete binary tree)和满二叉树(full binary tree)。 • 一棵深度为k且有2k - 1个结点的二叉树称为满二叉树,图6.5(a)是一棵深度为4的满二叉树。 • 满二叉树中每一层上的结点数都达到最大值。满二叉树中不存在度为1的结点,每一个结点均有两棵高度相同的子树,叶子结点都在最下面的同一层上。
6.2.2 二叉树的性质(3) • 若在一棵深度为k (k > 1) 的二叉树中,第1层到第k-1层构成一棵深度为k-1的满二叉树,第k层的结点不满2k-1个结点,而这些结点都满放在该层最左边,则此二叉树称为完全二叉树。 • 图6.5(b)是一棵完全二叉树。图6.5(c)和图6.5(d)不是完全二叉树。完全二叉树中的叶子结点只可能出现在二叉树中层次最大的两层上。最下一层的结点一定是从最左边开始向右满放的。且若某个结点没有左孩子,则它一定没有右孩子。 • 满二叉树是完全二叉树,而完全二叉树不一定是满二叉树。
6.2.2 二叉树的性质(4) • 性质4 具有n个结点的完全二叉树的深度为log2n+ 1。 • 性质5 对一棵有n个结点的完全二叉树的结点按层自左向右编号,则对任一编号为i (1 ≤ i ≤ n)的结点有下列性质: • (1) 若i = 1,则结点i是二叉树的根,若i > 1,则结点i的双亲结点是 i/2; • (2) 若2i ≤ n,则结点i有左孩子,左孩子的编号是2i,否则结点i无左孩子,并且是叶子结点; • (3) 若2i + 1 ≤ n,则结点i有右孩子,右孩子的编号是2i + 1,否则结点i无右孩子。 • 这个性质是一般二叉树顺序存储的重要基础。
6.2.2 二叉树的性质(5) • 性质6 对一棵有n个结点的完全二叉树的结点按层自左向右编号,从编号为1的根结点开始到编号为 n/2 的结点为止,都是有孩子结点的非叶子结点,余后的结点均是叶子结点。 n/2 编号的结点可能只有左孩子结点,可能是既有左孩子结点又有右孩子结点。 Return
图6.5 特殊形态的二叉树 Return
6.2.3 二叉树的存储结构(1) • 1.顺序存储结构 • 顺序存储就是用一组地址连续的存储单元来存放一棵二叉树的结点。显然,必须要按规定的次序来存放,这种次序应能反映结点之间的逻辑关系(父子关系),否则二叉树上的基本操作在顺序存储结构上难以实现。 • 若对任意一棵完全二叉树上的结点按层自左向右一一存入向量中,根据上述性质5可知,完全二叉树中结点之间的逻辑关系清楚地通过结点在向量中的序号位置准确地反映出来。图6.6(a)就是一棵完全二叉树的顺序存储结构示意图。
6.2.3 二叉树的存储结构(2) • 对于一般二叉树只能将其“转化”为完全二叉树后,按照完全二叉树的顺序存储方式将结点存入向量中。转化的方法是在非完全二叉树的“残缺”位置上增设“虚结点”。 • 如图6.6(b)所示是一棵一般二叉树,只有将它“完全化”以后将结点顺序存入向量中才能通过结点的序号位置来确定结点之间的逻辑关系。图中“∧”表示不存在此结点,上述方法解决了一般二叉树的顺序存储问题,但这种存储方法有时会造成存储空间的浪费。最坏的情况下,一个深度为k的且只有k个结点的右单支二叉树却需要2k -1个结点的存储空间,如图6.6(c)所示。
6.2.3 二叉树的存储结构(3) • 2.链式存储结构 • (1)二叉树的链式存储结构主要是设计结点结构。由二叉树的定义得知,二叉树的结点由一个数据元素和分别指向其左、右子树的两个指针构成。因此链式存储二叉树的结点结构如图6.7(a)所示。结点包括三个域:数据域和左、右指针域。结点的类型说明如下: #define DATATYPE2 char typedef struct node1 { DATATYPE2 data; struct node1 *lchild, *rchild; }BTCHINALR;
6.2.3 二叉树的存储结构(4) • 在一棵二叉树中,所有结点的类型都为BTCHINALR,头指针root指向根结点,这就构成了二叉树的存储结构。我们把这种存储结构称为二叉链表。一个二叉链表由头指针惟一确定。 • 当root = NULL时,二叉树为空。具有n个结点的二叉树中,共有2n个指针域,其中n-1个指针域用来指示结点的左、右孩子,其余的n+1个指针域为空。图6.7(b)是一棵二叉树的二叉链表存储结构的示意图。 • (2)有时为了便于找到结点的双亲,结点结构中增加一个指向其双亲结点的指针域,对应的结点类型说明如下:
6.2.3 二叉树的存储结构(5) #define DATATYPE2 char typedef struct node2 { DATATYPE2 data; struct node2 *lchild, *rchild, *parent; }BTCHINALRP; • 图6.7(c)和图6.7(d)是带双亲指针域的二叉树的结点结构和二叉树的三叉链表存储结构示意图。 Return
图6.6 二叉树的顺序存放结构 Return
图6.7 二叉树的链式存储结构 Return
6.2.4 遍历二叉树(1) • 在二叉树的很多应用中,常常要求在树中查找某些指定的结点或对树中全部结点逐一进行某种操作,这就需要依次访问二叉树中的结点,即遍历二叉树(traversing binary tree)的问题。 • 遍历二叉树是指按某种规律周游二叉树,对树中的每个结点访问一次且仅访问一次。 • 对二叉树的遍历过程是将非线性结构的二叉树中的结点排列在一个线性序列上的过程。 • 二叉树的定义是递归的,一棵非空的二叉树是由根结点、左子树、右子树这三个基本部分组成,因此若能依次遍历这三部分,便是遍历了整个二叉树,假如以L,D,R分别表示左子树、根结点和右子树,则可有DLR,LDR,LRD,DRL,RDL,RLD六种遍历的方法。再限定先左子树后右子树,则将前三种方案称之为先序遍历、中序遍历和后序遍历。
6.2.4 遍历二叉树(2) • 1.先序遍历二叉树 • 若二叉树非空,则依次进行如下操作: • (1) 访问根结点; • (2) 先序遍历左子树; • (3) 先序遍历右子树。 • 其递归算法如下: void preorder(BTCHINALR * bt) { if(bt != NULL) { printf(″%c ″,bt->data) ; preorder(bt->lchild); preorder(bt->rchild) ;} }
6.2.4 遍历二叉树(3) • 2.中序遍历二叉树 • 若二叉树非空,则依次进行如下操作: • (1) 中序遍历左子树; • (2) 访问根结点; • (3) 中序遍历右子树。 • 其递归算法如下: void inorder(BTCHINALR *bt) { if(bt != NULL) { inorder(bt->lchild); printf(″%c ″,bt->data); inorder(bt->rchild); } }
6.2.4 遍历二叉树(4) • 3.后序遍历二叉树 • 若二叉树非空,则依次进行如下操作: • (1) 后序遍历左子树; • (2) 后序遍历右子树; • (3) 访问根结点。 • 其递归算法如下: void postorder(BTCHINALR *bt) { if(bt != NULL) { postorder(bt->lchild); postorder(bt->rchild); printf(″%c ″,bt->data);} }
6.2.4 遍历二叉树(5) • 对同一棵二叉树,用上述三种不同的遍历规律所得到的结果都是线性序列,有且仅有一个开始结点和一个终端结点。但三个线性序列并不相同,因此必须指明序列的遍历规则。图6.8是二叉树BT的三种遍历的序列。 Return
图6.8 二叉树BT的三种遍历序列 Return
6.3 哈夫曼树 • 6.3.1 哈夫曼树的定义及构造方法 • 6.3.2 哈夫曼编码 Return
6.3.1 哈夫曼树的定义及构造方法(1) • 哈夫曼树(Huffman),又称最优二叉树。 • 1、路径和路径长度? • 路径:从树中一个结点到另一个结点之间的分支。 • 路径长度:路径上的分支数目(大小)称为两个结点之间的路径长度。 • 树的路径长度:是从该树的根到每一结点的路径长度之和。 • 2、在许多应用中,树中结点常常带权,权值是具有某种含义的实数,称为该结点的权。结点的带权路径长度是该结点到树根之间的路径长度与结点的权的乘积。树的带权路径长度(weighted path length of tree)定义为树中所有叶子结点的带权路径长度之和,记为:
6.3.1 哈夫曼树的定义及构造方法(2) n i=1 WPL=∑wili • 其中,n表示叶子结点的数目;wi 和 li 分别表示第i个叶子结点的权值和树根到该叶子结点之间的路径长度。 • 假设有n个权值(w1, w2, w3, …, wn),欲构造一棵有n个叶子结点的二叉树,每个叶子结点带权为wi,则这样的二叉树可以有若干棵,如图6.16中的三棵二叉树,它们都有4个叶子结点A, B, C, D,且分别带权7, 5, 2, 4,三棵二叉树的带权路径长度分别为36,46,35。研究WPL的大小在树的实际应用问题中很有价值,同时如何构造二叉树也成为研究的问题。
6.3.1 哈夫曼树的定义及构造方法(3) • 3、哈夫曼树的定义: • 假设有n个权值{w1, w2, …, wn},在以这些权值为叶子结点权值所构造的所有二叉树中,带权路径长度WPL最小的二叉树称为哈夫曼树又称为最优二叉树。 • 构造最优二叉树的算法称之为哈夫曼算法,描述如下: • (1) 根据给出的n个权值{w1, w2, …, wn},构成n棵二叉树的集合F = {T1, T2, …, Tn},其中每棵二叉树Ti中只有一个带权为wi的根结点,其左右子树均空; • (2) 在F中选取两棵根结点的权值最小的二叉树作为左右子树构成一棵新的二叉树,且置新的二叉树的根结点的权值为左、右子树根结点的权值之和;
6.3.1 哈夫曼树的定义及构造方法(4) • (3) 在F中删除上面选中的那两棵二叉树,同时将新得到的二叉树加入F中; • (4) 重复(2)、(3),直到F中只含有一棵二叉树为止。 • 这棵二叉树便是哈夫曼树。图6.17展示了按照权值{1,2,3,2,2}构造出哈夫曼树的过程。哈夫曼算法中要说明的是: • (1) 在选取两棵根结点权值最小的二叉树时,出现权值相同的情况时,可以在相同权值的二叉树中任选一棵; • (2) 两棵根结点最小的二叉树组成新的二叉树的左右子树时,谁左谁右没有规定; • (3) 哈夫曼树中,权值越大的叶子结点离根越近,这也是WPL最小的实际根据和哈夫曼树的应用依据; • (4) 哈夫曼树中没有度为1的结点,根据二叉树性质n0 = n2 +1,可推得n个叶子结点的哈夫曼树共有2n-1个结点。 Return
图6.16 具有不同带权路径长度的二叉树 Return
图6.17 构造哈夫曼树的过程示例 Return
6.3.2 哈夫曼编码(1) • 在电报的运作过程中,传送的电文以二进制代码作为电报编码。即电报发出方需将电文转换成二进制编码发出,而接收方又将接收到的一串二进制码按编码原则翻译成原电文。 • 例如,传送的电文为“ABAACCBADCA”, 电文中只含有四个字符,每个字符用二个bit作为定长编码即可发送电文和接收电文。假设A, B, C, D的二进制编码分别为00, 01, 10, 11,则上述电文的译文为“0001000010100100111000”,总长为22个bit。对方接收时,按二位一组进行译码即可。
6.3.2 哈夫曼编码(2) • 从实际应用出发,希望译码后传送的电文总长尽可能地短。对每一个字符的编码,设计为定长码已无法缩短电文的总长,如果将字符设计成长度不等的编码,且让电文中出现次数较多的字符采用尽可能短的编码,则可减少电文总长。上述例子中,因A,C字符出现的频率较高,如果A,B,C,D的编码分别设计为0, 00, 1, 01,则上述字串的电文可译成总长为14个bit的二进制串“00000110000110”,虽然减少了电文的长度,但是接收方无法将电文译成原文,例如前面5个0的二进制位串“00000”就有多种译法,可以译成“AAAAA”,也可以译成“ABAA”等等。其原因是上述设计的字符编码不是前缀编码。前缀编码是这样一种编码:若要设计长短不等的字符编码,则必须保证任何一个字符的编码都不是另一个字符的编码的前缀。按前缀编码翻译电文,一定能惟一地被翻译成原文。
6.3.2 哈夫曼编码(3) • 设每种字符在电文中出现的频率为wi,其编码长度(二进制码位数)为li,电文中可能出现的字符有n种,则电文总长为: ∑wili • 正好是对应的哈夫曼树的WPL。 • 因此,可利用哈夫曼树的原理来设计二进制前缀编码,并使译得的电文总长最短。 • 具体设计如下:将可能出现的字符作为叶子结点,在电文中出现的频率作为各个对应叶子结点的权值,设计一棵哈夫曼树,树中左子树分支表示二进制数“0”,右子树分支表示二进制数 “1”,则可得到每一个字符的二进制前缀编码。设A,B,C,D出现的频率为0.4, 0.3, 0.2, 0.1,则得到的哈夫曼树和二进制前缀编码如图6.18所示。 n i=1
6.3.2 哈夫曼编码(4) • 按此编码,前面的电文可转换成总长为21个bit的二进制串 “010001101101001111100”,可以看出,这种编码不定长的前缀编码能将电文惟一地无二义性地翻译成原文。当原文较长,频率很不均匀时,这种编码可使传送的电文缩短很多。 • 当然,也可以在哈夫曼树中规定左分支表示“1”,右分支表示“0”,得到的二进制前缀编码虽然不一样,但使用效果一样。 Return
图6.18 哈夫曼编码设计示例 Return
6.4 应用举例及分析(1) • 例6.1 写出二叉树的中根遍历非递归算法。 Void inorder_notrecursive(BTCHINALR *bt) { BTCHINALR *q, *s[20]; int top = 0; int bool = 1; q = bt; do { while(q != NULL) { top ++; s[top] = q; q = q->lchild; }