第一二节 树及二叉树.

Slides:



Advertisements
Similar presentations
因数与倍数 2 、 5 的倍数的特征
Advertisements

3 的倍数的特征 的倍数有 : 。 5 的倍数有 : 。 既是 2 的倍数又是 5 的倍数有 : 。 12 , 18 , 20 , 48 , 60 , 72 , , 25 , 60 ,
2 和 5 的倍数的特征 运动热身 怎样找一个数的倍数? 从小到大写出 2 的倍数( 10 个): 写出 5 的倍数( 6 个) 2 , 4 , 6 , 8 , 10 , 12 , 14 , 16 , 18 , 20 5 , 10 , 15 , 20 , 25 , 30.
练一练: 在数轴上画出表示下列各数的点, 并指出这些点相互间的关系: -6 , 6 , -3 , 3 , -1.5, 1.5.
一、算法的基本概念 二、数据结构 三、栈和队列 四、排序方法 五、二叉树
二叉树在树结构的应用中起着非常重要的作用,因为对二叉树的许多操作算法简单,而任何树都可以与二叉树 相互转换,这样就解决了树的
英语词典的维护和识别 题集P168,课本P249 问题描述:
主讲:计算机工程学院 李兰 答疑地点:主教学楼B区213
常用逻辑用语复习课 李娟.
小学生游戏.
数据结构学习考 复习课(2) 主要内容: 第三部分:树、二叉树、森林.
第6章 树和二叉树 树型结构是一类非常重要的非线性结构。直观地,树型结构是以分支关系定义的层次结构。
助教:李伟力 电子科学与技术系 数据结构第二次习题课 助教:李伟力 电子科学与技术系
实用数据结构基础 第6章 树.
机械CAD中常用的数据结构.
第六章 二叉树和树 6.1 二叉树 6.2 二叉树的基本操作与存储实现 6.3 二叉树的遍历 6.4 线索二叉树 6.5 树和森林
第5章 定积分及其应用 基本要求 5.1 定积分的概念与性质 5.2 微积分基本公式 5.3 定积分的换元积分法与分部积分法
初中数学八年级下册 (苏科版) 10.4 探索三角形 相似的条件(2).
C++中的声音处理 在传统Turbo C环境中,如果想用C语言控制电脑发声,可以用Sound函数。在VC6.6环境中如果想控制电脑发声则采用Beep函数。原型为: Beep(频率,持续时间) , 单位毫秒 暂停程序执行使用Sleep函数 Sleep(持续时间), 单位毫秒 引用这两个函数时,必须包含头文件
树(三) 2012初赛知识点梳理.
第六章 树和二叉树.
赵海燕 软件研究所 14 Apr 第5章 树与二叉树 之三 赵海燕 软件研究所 14 Apr
树和二叉树(三).
强连通分量 无向图 1、任意两顶点连通称该图为连通图 2、否则将其中的极大连通子图称为连通分量 A D C B E 有向图
第六章 树与二叉树 6.1 树的定义和基本术语 6.2 二叉树 6.3 遍历二叉树和线索二叉树 6.4 树和森林
走进编程 程序的顺序结构(二).
元素替换法 ——行列式按行(列)展开(推论)
湖北大学知行学院 教师:涂晓帆 Sunday, December 09, 2018
第六章 树和二叉树.
§2 求导法则 2.1 求导数的四则运算法则 下面分三部分加以证明, 并同时给出相应的推论和例题 .
第5章 树和二叉树 5.1树 5.2二叉树 5.3二叉树的遍历 5.4线索二叉树 5.5树、森林与二叉树的转换 5.6哈夫曼树.
第11讲 树和二叉树(二).
第六章 树 2019/1/14.
第六章 树与森林 树和森林的概念 二叉树 (Binary Tree) 二叉树的表示
第五章 树 5.1 树的定义 树是一类重要的非线性数据结构,是以分支关系定义的层次结构 定义
Tree & Binary Tree.
使用矩阵表示 最小生成树算法.
无向树和根树.
第一章 函数与极限.
计算.
C++语言程序设计 C++语言程序设计 第七章 类与对象 第十一组 C++语言程序设计.
数列.
简单介绍 用C++实现简单的模板数据结构 ArrayList(数组, 类似std::vector)
线段的有关计算.
顺序表的删除.
线 性 代 数 厦门大学线性代数教学组 2019年4月24日6时8分 / 45.
第4章 Excel电子表格制作软件 4.4 函数(一).
第六章 树和二叉树.
第九节 赋值运算符和赋值表达式.
§6.7 子空间的直和 一、直和的定义 二、直和的判定 三、多个子空间的直和.
3.16 枚举算法及其程序实现 ——数组的作用.
第5节 树 前两章学习的栈和队列属于线性结构。在这种结构中,数据元素的逻辑位置之间呈线性关系,每一个数据元素通常只有一个前件(除第一个元素外)和一个后件(除最后一个元素外)。在实际生活中,可以用线性结构描述数据元素之间逻辑关系的问题是很广泛的,但也有很多问题不能依靠线性结构来解决,例如家谱、行政组织机构等都是非线性的数据结构。其中树就是一种非线性的数据结构。
1.设A和B是集合,证明:A=B当且仅当A∩B=A∪B
树和图 tree and graph 蔡亚星.
第4课时 绝对值.
多层循环 Private Sub Command1_Click() Dim i As Integer, j As Integer
第七、八次实验要求.
分数再认识三 真假带分数的练习课.
第15讲 特征值与特征向量的性质 主要内容:特征值与特征向量的性质.
#define RBTREE_RED 0 #define RBTREE_BLACK 1 typedef int dataType; typedef struct treeNode{ dataType key; int color; //red:0.
实验目的:掌握数据的顺序存储结构及它们在计算机中的操作。 实验内容:
基于列存储的RDF数据管理 朱敏
第四节 向量的乘积 一、两向量的数量积 二、两向量的向量积.
位似.
§4.5 最大公因式的矩阵求法( Ⅱ ).
本节内容 1. 平衡二叉树的定义 2.平衡化旋转 3.平衡二叉排序树的插入操作 昆山爱达人信息技术有限公司
最小生成树 最优二叉树.
§2 自由代数 定义19.7:设X是集合,G是一个T-代数,为X到G的函数,若对每个T-代数A和X到A的函数,都存在唯一的G到A的同态映射,使得=,则称G(更严格的说是(G,))是生成集X上的自由T-代数。X中的元素称为生成元。 A变, 变 变, 也变 对给定的 和A,是唯一的.
第五章 树和二叉树.
第三章 图形的平移与旋转.
Presentation transcript:

第一二节 树及二叉树

简介 树是一种非线性的数据结构,用它能很好地描述有分支和层次特性的数据集合。树型结构在现实世界中广泛存在,如社会组织机构的组织关系图就可以用树型结构来表示。树在计算机领域中也有广泛应用,如在编译系统中,用树表示源程序的语法结构。在数据库系统中,树型结构是数据库层次模型的基础,也是各种索引和目录的主要组织形式。在许多算法中,常用树型结构描述问题的求解过程、所有解的状态和求解的对策等。在这些年的国内、国际信息学奥赛、大学生程序设计比赛等竞赛中,树型结构成为参赛者必备的知识之一,尤其是建立在树型结构基础之上的搜索算法。 在树型结构中,二叉树是最常用的结构,它的分支个数确定、又可以为空、并有良好的递归特性,特别适宜于程序设计,因此也常常将一般树转换成二叉树进行处理。

第一节 树的概念----树的定义 一棵树是由n(n>0)个元素组成的有限集合,其中: (1)每个元素称为结点(node); 第一节 树的概念----树的定义 一棵树是由n(n>0)个元素组成的有限集合,其中: (1)每个元素称为结点(node); (2)有一个特定的结点,称为根结点或树根(root); (3)除根结点外,其余结点能分成m(m>=0)个互不相交的有限集合T0,T1,T2,……Tm-1。其中的每个子集又都是一棵树,这些集合称为这棵树的子树。 如下图是一棵典型的树:

树的基本概念 树是递归定义的; 一棵树中至少有1个结点。这个结点就是根结点,它没有前驱,其余每个结点都有唯一的一个前驱结点。每个结点可以有0或多个后继结点。因此树虽然是非线性结构,但也是有序结构。至于前驱后继结点是哪个,还要看树的遍历方法,我们将在后面讨论; 一个结点的子树个数,称为这个结点的度(degree,结点1的度为3,结点3的度为0);度为0的结点称为叶结点(树叶leaf,如结点3、5、6、8、9);度不为0的结点称为分支结点(如结点1、2、4、7);根以外的分支结点又称为内部结点(结点2、4、7);树中各结点的度的最大值称为这棵树的度(这棵树的度为3)。 在用图形表示的树型结构中,对两个用线段(称为树枝)连接的相关联的结点,称上端结点为下端结点的父结点,称下端结点为上端结点的子结点。称同一个父结点的多个子结点为兄弟结点。如结点1是结点2、3、4的父结点,结点 2、3、4是结点1的子结点,它们又是兄弟结点,同时结点2又是结点5、6的父结点。称从根结点到某个子结点所经过的所有结点为这个子结点的祖先。如结点1、4、7是结点8

的祖先。称以某个结点为根的子树中的任一结点都是该结点的子孙。如结点7、8、9都是结点4的子孙。 E.定义一棵树的根结点的层次(level)为1,其它结点的层次等于它的父结点层次加1。如结点2、3、4的层次为1,结点5、6、7的层次为2,结点8、9的层次为3。一棵树中所有的结点的层次的最大值称为树的深度(depth)。如这棵树的深度为3。 F.对于树中任意两个不同的结点,如果从一个结点出发,自上而下沿着树中连着结点的线段能到达另一结点,称它们之间存在着一条路径。可用路径所经过的结点序列表示路径,路径的长度等于路径上的结点个数减1。如上图中,结点1和结点8之间存在着一条路径,并可用(1、4、7、8)表示这条路径,该条路径的长度为3。注意,不同子树上的结点之间不存在路径,从根结点出发,到树中的其余结点一定存在着一条路径。 G.森林(forest)是m(m>=0)棵互不相交的树的集合。

树的存储结构 方法1:数组,称为“父亲表示法”。   const int m = 10; //树的结点数   struct node   {    int data, parent; //数据域,指针域   };   node tree[m]; 优缺点:利用了树中除根结点外每个结点都有唯一的父结点这个性质。很容易找到树根,但找孩子时需要遍历整个线性表。

方法2:树型单链表结构,称为“孩子表示法”。每个结点包括一个数据域和一个指针域(指向若干子结点)。称为“孩子表示法”。假设树的度为10,树的结点仅存放字符,则这棵树的数据结构定义如下: const int m = 10; //树的度 typedef struct node; typedef node *tree; struct node { char data; //数据域 tree child[m]; //指针域,指向若干孩子结点 }; tree t; 缺陷:只能从根(父)结点遍历到子结点,不能从某个子结点返回到它的父结点。但程序中确实需要从某个结点返回到它的父结点时,就需要在结点中多定义一个指针变量存放其父结点的信息。这种结构又叫带逆序的树型结构。

方法3:树型双链表结构,称为“父亲孩子表示法”。每个结点包括一个数据域和二个指针域(一个指向若干子结点,一个指向父结点)。假设树的度为10,树的结点仅存放字符,则这棵树的数据结构定义如下: const int m = 10; //树的度 typedef struct node; typedef node *tree; //声明tree是指向node的指针类型 struct node { char data; //数据域 tree child[m]; //指针域,指向若干孩子结点 tree father; //指针域,指向父亲结点 }; tree t;

方法4:二叉树型表示法,称为“孩子兄弟表示法”。也是一种双链表结构,但每个结点包括一个数据域和二个指针域(一个指向该结点的第一个孩子结点,一个指向该结点的下一个兄弟结点)。称为“孩子兄弟表示法”。假设树的度为10,树的结点仅存放字符,则这棵树的数据结构定义如下:   typedef struct node;   typedef node *tree;   struct node   {    char data; //数据域    tree firstchild, next; //指针域,分别指向第一个孩子结点和下一个兄弟结点   };   tree t;

【例3-1】找树根和孩子 【问题描述】   给定一棵树,输出树的根root,孩子最多的结点max以及他的孩子 【输入格式】   第一行:n(结点数<=100),m(边数<=200)。    以下m行;每行两个结点x和y, 表示y是x的孩子(x,y<=1000)。 【输出格式】   第一行:树根:root。    第二行:孩子最多的结点max。    第三行:max的孩子。 【输入样例】   8 7   4 1   4 2   1 3   1 5   2 6   2 7   2 8 【输出样例】   4   2   6 7 8

【参考程序】 #include<iostream> using namespace std; int n,m,tree[101]={0}; int main() { int i,x,y,root,maxroot,sum=0,j,Max=0; cin>>n>>m; for(i=1;i<=m;i++) cin>>x>>y; tree[y]=x; } for(i=1;i<=n;i++) //找出树根 if(tree[i]==0) root=i;break; for(i=1;i<=n;i++) //找孩子最多的结点 sum=0; for(j=1;j<=n;j++) if(tree[j]==i) sum++; if(sum>Max) { Max=sum;maxroot=i; } cout<<root<<endl<<maxroot<<endl; for(i=1;i<=n;i++) if(tree[i]==maxroot) cout<<i<<" "; return 0;

树的遍历 在应用树结构解决问题时,往往要求按照某种次序获得树中全部结点的信息,这种操作叫作树的遍历。遍历的方法有多种,常用的有:   A、先序(根)遍历:先访问根结点,再从左到右按照先序思想遍历 各棵子树。 如上图先序遍历的结果为:125634789;   B、后序(根)遍历:先从左到右遍历各棵子树,再访问根结点。如 上图后序遍历的结果为:562389741;   C、层次遍历:按层次从小到大逐个访问,同一层次按照从左到右的 次序。 如上图层次遍历的结果为:123456789;   D、叶结点遍历:有时把所有的数据信息都存放在叶结点中,而其余 结点都是用来表示数据之间的某种分支或层次关系,这种情况就 用这种方法。如上图按照这个思想访问的结果为:56389;

大家可以看出,AB两种方法的定义是递归的,所以在程序实现时往往也是采用递归的思想。既通常所说的“深度优先搜索”。如用先序遍历编写的过程如下:   void tral(tree t, int m)   {    If (t)    {    cout << t->data << endl;    for(int i = 0 ; i < m ; i++)    tral(t->child[i], m);    }   } C方法应用也较多,实际上是我们讲的“广度优先搜索”。思想如下:若某个结点被访问,则该结点的子结点应记录,等待被访问。顺序访问各层次上结点,直至不再有未访问过的结点。为此,引入一个队列来存储等待访问的子结点,设一个队首和队尾指针分别表示出队、进队的下标。程序框架如下:

  const int n = 100;   int head, tail, i;   tree q[n];   tree p;   void work()   {    tail = head = 1;    q[tail] = t;    tail++; //队尾为空    while(head < tail)    {    p = q[head];    head++;    cout << p->data << ' ';    for(i = 0 ; i < m ; i++)    if(p->child[i])    {    q[tail] = p->child[i];    tail++;    }    }   }

【例3-2】单词查找树 【问题描述】 在进行文法分析的时候,通常需要检测一个单词是否在我们的单词列表里。为了提高查找和定位的速度,通常都画出与单词列表所对应的单词查找树,其特点如下: 1.根结点不包含字母,除根结点外每一个结点都仅包含一个大写英文字母; 2.从根结点到某一结点,路径上经过的字母依次连起来所构成的字母序列,称为该结点对应的单词。单词列表中的每个单词,都是该单词查找树某个结点所对应的单词; 3.在满足上述条件下,该单词查找树的结点数最少。 4.例如图左边的单词列表就对应于右边的单词查找树。注意,对一个确定的单词列表,请统计对应的单词查找树的结点数(包含根结点)。

【问题输入】   输入文件名为word.in,该文件为一个单词列表,每一行仅包含一个单词和一个换行/回车符。每个单词仅由大写的英文字母组成,长度不超过63个字母 。文件总长度不超过32K,至少有一行数据。 【问题输出】   输出文件名为word.out,该文件中仅包含一个整数,该整数为单词列表对应的单词查找树的结点数。 【样例输入】    A    AN    ASP    AS    ASC    ASCII    BAS    BASIC 【样例输出】 13

【算法分析】 首先要对建树的过程有一个了解。对于当前被处理的单词和当前树:在根结点的子结点中找单词的第一位字母,若存在则进而在该结点的子结点中寻找第二位……如此下去直到单词结束,即不需要在该树中添加结点;或单词的第n位不能被找到,即将单词的第n位及其后的字母依次加入单词查找树中去。但,本问题只是问你结点总数,而非建树方案,且有32K文件,所以应该考虑能不能不通过建树就直接算出结点数?为了说明问题的本质,我们给出一个定义:一个单词相对于另一个单词的差:设单词1的长度为L,且与单词2从第N位开始不一致,则说单词1相对于单词2的差为L-N+1,这是描述单词相似程度的量。可见,将一个单词加入单词树的时候,须加入的结点数等于该单词树中已有单词的差的最小值。 单词的字典顺序排列后的序列则具有类似的特性,即在一个字典顺序序列中,第m个单词相对于第m-1个单词的差必定是它对于前m-1个单词的差中最小的。于是,得出建树的等效算法:

① 读入文件; ② 对单词列表进行字典顺序排序; ③ 依次计算每个单词对前一单词的差,并把差累加起来。注意:第 一个单词相对于“空”的差为该单词的长度; ④ 累加和再加上1(根结点),输出结果。 就给定的样例,按照这个算法求结点数的过程如下表:

【数据结构】 先确定32K(32*1024=32768字节)的文件最多有多少单词和字母。当然应该尽可能地存放较短的单词。因为单词不重复,所以长度为1的单词(单个字母)最多26个;长度为2的单词最多为26*26=676个;因为每个单词后都要一个换行符(换行符在计算机中占2个字节),所以总共已经占用的空间为:(1+2)*26+(2+2)*676=2782字节;剩余字节(32768-2782=29986字节)分配给长度为3的单词(长度为3的单词最多有 26*26*26=17576个)有29986/(3+2)≈5997。所以单词数量最多为26+676+5997=6699。 定义一个数组:string a[32768];把所有单词连续存放起来,用选择排序或快排对单词进行排序。

【参考程序】 #include <iostream> #include <cstdio> #include <string> using namespace std; int i, j, n, t, k; string a[8001]; //数组可以到32768 string s; int main() { freopen("word.in", "r", stdin); freopen("word.out", "w", stdout); while(cin >> a[++n]); //读入文件中的单词并且存储到数组中 n--; for(i = 1 ; i < n ; i++) //单词从小到大排序,选择排序可改为快排sort(a + 1, a + n + 1) for(j = i + 1 ; j <= n ; j++) if(a[i] > a[j]) //两个单词进行交换 s = a[i];

a[i] = a[j]; a[j] = s; } t = a[1].length(); //先累加第1个单词的长度 for(i = 2 ; i <= n ; i++) //依次计算每个单词对前一单词的差 { j = 0; while(a[i][j] == a[i - 1][j] && j < a[i - 1].length()) j++; //求两个单词相同部分的长度 t += a[i].length() - j; //累加两个单词的差length(a[i])-j cout << t + 1 << endl; return 0;

第二节 二叉树----二叉树基本概念 二叉树(binary tree,简写成BT)是一种特殊的树型结构,它的度数为2的树。即二叉树的每个结点最多有两个子结点。每个结点的子结点分别称为左孩子、右孩子,它的两棵子树分别称为左子树、右子树。二叉树有5中基本形态: 前面引入的树的术语也基本适用于二叉树,但二叉树与树也有很多不同,如:首先二叉树的每个结点至多只能有两个结点,二叉树可以为空,二叉树一定是有序的,通过它的左、右子树关系体现出来。

二叉树的性质 【性质1】在二叉树的第i层上最多有2i-1个结点(i>=1)。 证明:很简单,用归纳法:当i=1时,2i-1=1显然成立;现在假设第i-1层时命题成立,即第i-1层上最多有2i –2 个结点。由于二叉树的每个结点的度最多为2,故在第i层上的最大结点数为第i-1层的2倍, 即 2*2i-2=2i–1。 【性质2】深度为k的二叉树至多有2k –1个结点(k>=1)。 证明:在具有相同深度的二叉树中,仅当每一层都含有最大结点数时,其树中结点数最多。因此利用性质1可得,深度为k的二叉树的结点数至多为: 20+21+…+2k-1=2k-1 故命题正确。

【特别】一棵深度为k且有2k–1个结点的二叉树称为满二叉树。如下图A为深度为4的满二叉树,这种树的特点是每层上的结点数都是最大结点数。 可以对满二叉树的结点进行连续编号,约定编号从根结点起,自上而下,从左到右,由此引出完全二叉树的定义,深度为k,有n个结点的二叉树当且仅当其每一个结点都与深度为k的满二叉树中编号从1到n的结点一一对应时,称为完全二叉树。 下图B就是一个深度为4,结点数为12的完全二叉树。它有如下特征:叶结点只可能在层次最大的两层上出现;对任一结点,若其右分支下的子孙的最大层次为m,则在其左分支下的子孙的最大层次必为m或m+1。下图C、D不是完全二叉树,请大家思考为什么?

【性质3】对任意一棵二叉树,如果其叶结点数为n0,度为2的结点数为n2,则一定满足:n0=n2+1。 n=no+n1+n2 ……(式子1) 另一方面,1度结点有一个孩子,2度结点有两个孩子,故二叉树中孩子结点总数是: nl+2n2 树中只有根结点不是任何结点的孩子,故二叉树中的结点总数又可表示为: n=n1+2n2+1 ……(式子2) 由式子1和式子2得到: no=n2+1

【性质4】具有n个结点的完全二叉树的深度为floor(log2n)+1 证明:假设深度为k,则根据完全二叉树的定义,前面k-1层一定是满的,所以n>2k –1 -1。但n又要满足n<=2k -1。所以,2k–1–1<n<=2k -1。变换一下为2k–1<=n<2k。 以2为底取对数得到:k-1<=log2n<k。而k是整数,所以k= floor(log2n)+1。 【性质5】对于一棵n个结点的完全二叉树,对任一个结点(编号为i),有: ①如果i=1,则结点i为根,无父结点;如果i>1,则其父结点编号为i/2。 如果2*i>n,则结点i无左孩子(当然也无右孩子,为什么?即结点i为叶结点);否则左孩子编号为2*i。 ②如果2*i+1>n,则结点i无右孩子;否则右孩子编号为2*i+1。

证明:略,我们只要验证一下即可。总结如图:

二叉树的存储结构 ①链式存储结构,即单链表结构或双链表结构(同树)。数据结构修改如下: 如左图的一棵二叉树用单链表就可以表示成右边的形式:   typedef struct node;   typedef node *tree;   struct node   {    char data;    tree lchild, rchild;   };   tree bt;   或:    tree lchild, rchild,father; 如左图的一棵二叉树用单链表就可以表示成右边的形式:

②顺序存储结构,即几个数组加一个指针变量。数据结构修改如下:   const int n = 10;   char data[n];   char lchild[n];   char rchild[n];   int bt; //根结点指针 二叉树在处理表达式时经常用到,一般用叶结点表示运算元,分支结点表示运算符。这样的二叉树称为表达式树。如现在有一个表达式:(a+b/c)*(d-e)。可以用以图表示:

二叉树的操作: 最重要的是遍历二叉树,但基础是建一棵二叉树、插入一个结点到二叉树中、删除结点或子树等。 数据结构定义如下: 按表达式的书写顺序逐个编号,分别为1..9,注意表达式中的所有括号在树中是不出现的,因为表达式树本身就是有序的。叶结点的左右子树均为空(用0表示)。   char data[9] = {'a', '+', 'b', '/', 'c', '*', 'd', '-', 'e'};   int lchild[9] = {0,1,0,3,0,2,0,7,0};   int rchild[9] = {0,4,0,5,0,8,0,9,0};   int bt; //根结点指针,初值=6,指向'*' 二叉树的操作: 最重要的是遍历二叉树,但基础是建一棵二叉树、插入一个结点到二叉树中、删除结点或子树等。

【例3-3】医院设置 【问题描述】 设有一棵二叉树(如图3-8,其中圈中的数字表示结点中居民的人口,圈边上数字表示结点编号。现在要求在某个结点上建立一个医院,使所有居民所走的路程之和为最小,同时约定,相邻结点之间的距离为1。就本图而言,若医院建在1处,则距离和=4+12+2*20+2*40=136;若医院建在3处,则距离和=4*2+13+20+40=81…… 【输入格式】 输入文件名为hospital.in,其中第一行一个整数n,表示树的结点数(n<=100)。接下来的n行每行描述了一个结点的状况,包含三个整数,整数之间用空格(一个或多个)分隔,其中:第一个数为居民人口数;第二个数为左链接,为0表示无链接;第三个数为右链接,为0表示无链接。 【输出格式】 输出文件名为hospital.out,该文件只有一个整数,表示最小距离和。

【样例输入】   5   13 2 3   4 0 0   12 4 5   20 0 0   40 0 0 【样例输出】   81 【算法分析】 这是一道简单的二叉树应用问题,问题中的结点数并不多,数据规模也不大,采用邻接矩阵存储,用Floyed法求出任意两结点之间的最短路径长,然后穷举医院可能建立的n个结点位置,找出一个最小

距离的位置即可。当然也可以用双链表结构或带父结点信息的数组存储结构来解决,但实际操作稍微麻烦了一点。 【参考程序】 #include <iostream> #include <cstdlib> include <cstdio> using namespace std; int a[101]; int g[101][101]; int main() { int n, i, j, k, l, r, min, total; freopen("hospital.in", "r", stdin); freopen("hospital.out", "w", stdout); cin >> n; for(i = 1 ; i <= n ; i++) for(j = 1 ; j <= n ; j++) g[i][j] = 1000000; for(i = 1 ; i <= n ; i++) //读入、初始化

{ g[i][i] = 0; cin >> a[i] >> l >> r; if(l > 0) g[i][l] = g[l][i] = 1; if(r > 0) g[i][r] = g[r][i] = 1; } for(k = 1 ; k <= n ; k++) //用Floyed求任意两结点之间的最短路径 for(i = 1 ; i <= n ; i++) if(i != k) for(j = 1 ; j <= n ; j++) if(i != j && k != j && g[i][k] + g[k][j] < g[i][j]) g[i][j] = g[i][k] + g[k][j]; min = 0x7fffffff; for(i = 1 ; i <= n ; i++) //穷举医院建在N个结点,找出最短距离 total = 0; for(j = 1 ; j <= n ; j++) total += g[i][j] * a[j]; if(total < min) min = total; cout << min << endl; return 0;

【后记】 在各种竞赛中经常遇到这样的问题:N-1条公路连接着N个城市,从每个城市出发都可以通过公路到达其它任意的城市。每个城市里面都有一定数量的居民,但是数量并不一定相等,每条公路的长度也不一定相等。X公司(或者是政府)决定在某一个城市建立一个医院/酒厂/游乐场……,问:将它建在哪里,可以使得所有的居民移动到那里的总耗费最小?这种题目都是本题的“变型”,一般称为“树的中心点问题”。除了简单的穷举法外,还有更好的时间复杂度为O(n)的算法,我们讲在后面的章节中继续讨论。

遍历二叉树 在二叉树的应用中,常常要求在树中查找具有某种特征的结点,或者对全部结点逐一进行某种处理。这就是二叉树的遍历问题。所谓二叉树的遍历是指按一定的规律和次序访问树中的各个结点,而且每个结点仅被访问一次。“访问”的含义很广,可以是对结点作各种处理,如输出结点的信息等。遍历一般按照从左到右的顺序,共有3种遍历方法,先(根)序遍历,中(根)序遍历,后(根)序遍历。 ㈠先序遍历的操作定义如下: 若二叉树为空,则空操作,否则 ①访问根结点 ②先序遍历左子树 ③先序遍历右子树 void preorder(tree bt) //先序遍历根结点为bt的二叉树的递归算法 { if(bt) cout << bt->data; preorder(bt->lchild); preorder(bt->rchild); } 先序遍历此图结果为:124753689

㈡中序遍历的操作定义如下: 若二叉树为空,则空操作,否则 ①中序遍历左子树 ②访问根结点 ③中序遍历右子树   ②访问根结点 ③中序遍历右子树    void inorder(tree bt) //中序遍历根结点为bt的二叉树的递归算法    {     if(bt)     {     inorder(bt->lchild);     cout << bt->data;     inorder(bt->rchild);     }    } 中序遍历此图结果为:742513869

㈢后序遍历的操作定义如下: 若二叉树为空,则空操作,否则 ①后序遍历左子树 ②后序遍历右子树 ③访问根结点 void postorder(tree bt) //后序遍历根结点为bt的二叉树的递归算法 { if(bt) postorder(bt->lchild); postorder(bt->rchild); cout << bt->data; } 后序遍历此图结果为:745289631

当然,我们还可以把递归过程改成用栈实现的非递归过程,以先序遍历为例,其它的留给读者完成。   void preorder(tree bt) //先序遍历bt所指的二叉树   {    tree stack[n]; //栈    int top = -1; //栈顶指针    tree P;    while(bt || top)    {    while(bt) //非叶结点    {    cout << bt->data; //访问根    stack[++top] = bt->rchild; //右子树压栈    bt = bt->lchild; //遍历左子树    }    if(top) //栈中所有元素出栈,遍历完毕    bt = stack[top--];   }    }   }

关于前面讲的表达式树,我们可以分别用先序、中序、后序的遍历方法得出完全不同的遍历结果,如对于下图中遍历结果如下,它们正好对应着表达式的3种表示方法。 -+a*b-cd/ef (前缀表示、波兰式) a+b*c-d-e/f (中缀表示) abcd-*+ef/- (后缀表示、逆波兰式)

二叉树的其它重要操作 除了“遍历”以外,二叉树的其它重要操作还有:建立一棵二叉树、插入一个结点到二叉树中、删除结点或子树等。 1、建立一棵二叉树 void pre_crt(tree &bt) //按先序次序输入二叉树中结点的值,生成 { char ch; ch = getchar(); //二叉树的单链表存储结构,bt为指向根结点的指针,'$'表示空树 if(ch != '$') bt = new node; //建根结点 bt->data = ch; pre_crt(bt->lchild); //建左子树 pre_crt(bt->rchild); //建右子树 } else bt = NULL;

2、删除二叉树 3.插入一个结点到排序二叉树中 void dis(tree &bt) //删除二叉树 { if(bt) dis(bt->lchild); //删左子树 dis(bt->rchild); //删右子树 delete bt; //释放父结点 } 3.插入一个结点到排序二叉树中 void insert(tree &bt, int n) //插入一个结点到排序二叉树中 if(n < bt->data) insert(bt->lchild, n); else if(n > bt->data) insert(bt->rchild, n); else bt = new node; //新开一个空间 bt->data = n; bt->lchild = bt->rchild = NULL;

5.用嵌套括号表示法输出二叉树 4.在排序二叉树中查找一个数,找到返回该结点,否则返回NULL tree findn(tree bt, int n) //在二叉树中查找一个数,找到返回该结点,否则返回NULL。 { if(bt) if(n < bt->data) findn(bt->lchild, n); else if(n > bt->data) findn(bt->rchild, n); else return bt; } else return NULL; 5.用嵌套括号表示法输出二叉树 void print(tree bt) //用嵌套括号表示法输出二叉树 cout << bt->data; if(bt->lchild || bt->rchild) cout << '('; print(bt->lchild); if(bt->rchild) cout << ',';

print(bt->rchild); cout << ')'; } 下面我们换个角度考虑这个问题,从二叉树的遍历已经知道,任意一棵二叉树结点的先序序列和中序序列是唯一的。反过来,给定结点的先序序列和中序序列,能否确定一棵二叉树呢?又是否唯一呢? 由定义,二叉树的先序遍历是先访问根结点,再遍历左子树,最后遍历右子树。即在结点的先序序列中,第一个结点必是根,假设为root。再结合中序遍历,因为中序遍历是先遍历左子树,再访问根,最后遍历右子树。所以结点root正好把中序序列分成了两部分,root之前的应该是左子树上的结点,root之后的应该是右子树上的结点。依次类推,便可递归得到整棵二叉树。

结论:已知先序序列和中序序列可以确定出二叉树;    已知中序序列和后序序列也可以确定出二叉树; 但,已知先序序列和后序序列却不可以确定出二叉树;为什么?举个3个结点的反例。 例如:已知结点的先序序列为ABCDEFG,中序序列为CBEDAFG。构造出二叉树。过程见下图:

【例3-4】求后序遍历 【问题描述】   输入一棵二叉树的先序和中序遍历序列,输出其后序遍历序列。 【输入格式】   输入文件为tree.in,共两行,第一行一个字符串,表示树的先序遍 历,第二行一个字符串,表示树的中序遍历。树的结点一律用小写 字母表示。 【输出格式】   输出文件为tree.out,仅一行,表示树的后序遍历序列。 【样例输入】   abdec   dbeac 【样例输出】   debca

【参考程序】 #include <cstring> #include <iostream> #include <cstdio> using namespace std; string s1, s2; void calc(int l1, int r1, int l2, int r2) { int m = s2.find(s1[l1]); if(m > l2) calc(l1 + 1, l1 + m - l2, l2, m - 1); if(m < r2) calc(l1 + m - l2 + 1, r1, m + 1, r2); cout << s1[l1]; } int main() freopen("tree.in", "r", stdin); freopen("tree.out", "w", stdout); cin >> s1 >> s2; calc(0, s1.length() - 1, 0, s2.length() - 1); cout << endl; return 0;

【例3-5】扩展二叉树 【问题描述】 由于先序、中序和后序序列中的任一个都不能唯一确定一棵二叉树,所以对二叉树做如下处理,将二叉树的空结点用·补齐,如图所示。我们把这样处理后的二叉树称为原二叉树的扩展二叉树,扩展二叉树的先序和后序序列能唯一确定其二叉树。 现给出扩展二叉树的先序序列,要求输出其中序和后序序列。 【输入样例】tree_b.in ABD..EF..G..C.. 【输出样例】tree_b.out DBFEGAC DFGEBCA

#include <iostream> #include <stdlib.h> #include <string> #include <cstring> #include <algorithm> #include <cstdio> using namespace std; typedef struct node; typedef node *tree; struct node { char data; tree lchild, rchild; }; tree bt; int i; string s; void build(tree &bt) //建树 if(s[++i]!='.') bt = new node;

bt = new node; bt->data = s[i]; build(bt->lchild); build(bt->rchild); } else bt = NULL; void printzx(tree bt) //输出中序序列 { if(bt) printzx(bt->lchild); cout << bt->data; printzx(bt->rchild); void printhx(tree bt) //输出后序序列 printhx(bt->lchild); printhx(bt->rchild);

int main() { freopen("tree_b.in", "r", stdin); freopen("tree_b.out", "w", stdout); cin >> s; i = -1; build(bt); printzx(bt); cout << endl; printhx(bt); return 0; }

普通树转换成二叉树 由于二叉树是有序的,而且操作和应用更广泛,所以在实际使用时,我们经常把普通树转换成二叉树进行操作。如何转换呢?一般方法如下: 1.将树中每个结点除了最左边的一个分支保留外,其余分支都去掉; 2.从最左边结点开始画一条线,把同一层上的兄弟结点都连起来; 3.以整棵树的根结点为轴心,将整棵树顺时针大致旋转45度。 第一节中的图A所示的普通树转换成二叉树的过程如图B所示:

同样我们可以把森林也转换成二叉树处理,假设F={T1,T2,…,Tm}是森林,则可按如下规则转换成一棵二叉树B=(root,lb,rb)。 1.若F为空,即m=0,则 B为空树; 2.若F非空,即m!=0,则B的根root即为森林中第一棵树的根root(T1);B的左子树lb是从T1中根结点的子树森林F1={T11,T12,…,T1m1}转换而成的二叉树;其右子树rb是从森林F’={T2,T3,…,Tm}转换而成的二叉树。

树的计数问题 具有n个结点的不同形态的二叉树有多少棵?具有n个结点的不同形态的树有多少棵?首先了解两个概念,“相似二叉树”是指两者都为空树或者两者均不为空树,且它们的左右子树分别相似。“等价二叉树”是指两者不仅相似,而且所有对应结点上的数据元素均相同。二叉树的计数问题就是讨论具有n个结点、互不相似的二叉树的数目Bn。 在n很小时,很容易得出,B0=1,B1=1,B2=2,B3=5(下图)。

一般情况,一棵具有n(n>1)个结点的二叉树可以看成是由一个根结点、一棵具有i个结点的左子树和一棵具有n-i-1个结点的右子树组成,其中0<=i<=n-1, 由此不难得出下列递归公式: 我们可以利用生成函数讨论这个递归公式,得出:Bn= 类推到具有n个结点、互不相似的多叉树的数目Tn 由于树可以转换成二叉树且转换之后的根节点没有右儿子,所以,可以推出:Tn=Bn-1。

【课堂练习】 1、一棵完全二叉树的结点总数为18,其叶结点数为 。 A.7个 B.8个 C.9个 D.10个 1、一棵完全二叉树的结点总数为18,其叶结点数为 。    A.7个 B.8个 C.9个 D.10个 2、 二叉树第10层的结点数的最大数目为 。    A.10 B.100 C.512 D.1024 3、一棵深度为K的满二叉树有( )个结点。    A.2K-1 B.2K C.2*K D.2*K-1 4、对任何一棵二叉树T,设n0、n1、n2分别是度数为0、1、2的顶点数,则下列判断中正确的是 。 A.n0=n2+1 B.n1=n0+1 C. n2=n0+1 D.n2=n0+1 5、一棵n个节点的完全二叉树,则该二叉树的高度h为( )。    A.n/2 B.log(n) C.log(n)/2 D. 6、一棵完全二叉树上有1001个结点,其中叶子结点的个数是 。    A.250 B.500 C.254 D.501

7、如果一棵二叉树有N个度为2的节点,M个度为1的节点,则该树的叶子个数为 。    A. N+1 B. 2 * N-1 C.N-1 D. M+N-1 8、一棵非空二叉树的先序遍历序列和后序遍历序列正好相反,则该二叉树一定满足 。    A.所有结点均无左孩子 B.所有的结点均无右孩子    C.只有一个叶子结点 D.是任意一棵二叉树 9、将有关二叉树的概念推广到三叉树,则一棵有244个结点的完全三叉树的高度是 。    A.4 B.5 C.6 D.7 10、在一棵具有K层的满三叉树中,结点总数为 。    A.(3k-1)/2 B.3k-1 C.(3k-1)/3 D.3k 11、设树T的高度为4,其中度为1、2、3和4的结点个数分别为4、2、1、1,则T中的叶子数为 。    A.5 B.6 C.7 D.8

12、一棵树T有2个度数为2的结点、有1个度数为3的结点、有3个度数为4的结点,那么树T有( )个树叶。    A.14 B.6 C.18 D.7 13、某二叉树中序序列为abcdefg,后序序列为bdcafge,则前序序列是 。    A.egfacbd B.eacbdgf C.eagcfbd D.以上答案都不对 14、已知某二叉树的中序遍历序列是debac,后序遍历序列是dabec,则它的前序遍历序列是 。 A.a c b e d B.d e c a b C.d e a b c D.c e d b a 15、一颗二叉树的中序遍历序列为DGBAECHF,后序遍历序列为GDBEHFCA,则前序遍历序列是 。    A. ABCDFGHE B. ABDGCEFH C. ACBGDHEF D. ACEFHBGD 16、已知一棵二叉树的前序序列为ABDEGCFH,中序序列为DBGEACHF,则该二叉树的层次序列为 。 A.GEDHFBCA B.DGEBHFCA C.ABCDEFGH D.ACBFEDHG

17、已知一棵二叉树的前序遍历结果为ABDECFHJIG,中序遍历的结果为DBEAJHFICG,则这棵二叉树的深度为 。    A.3 B.4 C.5 D.6 18、二叉树的先序遍历和中序遍历如下:先序遍历:EFHIGJK,中序遍历:HFIEJKG。   该二叉树根的右子树的根是__________。    A. E B. F C. G D.H 19、中缀表达式A-(B+C/D)*E的后缀形式是 。 A.AB-C+D/E* B.ABC+D/-E* C.ABCD/E*+- D.ABCD/+E*- 20、设森林F对应的二叉树为B,它有M个结点,B的根为P,P的右子树结点个数为N,森林F中第一棵树的结点个数是 。    A.M-N B.M-N-1 C.N+1 D.条件不充分,无法确定

【练习答案】 1.C 2.C 3.A 4.A 5.D 6.D 7.A 8.A 9.B 10.A 11.D 12.A 13.B 14.D 15.B 16.C 17.C 18.C 19.D 20.A 7、此树的边数为2N+M,故而总共有2N+M+1个顶点。除去度为1、2的顶点,度为0的节点(即叶子节点)有(2N+M+1)-(N+M)=N+1。答案:A 12、设树T有n个结点,m条边。边数为结点的度数之和,   即m=2×2+1×3+3×4=19,n=m+1=20。n个结点中   有1+2+3=6个分支结点,有叶结点20-6=14个。答案:A 15、中序遍历DGBAECHF和后序遍历GDBEHFCA唯一对应   一棵二叉树前序遍历为ABDGCEFH。答案:B

16、由前序序列和中序序列可以唯一地确定一棵二叉树,根据题设中的前序序列和中序序列确定的二叉树为: 由此可见,其层次序列为ABCDEFGH。答案:C 17、由题目给出二叉树先序遍历结点的顺序:ABDECFHJIG可知结点A为二叉树的根结点。再由中序遍历的结点顺序:DBEAJHFICG可知结点A前的DBE为根结点A的左子树的结点,而JHFICG为根结点A的右子树的结点。

先来看A结点的左子树的结点DBE。在先序遍历顺序中,B结点在A结点之后,说明B结点是左子树的根结点,D结点又在B结点之后,则D是B的左子树的根结点。结点E由中序遍历的顺序可知是B的右子树的根结点。同样求出右子树JHFICG,如下图。 由图可知,二叉树的深度为5。答案:C。 19、该题答案是(D),本题主要考察学生怎样将中缀表达式 转换为后缀表达式。可以先画出该二叉树,对其进行后根遍历 即为答案。

【上机练习】 1、小球(DROP) 【问题描述】 许多的小球一个一个的从一棵满二叉树上掉下来组成FBT(Full Binary Tree,满二叉树),每一时间,一个正在下降的球第一个访问的是非叶子节点。然后继续下降时,或者走右子树,或者走左子树,直到访问到叶子节点。决定球运动方向的是每个节点的布尔值。最初,所有的节点都是FALSE,当访问到一个节点时,如果这个节点是FALSE,则这个球把它变成TRUE,然后从左子树走,继续它的旅程。如果节点是TRUE,则球也会改变它为FALSE,而接下来从右子树走。满二叉树的标记方法如下图。

因为所有的节点最初为FALSE,所以第一个球将会访问节点1,节点2和节点4,转变节点的布尔值后在在节点8停止。第二个球将会访问节点1、3、6,在节点12停止。明显地,第三个球在它停止之前,会访问节点1、2、5,在节点10停止。   现在你的任务是,给定FBT的深度D,和I,表示第I个小球下落,你可以假定I不超过给定的FBT的叶子数,写一个程序求小球停止时的叶子序号。 【输入格式】 输入文件仅一行包含两个用空格隔开的整数D和I。其中2<=D<=20,1<=I<=524288。 【输出格式】 对应输出第I个小球下落停止时的叶子序号。 【输入样例】DROP.IN   4 2 【输出样例】DROP.OUT    12

2、二叉树遍历(flist) 【问题描述】   树和二叉树基本上都有先序、中序、后序、按层遍历等遍历顺序,给定中序和其它一种遍历的序列就可以确定一棵二叉树的结构。   假定一棵二叉树一个结点用一个字符描述,现在给出中序和按层遍历的字符串,求该树的先序遍历字符串。 【输入格式】 输入文件flist.in共两行,每行是由字母组成的字符串(一行的每个字符都是唯一的),分别表示二叉树的中序遍历和按层遍历的序列。 【输出格式】 输出文件flist.out就一行,表示二叉树的先序序列。 【输入样例】flist.in   DBEAC   ABCDE 【输出样例】flist.out   ABDEC

3、FBI树(fbi) 【问题描述】 我们可以把由“0”和“1”组成的字符串分为三类:全“0”串称为B串,全“1”串称为I串,既含“0”又含“1”的串则称为F串。 FBI树是一种二叉树[ 二叉树:二叉树是结点的有限集合,这个集合或为空集,或由一个根结点和两棵不相交的二叉树组成。这两棵不相交的二叉树分别称为这个根结点的左子树和右子树。],它的结点类型也包括F结点,B结点和I结点三种。由一个长度为2N的“01”串S可以构造出一棵FBI树T,递归的构造方法如下: T的根结点为R,其类型与串S的类型相同; 若串S的长度大于1,将串S从中间分开,分为等长的左右子串S1和S2;由左子串S1构造R的左子树T1,由右子串S2构造R的右子树T2。 现在给定一个长度为2N的“01”串,请用上述构造方法构造出一棵FBI树,并输出它的后序遍历[ 后序遍历:后序遍历是深度优先遍历二叉树的一种方法,它的递归定义是:先后序遍历左子树,再后序遍历右子树,最后访问根。]序列。

【输入文件】 输入文件fbi.in的第一行是一个整数N(0 <= N <= 10),第二行是一个长度为2N的“01”串。 【输出文件】 输出文件fbi.out包括一行,这一行只包含一个字符串,即FBI树的后序遍历序列。 【样例输入】fbi.in   3   10001011 【样例输出】fbi.out   IBFBBBFIBFIIIFF 【数据规模】   对于40%的数据,N <= 2;   对于全部的数据,N <= 10。

4、二叉树输出(btout) 【问题描述】 树的凹入表示法主要用于树的屏幕或打印输出,其表示的基本思想是兄弟间等长,一个结点要不小于其子结点的长度。二叉树也可以这样表示,假设叶结点的长度为1,一个非叶结点的长并等于它的左右子树的长度之和。 一棵二叉树的一个结点用一个字母表示(无重复),输出时从根结点开始: 每行输出若干个结点字符(相同字符的个数等于该结点长度), 如果该结点有左子树就递归输出左子树; 如果该结点有右子树就递归输出右子树。 假定一棵二叉树一个结点用一个字符描述,现在给出先序和中序遍历的字符串,用树的凹入表示法输出该二叉树。

【输入格式】 输入文件btout.in共两行,每行是由字母组成的字符串(一行的每个字符都是唯一的),分别表示二叉树的先序遍历和中序遍历的序列。 【输出格式】 输出文件btout.out的行数等于该树的结点数,每行的字母相同。 【输入样例】btout.in   ABCDEFG   CBDAFEG 【输出样例】btout.out   AAAA   BB   C   D   EE   F   G

5、查找二叉树 【问题描述】 已知一棵二叉树用邻接表结构存储,中序查找二叉树中值为x的结点,并指出是第几个结点。例:如图二叉树的数据文件的数据格式如下 7 15 5 2 3 12 4 5 10 0 0 29 0 0 15 6 7 8 0 0 23 0 0   第一行n为二叉树的结点个树,n<=100;第二行x表示要查找的结点的值;以下第一列数据是各结点的值,第二列数据是左儿子结点编号,第三列数据是右儿子结点编号。   输出一个数即查找的结点编号。 本例的输出样例: 4

6、对称二叉树 【问题描述】 如果二叉树的左右子树的结构是对称的,即两棵子树皆为空,或者皆不空,则称该二叉树是对称的。编程判断给定的二叉树是否对称. 例:如下图中的二叉树T1是对称的,T2是不对称的。     二叉树用顺序结构给出,若读到#则为空,二叉树T1=ABCDE,T2=ABCD#E,如果二叉树是对称的,输出“Yes”,反之输出“No”。 【输入样例】tree_c.in    ABCDE 【输出样例】tree_c.out    Yes