第六章 树和二叉树 |
本章主要讲授内容 1.1 定义 树是一种常用的非线性结构。我们可以这样定义:树是n(n≥0)个结点的有限集合。若n=0,则称为空树;否则,有且仅有一个特定的结点被称为根,当n>1时,其余结点被分成m(m>0)个互不相交的子集T1,T2,...,Tm,每个子集又是一棵树。由此可以看出,树的定义是递归。 |
结点 数据元素的内容及其指向其子树根的分支统称为结点。 常用操作: |
类型定义: #define MAX_TREE_LINKLIST_SIZE 100 typedef struct { TElemtype info; int parent; } ParentLinklist; typedef struct { ParentLinklist elem[MAX_TREE_LINKLIST_SIZE]; int n; //树中当前的结点数目 }ParentTree; 这种存储方法的特点是寻找结点的双亲很容易,但寻找结点的孩子比较困难。 算法实现举例: int Parent(ParentTree T,int linklist) { if (linklist<0||linklist>=T.n) return -2; else return T.elem[linklist].parent; } |
在C语言中,这种存储形式定义如下: #define MAX_TREE_LINKLIST_SIZE 10 typedef struct ChildLinklist{ int child; //该孩子结点在一维数组中的下标值 struct ChileLinklist *next; //指向下一个孩子结点 }CLinklist; typedef struct{ Elemtype info; //结点信息 CLinklist *firstchild; //指向第一个孩子结点的指针 }TLinklist; typedef struct { TLinklist elem[MAX_TREE_LINKLIST_SIZE]; int n,root; //n为树中当前结点的数目,root为根结点在一维数组中的位置 }ChildTree; 这种存储结构的特点是寻找某个结点的孩子比较容易,但寻找双亲比较麻烦,所以,在必要的时候,可以将双亲表示法和孩子表示法结合起来,即将一维数组元素增加一个表示双亲结点的域parent,用来指示结点的双亲在一维数组中的位置。 获取给定结点第i个孩子的操作算法实现: int Child(ChildTree T, int linklist, int i) { if (linklist<0||linklist>=T.n) return -2; p=T.elem[linklist].firstchild; j=1; while (p&&j!=i) { p=p->next; j++;} if (!p) return -2; else return p->child; } 2.3 孩子兄弟表示法 孩子兄弟表示法也是一种链式存储结构。它通过描述每个结点的一个孩子和兄弟信息来反映结点之间的层次关系,其结点结构为: |
在C语言中,这种存储形式定义如下: typedef struct CSLinklist{ 1.1 定义 定义:二叉树是另一种树形结构。它与树形结构的区别是: (1)每个结点最多有两棵子树; (2)子树有左右之分。 二叉树也可以用递归的形式定义。即:二叉树是n(n≥0)个结点的有限集合。当n=0时,称为空二叉树;当n>0时,有且仅有一个结点为二叉树的根,其余结点被分成两个互不相交的子集,一个作为左子集,另一个作为右子集,每个子集又是一个二叉树。 |
二叉树的5种形态 |
1.2 二叉树的基本运算(略见教材) (1) 构造一棵二叉树 CreateBTree ( BT) (2)清空以BT为根的二叉树 ClearBTree(BT) (3)判断二叉树是否为空 BTreeEmpty(BT) (4)获取给定结点的左孩子和右孩子 LeftChild(BT,linklist),RightChild(BT,linklist) (5)获取给定结点的双亲 Parent(BT,linklist) (6)遍历二叉树Traverse(BT) 2.二叉树的性质 二叉树具有下列5个重要的性质。 【性质1】 在二叉树的第i层上最多有2i-1个结点(i≥1)。 叉树的第1层只有一个根结点,所以,i=1时,2i-1=21-1=20=1成立。 假设对所有的j,1≤j成立,即第j层上最多有2j-1个结点成立。若j=i-1,则第j层上最多有2j-1=2i-2个结点。由于在二叉树中,每个结点的度最大为2,所以可以推导出第i层最多的结点个数就是第i-1层最多结点个数的2倍,即2i-2*2=2i-1。 【性质2】 深度为K的二叉树最多有2K-1个结点(K≥1)。 由性质1可以得出,1至K层各层最多的结点个数分别为:20,21,22,23,...,2K-1。这是一个以2为比值的等比数列,前n项之 |
【性质3】 对于任意一棵二叉树BT,如果度为0的结点个数为n0,度为2的结点个数为n2,则n0=n2+1。 证明:假设度为1的结点个数为n1,结点总数为n,B为二叉树中的分支数。 因为在二叉树中,所有结点的度均小于或等于2,所以结点总数为: n=n0+n1+n2 (1) 再查看一下分支数。在二叉树中,除根结点之外,每个结点都有一个从上向下的分支指向,所以,总的结点个数n与分支数B之间的关系为:n=B+1。 又因为在二叉树中,度为1的结点产生1个分支,度为2的结点产生2个分支,所以分支数B可以表示为:B=n1+2n2。 将此式代入上式,得: n=n1+2n2+1 (2) 用(1)式减去(2)式,并经过调整后得到:n0=n2+1。 满二叉树: 如果一个深度为K的二叉树拥有2K-1个结点,则将它称为满二叉树。 |
完全二叉树:有一棵深度为h,具有n个结点的二叉树,若将它与一棵同深度的满二叉树中的所有结点按从上到下,从左到右的顺序分别进行编号,且该二叉树中的每个结点分别与满二叉树中编号为1~n的结点位置一一对应,则称这棵二叉树为完全二叉树。 【性质4】 具有n个结点的完全二叉树的深度为 ?log2n?+1。其中,?log2n? 的结果是不大于log2n的最大整数。 证明:假设具有n个结点的完全二叉树的深度为K,则根据性质2可以得出: 2K-1-1 2K-1≤n<2K 将不等式中的三项同取以2为底的对数,并经过化简后得到: K-1≤log2n 【性质5】 对于有n个结点的完全二叉树中的所有结点按从上到下,从左到右的顺序进行编号,则对任意一个结点i (1≤i≤n),都有: (1)如果i=1,则结点i是这棵完全二叉树的根,没有双亲;否则其双亲结点的编号为 ?i/2?。 (2)如果2i>n,则结点i没有左孩子;否则其左孩子结点的编号为2i。 (3)如果2i+1>n,则结点i没有右孩子;否则其右孩子结点的编号为2i+1。 下面我们利用数学归纳法证明这个性质。 我们首先证明(2)和(3)。 当i=1时,若n≥3,则根的左、右孩子的编号分别是2,3;若n<3,则根没有右孩子;若n<2,则根将没有左、右孩子;以上对于(2)和(3)均成立。 假设:对于所有的1≤j≤i 结论成立。即:结点j的左孩子编号为2j;右孩子编号为2j+1。 |
由完全二叉树的结构可以看出:结点i+1或者与结点i同层且紧邻i结点的右侧,或者i位于某层的最右端,i+1位于下一层的最左端。 可以看出,i+1的左、右孩子紧邻在结点i的孩子后面,由于结点i 的左、右孩子编号分别为2i和2i+1,所以,结点i+1的左、右孩子编号分别为2i+2和2i+3,经提取公因式可以得到:2(i+1)和2(i+1)+1,即结点i+1的左孩子编号为2(i+1);右孩子编号为2(i+1)+1。 又因为二叉树由n个结点组成,所以,当2(i+1)+1>n,且2(i+1)=n时,结点i+1只有左孩子,而没有右孩子;当2(i+1)>n,结点i+1既没有左孩子也没有右孩子。 以上证明得到(2)和(3)成立。 下面利用上面的结论证明(1)。 对于任意一个结点i,若2i≤n,则左孩子的编号为2i,反过来结点2i的双亲就是i,而 ?2i/2?=i;若2i+1≤n,则右孩子的编号为2i+1,反过来结点2i+1的双亲就是i,而 ?(2i+1)/2? =i,由此可以得出(1)成立。 3.二叉树的存储结构 二叉树也可以采用两种存储方式:顺序存储结构和链式存储结构。 3.1 顺序存储结构 这种存储结构适用于完全二叉树。其存储形式为:用一组连续的存储单元按照完全二叉树的每个结点编号的顺序存放结点内容。下面是一棵二叉树及其相应的存储结构 |
在C语言中,这种存储形式的类型定义如下所示: #define MAX_TREE_LINKLIST_SIZE 100 typedef struct { Elemtype elem[MAX_TREE_LINKLIST_SIZE]; //根存储在下标为1的数组单元中 int n; //当前完全二叉树的结点个数 }QBTree; 这种存储结构的特点是空间利用率高、寻找孩子和双亲比较容易。下面我们给出完全二叉树在这种存储形式下的操作算法。 (1)构造一棵完全二叉树 void CreateBTree(QBTree *BT,Elemtype elem[ ],int n) { if (n>=MAX_TREE_LINKLIST_SIZE) n=MAX_TREE_LINKLIST_SIZE-1; for (i=1; i<=n;i++) BT->elem[i]=elem[i]; BT->n=n; } (2)获取给定结点的左孩子 int LeftCHild(QBTree BT,int linklist) { if (2*linklist>BT.n) return 0; else return 2*linklist; } RightChild(BT,linklist)与这个操作类似,读者可试着自行完成。 (3)获取给定结点的双亲 int Parent(QBTree BT,int linklist) { if (1<=linklist&&linklist<=BT.n) return i/2; else return -1; } 3.2 链式存储结构 在顺序存储结构中,利用编号表示元素的位置及元素之间孩子或双亲的关系,因此对于非完全二叉树,需要将空缺的位置用特定的符号填补,若空缺结点较多,势必造成空间利用率的下降。在这种情况下,就应该考虑使用链式存储结构。 常见的二叉树结点结构如下所`示: |
其中,Lchild和Rchild是分别指向该结点左孩子和右孩子的指针,elem是数据元素的内容。在C语言中的类型定义为: typedef struct BTLinklist{ Elemtype elem; struct BTLinklist *Lchild,*Rchlid; }BTLinklist,*BTree; 下面是一棵二叉树及相应的链式存储结构 |
这种存储结构的特点是寻找孩子结点容易,双亲比较困难。因此,若需要频繁地寻找双亲,可以给每个结点添加一个指向双亲结点的指针域,其结点结构如下所示。 |
4. 遍历二叉树 二叉树是一种非线性的数据结构,在对它进行操作时,总是需要逐一对每个数据元素实施操作,这样就存在一个操作顺序问题,由此提出了二叉树的遍历操作。所谓遍历二叉树就是按某种顺序访问二叉树中的每个结点一次且仅一次的过程。这里的访问可以是输出、比较、更新、查看元素内容等等各种操作。 二叉树的遍历方式分为两大类:一类按根、左子树和右子树三个部分进行访问;另一类按层次访问。下面我们将分别进行讨论。 4.1 按根、左子树和右子树三部分进行遍历 遍历二叉树的顺序存在下面6种可能: TLR(根左右), TRL(根右左) LTR(左根右), RTL(右根左) LRT(左右根), RLT(右左根) 其中,TRL、RTL和RLT三种顺序在左右子树之间均是先右子树后左子树,这与人们先左后右的习惯不同,因此,往往不予采用。余下的三种顺序TLR、LTR和LRT根据根访问的位置不同分别被称为先序遍历、中序遍历和后序遍历。 (1)先序遍历 若二叉树为空,则结束遍历操作;否则 访问根结点; 先序遍历左子树; 先序遍历右子树。 (2)中序遍历若二叉树为空,则结束遍历操作;否则 中序遍历左子树; 访问根结点; 中序遍历右子树。 (3)后序遍历 若二叉树为空,则结束遍历操作;否则 后序遍历左子树; 后序遍历右子树; 访问根结点。 下面是一棵二叉树及其经过三种遍历得到的相应序列 |
下面我们再给出两种遍历二叉树的方法: (1)对一棵二叉树中序遍历时,若我们将二叉树严格地按左子树的所有结点位于根结点的左侧,右子树的所有结点位于根右侧的形式绘制,就可以对每个结点做一条垂线,映射到下面的水平线上,由此得到的顺序就是该二叉树的中序遍历序列 |
(2)任何一棵二叉树都可以将它的外部轮廓用一条线绘制出来,我们将它称为二叉树的包线,这条包线对于理解二叉树的遍历过程很有用。 |
由此可以看出:(1)遍历操作实际上是将非线性结构线性化的过程,其结果为线性序列,并根据采用的遍历顺序分别称为先序序列、中序序列或后序序列;(2)遍历操作是一个递归的过程,因此,这三种遍历操作的算法可以用递归函数实现。 (1)先序遍历递归算法 void PreOrder(BTree BT) { if (BT) { Visit(BT); PreOrder(BT->Lchild); PreOrder(BT->Rchild); } } (2)中序遍历递归算法 void InOrder(BTree BT) { if (BT) { InOrder(BT->Lchild); Visit(BT); InOrder(BT->Rchild); } } (3)后序遍历递归算法 void PostOrder(BTree BT) { if (BT) { PostOrder(BT->Lchild); PostOrder(BT->Rchild); Visit(BT); } } 4.2 按层次遍历二叉树 实现方法为从上层到下层,每层中从左侧到右侧依次访问每个结点。下面我们将给出一棵二叉树及其按层次顺序访问其中每个结点的遍历序列。 |
void LevelOreder(QBTree BT)
特点:一棵树转换成二叉树后,根结点没有右孩子。 将森林转换成二叉树的方法与一棵树转换成二叉树的方法类似,只是把森林中所有树的根结点看作兄弟关系,并对其中的每棵树依依地进行转换。 6.2 二叉树还原成树或森林 这个过程实际上是树、森林转换成二叉树的逆过程,即将该二叉树看作是树或森林的孩子兄弟表示法。比如,若二叉树为空,树也为空;否则,由二叉树的根结点开始,延右指针向下走,直到为空,途经的结点个数是相应森林所含树的棵数;若某个结点的左指针非空,说明这个结点在树中必有孩子,并且从二叉树中该结点左指针所指结点开始,延右指针向下走,直到为空,途经的结点个数就是这个结点的孩子数目。 第三节 哈夫曼树及其应用 1.哈夫曼树的定义 |
在二叉树中,一个结点到另一个结点之间的分支构成这两个结点之间的路径。 |
这三棵二叉树的带权路径长度分别为: (2)在森林中选取两棵根结点权值最小的二叉树作为左右子树构造一棵新二叉树,新二叉树的根结点权值为这两棵树根的权值之和; (3)在森林中,将上面选择的这两棵根权值最小的二叉树从森林中删除,并将刚刚新构造的二叉树加入到森林中; (4)重复上面(2)和(3),直到森林中只有一棵二叉树为止。这棵二叉树就是哈夫曼树。 假设有一组权值{5,29,7,8,14,23,3,11},下面我们将利用这组权值演示构造哈夫曼树的过程。 |
这就是以上述8个权值为叶子结点权值构成的哈夫曼树,它的带权的路径长度为: WPL=(23+29)*2+(11+14)*3+(3+5+7+8)*4=271 2.判定树 在很多问题的处理过程中,需要进行大量的条件判断,这些判断结构的设计直接影响着程序的执行效率。例如,编制一个程序,将百分制转换成五个等级输出。大家可能认为这个程序很简单,并且很快就可以用下列形式编写出来: if (socre<60) printf("bad"); else if (socre<70) printf("pass"); else if (score<80) printf("general"); else if (score<90) printf("good"); esle printf("very good"); 在实际应用中,往往各个分数段的分布并不是均匀的。下面就是在一次考试中某门课程的各分数段的分布情况: |
3.前缀编码 在电文传输中,需要将电文中出现的每个字符进行二进制编码。在设计编码时需要遵守两个原则:(1)发送方传输的二进制编码,到接收方解码后必须具有唯一性,即解码结果与发送方发送的电文完全一样;(2)发送的二进制编码尽可能地短。下面我们介绍两种编码的方式。 (1)等长编码 这种编码方式的特点是每个字符的编码长度相同(编码长度就是每个编码所含的二进制位数)。假设字符集只含有4个字符A,B,C,D,用二进制两位表示的编码分别为00,01,10,11。若现在有一段电文为:ABACCDA,则应发送二进制序列:00010010101100,总长度为14位。当接收方接收到这段电文后,将按两位一段进行译码。这种编码的特点是译码简单且具有唯一性,但编码长度并不是最短的。 (2)不等长编码 在传送电文时,为了使其二进制位数尽可能地少,可以将每个字符的编码设计为不等长的,使用频度较高的字符分配一个相对比较短的编码,使用频度较低的字符分配一个比较长的编码。例如,可以为A,B,C,D四个字符分别分配0,00,1,01,并可将上述电文用二进制序列:000011010发送,其长度只有9个二进制位,但随之带来了一个问题,接收方接到这段电文后无法进行译码,因为无法断定前面4个0是4个A,1个B、2个A,还是2个B,即译码不唯一,因此这种编码方法不可使用。 (1)利用字符集中每个字符的使用频率作为权值构造一个哈夫曼树; (2)从根结点开始,为到每个叶子结点路径上的左分支赋予0,右分支赋予1,并从根到叶子方向形成该叶子结点的编码。 假设有一个电文字符集中有8个字符,每个字符的使用频率分别为{0.05,0.29,0.07,0.08,0.14,0.23,0.03,0.11},现以此为例设计哈夫曼编码。 哈夫曼编码设计过程为: (1)为方便计算,将所有字符的频度乘以100,使其转换成整型数值集合,得到{5,29,7,8,14,23,3,11}; (2)以此集合中的数值作为叶子结点的权值构造一棵哈夫曼树,如图5-27所示; (3)由此哈夫曼树生成哈夫曼编码,如图5-28所示。 |
比如,发送一段编码:0000011011010010, 第六章 作 业 1. 对于图6.29给出的树,指出树中的根结点、叶结点和分支结点。并指出各个结点的度数和层数。2. 对图6.29所示的树,采用先根次序、后根次序和中根次序遍历。问得到怎样的结点序列? 3. 对图6.29所示的树,分别采用先根次序的父指针表示法、子表表示法、长子-兄弟表示法,试画出各种方法的图示。 4. 已知一棵度为m的树中有n1个度为1的结点,n2个度为2的结点,…,nm个度为m的结点,问该树中有多少个叶子结点? 5. 用三个结点A,B,C可以构成多少种不同的二叉树?请把它们画出来。 6. 将图6.29所示的树转换成对应的二叉树是什么样子?请把它画出来。 7. 请写出利用栈对二叉树进行先根次序遍历的非递归算法。 |
8. 请写出利用栈对二叉树进行对称次序遍历的非递归算法。 |
21.画出下图所示的各二叉树所对应的森林。 |
22.假设用于通信的电文由字符集{a,b,c,d,e,f,g,h}中的字母构成,这8个字母在电文中出现的概率分别为{0.07,0.19,0.02,0.06,0.32,0.03,0.21,0.10}。 1) 为这8个字母设计Huffman编码。 2) 若用三位二进制数(0-7)对这8个字母进行等长编码,则Huffman编码的平均码长是等长编码的百分之几?它使电文总长平均压缩多少? 上 机 题 |
1. 设计一个程序,根据二叉树的先根序列和对称序序列创建一棵用左右指针表示的二叉树。
例题一: 数据结构上机测试4.1:二叉树的遍历与应用1 Time Limit: 1000MS Memory limit: 65536K题目描述 输入二叉树的先序遍历序列和中序遍历序列,输出该二叉树的后序遍历序列。
输入 第一行输入二叉树的先序遍历序列; 第二行输入二叉树的中序遍历序列。 输出 输出该二叉树的后序遍历序列。
示例输入ABDCEF
BDAECF
示例输出DBEFCA
代码如下:
题目描述如下:
数据结构实验之二叉树的建立与遍历 Time Limit: 1000MS Memory limit: 65536K题目描述
已知一个按先序序列输入的字符序列,如abc,,de,g,,f,,,(其中逗号表示空节点)。请建立二叉树并按中序和后序方式遍历二叉树,最后求出叶子节点个数和二叉树深度。
输入
输入一个长度小于50个字符的字符串。
输出
输出共有4行:
第1行输出中序遍历序列; 第2行输出后序遍历序列; 第3行输出叶子节点个数; 第4行输出二叉树深度。 示例输入abc,,de,g,,f,,, 示例输出cbegdfacgefdba35 代码如下:
例题三; 题目描述如下: 数据结构实验之求二叉树后序遍历和层次遍历 Time Limit: 1000MS Memory limit: 65536K题目描述
已知一棵二叉树的前序遍历和中序遍历,求二叉树的后序遍历。
输入
输入数据有多组,第一行是一个整数t (t<1000),代表有t组测试数据。每组包括两个长度小于50 的字符串,第一个字符串表示二叉树的先序遍历序列,第二个字符串表示二叉树的中序遍历序列。
输出
每组第一行输出二叉树的后序遍历序列,第二行输出二叉树的层次遍历序列
示例输入2 abdegcf dbgeafc xnliu lnixu 示例输出dgebfca abcdefg linux xnuli
代码如下:
|