第7天树
树是n(n≥0)个结点的有限集。在任意一棵非空树中:
(1)有且仅有一个特定的称为根的结点;
(2)当n>1时,其余结点可分为m(m>0)个互不相交的有限集T1,T2,…,Tm,
其中每一个集合本身又是一棵树,并且称为根的子树。
数据对象 D:
数据关系 R:
若D为空集,则称为空树 。
否则:
(1) 在D中存在唯一的称为根的数据元素root;
(2) 当n>1时,其余结点可分为m (m>0)个互不相交的有限集T1, T2, …, Tm,其中每一 棵子集本身又是一棵符合本定义的树,称为根root的子树。
基本操作:
1插 入 类
2查 找 类
3删 除 类
查找类:
Root(T) // 求树的根结点
Value(T, cur_e) // 求当前结点的元素值
Parent(T, cur_e) // 求当前结点的双亲结点
LeftChild(T cur_e) // 求当前结点的最左孩子
RightSibling(T, cur_e) // 求当前结点的右兄弟
TreeEmpty(T) // 判定树是否为空树
TreeDepth(T) // 求树的深度
TraverseTree( T, Visit() ) // 遍历
插入类:
InitTree(&T) // 初始化置空树
CreateTree(&T, definition) // 按定义构造树
Assign(T, cur_e, value) // 给当前结点赋值
InsertChild(&T, &p, i, c) // 将以c为根的树插入为结点p的第i棵子树
删除类:
ClearTree(&T) // 将树清空
DestroyTree(&T) // 销毁树的结构
DeleteChild(&T, &p, i) // 删除结点p的第i棵子树
对比树型结构和线性结构的结构特点
线性结构
第一个数据元素 (无前驱)
最后一个数据元素 (无后继)
其它数据元素 (一个前驱、 一个后继)
树型结构
根结点 (无前驱)
多个叶子结点 (无后继)
其它数据元素 (一个前驱、 多个后继)
基本术语 :
结点:数据元素及若干指向子树的分支
树的度:树中所有结点的度的最大值
叶子结点:度为零的结点
分支结点:度大于零的结点
结点的层次:假设根结点的层次为1,第l 层的结点的子树根结点的层次为l+1
树的深度:树中叶子结点所在的最大层次
任何一棵非空树是一个二元组Tree = (root,F)
其中:root 被称为根结点,F 被称为子树森林
二叉树的五种基本形态:空树,只含根结点,左右子树均不为空树,右子树为空树 左子树为空树
6.2 二叉树
二叉树或为空树,或是由一个根结点加上两棵分别称为左子树和右子树的、互不交的二叉树组成。
二叉树的五种基本形态:空树,只含根结点,左右子树均不为空树,右子树为空树 左子树为空树
性质:
性质 1:在二叉树的第 i 层上至多有个结点。 (i≥1)
性质 2 :深度为 k 的二叉树上至多含 个结点(k≥1)。
性质 3 :对任何一棵二叉树,若它含有n0 个叶子结点、n2 个度为 2 的结点,则必存在关系式:n0 = n2+1
设二叉树上结点总数n = n0 + n1 + n2
又二叉树上分支总数 b = n1+2n2而 b = n-1 = n0 + n1 + n2 - 1
由此, n0 = n2 + 1 。
满二叉树:指的是深度为k且含有个结点的二叉树。
完全二叉树:树中所含的 n 个结点和满二叉树中编号为 1 至 n 的结点一一对应。
性质 4 :具有 n 个结点的完全二叉树的深度为。
性质 5 :若对含 n 个结点的完全二叉树从上到下且从左至右进行 1 至 n 的编号,则对完全二叉树中任意一个编号为 i 的结点:
(1) 若 i=1,则该结点是二叉树的根,无双亲,否则,编号为 i/2 的结点为其双亲结点;(2) 若 2i>n,则该结点无左孩子,否则,编号为 2i 的结点为其左孩子结点;(3) 若 2i+1>n,则该结点无右孩子结点,否则,编号为2i+1 的结点为其右孩子结点。
二叉树的存储结构:
一、 二叉树的顺序存储表示
二、二叉树的链式存储表示
一、 二叉树的顺序存储表示
#define MAX_TREE_SIZE 100
// 二叉树的最大结点数
typedef TElemType SqBiTree[MAX_TREE_SIZE];
// 0号单元存储根结点 SqBiTree bt;
二、二叉树的链式存储表示
1. 二叉链表
2.三叉链表
C 语言的类型描述如下
1 typedef struct BiTNode {
2 // 结点结构
3 TElemType data;
4 struct BiTNode *lchild, *rchild;
5 // 左右孩子指针
6 } BiTNode, *BiTree;
结点结构:
- 三叉链表 parent lchild data rchild
1 typedef struct TriTNode { // 结点结构
2 TElemType data;
3 struct TriTNode *lchild, *rchild; // 左右孩子指针
4 struct TriTNode *parent; //双亲指针
5 } TriTNode, *TriTree;
6
6.3 二叉树的遍历
对“二叉树”而言,可以有三条搜索路径:
1.先上后下的按层次遍历;
2.先左(子树)后右(子树)的遍历;
3.先右(子树)后左(子树)的遍历。
二、按层次遍历二叉树
实现方法为从上层到下层,每层中从左侧到右侧依次访问每个结点。
下面我们将给出一棵二叉树及其按层次顺序访问其中每个结点的遍历序列。
三、先左后右的遍历算法
先(根)序的遍历算法
中(根)序的遍历算法
后(根)序的遍历算法
四、算法的递归描述
先(根)序的遍历算法
若二叉树为空树,则空操作;否则,
(1)访问根结点;
(2)先序遍历左子树;
(3)先序遍历右子树
1 void Preorder (BiTree T, void( *visit)(TElemType& e)) {
2
3 // 先序遍历二叉树
4 if (T) {
5 visit(T->data); // 访问结点
6 Preorder(T->lchild, visit); // 遍历左子树
7 Preorder(T->rchild, visit);// 遍历右子树
8 }
9
10 }
中(根)序的遍历算法:
若二叉树为空树,则空操作;否则,
(1)中序遍历左子树;
(2)访问根结点;
(3)中序遍历右子树。
1 void Inorder (BiTree T, void( *visit)(TElemType& e)) {
2
3 // 中序遍历二叉树
4 if (T) {
5 Inreorder(T->lchild, visit); // 遍历左子树
6 visit(T->data); // 访问结点
7 Inreorder(T->rchild, visit); // 遍历右子树
8 }
9
10 }
11
后(根)序的遍历算法:
若二叉树为空树,则空操作;否则,
(1)后序遍历左子树;
(2)后序遍历右子树;
(3)访问根结点。
1 void Postorder (BiTree T, void( *visit)(TElemType& e)) {
2 // 后序遍历二叉树
3 if (T) {
4 Postreorder(T->lchild, visit); // 遍历左子树
5 Postreorder(T->rchild, visit); // 遍历右子树
6 visit(T->data); // 访问结点
7 }
8
9 }
四:遍历算法的应用举例
1、输入结点值,构造二叉树 (先序遍历)
2、统计二叉树中叶子结点的个数 (先序遍历)
3、求二叉树的深度 (后序遍历)
1、输入结点值,构造二叉树
算法基本思想:
先序(或中序或后序)遍历二叉树, 读入一个字符,若读入字符为空,则二叉树为空,若读入字符非空,则生成一个结点。
将算法中“访问结点”的操作改为:生成一个结点,输入结点的值
1 Status CreateBiTree (BiTree &T)
2 { scanf( &ch ) ;
3 if (ch==’ ’) T=NULL;
4 else{
5 if(!(T=(BiTNode *)malloc(sizeof(BiTNode))) exit(OVERFLOW);
6 T->data=ch; //生成根结点
7 CreateBiTree( T->lchild); //构造左子树
8 CreateBiTree( T->rchild); //构造右子树
9 }
10 return(OK);
11 } // CreateBiTree
2、统计二叉树中叶子结点的个数
算法基本思想:
先序(或中序或后序)遍历二叉树,在遍历过程中查找叶子结点,并计数。
由此,需在遍历算法中增添一个“计数”的参数,并将算法中“访问结点”的操作改为:若是叶子,则计数器增1。
1 void CountLeaf (BiTree T, int& count){
2 if ( T ) {
3 if ((!T->lchild)&& (!T->rchild))
4 count++; // 对叶子结点计数
5 CountLeaf( T->lchild, count);
6 CountLeaf( T->rchild, count);
7 } // if
8 } // CountLeaf
3、求二叉树的深度(后序遍历)
算法基本思想:
首先分析二叉树的深度和它的左、右子树深度之间的关系。
从二叉树深度的定义可知,二叉树的深度应为其左、右子树深度的最大值加1。
由此,需先分别求得左、右子树的深度,算法中“访问结点”的操作改为:求得左、右子树深度的最大值,然后加1 。
1 int Depth (BiTree T ){ // 返回二叉树的深度
2 if ( !T ) depthval = 0;
3 else {
4 depthLeft = Depth( T->lchild );
5 depthRight= Depth( T->rchild );
6 depthval = 1 + (depthLeft > depthRight ?depthLeft : depthRight);
7 }
8 return depthval;
9 }
线索二叉树
一、何谓线索二叉树?
遍历二叉树的结果是, 求得结点的一个线性序列。
例如: 先序序列: A B C D E F G H K
中序序列: B D C A H G K F E
后序序列: D C B H K G F E A
指向该线性序列中的“前驱”和 “后继” 的指针,称作“线索“
包含 “线索” 的存储结构,称作 “线索链表”
与其相应的二叉树,称作 “线索二叉树”
对线索链表中结点的约定:
在二叉链表的结点中增加两个标志域, 并作如下规定:
若该结点的左子树不空, 则Lchild域的指针指向其左子树, 且左标志域的值为“指针 Link”或0;
否则,Lchild域的指针指向其“前驱”, 且左标志的值为“线索 Thread”或1 。
若该结点的右子树不空, 则rchild域的指针指向其右子树, 且右标志域的值为 “指针 Link”或0;
否则,rchild域的指针指向其“后继”, 且右标志的值为“线索 Thread”或1
如此定义的二叉树的存储结构称作“线索链表”
ltag和rtag是增加的两个标志域,用来区分结点的左、右指针域是指向其左、右孩子的指针,还是指向其前驱或后继的线索。
线索链表的类型描述:
1 typedef enum { Link, Thread } PointerThr;
2 // Link==0:指针,Thread==1:线索
3 typedef struct BiThrNod {
4 TElemType data;
5 struct BiThrNode *lchild, *rchild; // 左右指针
6 PointerThr LTag, RTag; // 左右标志
7 } BiThrNode, *BiThrTree;
8
二、线索链表的遍历算法:
由于在线索链表中添加了遍历中得到的“前驱”和“后继”的信息,从而简化了遍历的算法。
例如: 对中序线索化链表的遍历算法
中序遍历的第一个结点 ?
左子树上处于“最左下”(没有左子树)的结点。
在中序线索化链表中结点的后继 ?
若无右子树,则为后继线索所指结点; 否则为对其右子树进行中序遍历时访问的第一个结点。
1 void InOrderTraverse_Thr(BiThrTree T, void (*Visit)(TElemType e)) {
2 p = T->lchild; // p指向根结点
3 while (p != T) { // 空树或遍历结束时,p==T
4 while (p->LTag==Link) p = p->lchild; // 第一个结点
5 Visit(p->data);
6 while (p->RTag==Thread && p->rchild!=T)
7 { p = p->rchild; Visit(p->data); } // 访问后继结点
8 p = p->rchild; // p进至其右子树根
9 }
10 } // InOrderTraverse
三、如何建立线索链表?
在中序遍历过程中修改结点的左、 右指针域,以保存当前访问结点的“前 驱”和“后继”信息。
遍历过程中,附设 指针pre, 并始终保持指针pre指向刚刚访问过的结点,若指针p所指向当前访问的结点,则pre指向p的前驱。
1 Status InOrderThreading(BiThrTree &Thrt,BiThrTree T) { 2 // 构建中序线索链表 3 if (!(Thrt = (BiThrTree)malloc(sizeof( BiThrNode)))) 4 exit (OVERFLOW); //建头结点 5 Thrt->LTag = Link; 6 Thrt->RTag =Thread; 7 Thrt->rchild = Thrt; //右指针回指 8 if (!T) Thrt->lchild = Thrt; //若二叉树为空左指针回指 9 else { 10 Thrt->lchild = T; 11 pre = Thrt; 12 InThreading(T); //中序遍历进行中序线索化 13 pre->rchild = Thrt; 14 pre->RTag = Thread; //最后一个结点线索化 15 Thrt->rchild = pre; } 16 return OK; 17 } // InOrderThreading
1 void InThreading(BiThrTree p) {
2 if (p) { // 对以p为根的非空二叉树进行线索化
3 InThreading(p->lchild); // 左子树线索化
4 if (!p->lchild) // 建前驱线索
5 { p->LTag = Thread; p->lchild = pre; }
6 if (!pre->rchild) // 建后继线索
7 { pre->RTag = Thread; pre->rchild = p; }
8 pre = p; // 保持 pre 指向 p 的前驱
9 InThreading(p->rchild); // 右子树线索化
10 } // if
6.4 树和森林 的表示方法
树的三种存储结构
一、双亲表示法
二、孩子链表表示法
三、树的二叉链表(孩子-兄弟)存储表示法
一、双亲表示法:
结点结构:
1 #define MAX_TREE_SIZE 100
2 typedef struct PTNode {
3 Elem data;
4 int parent; // 双亲位置域
5 } PTNode;
树结构:
1 typedef struct { 2 PTNode nodes [MAX_TREE_SIZE]; 3 int r, n; // 根结点的位置和结点个数 4 } PTree;
二、孩子链表表示法:
孩子结点结构:
1 typedef struct CTNode {
2 int child;
3 struct CTNode *next;
4 } *ChildPtr;
双亲结点结构:
1 typedef struct {
2 Elem data;
3 ChildPtr firstchild;
4 // 孩子链的头指针
5 } CTBox;
1 typedef struct CSNode{
2 Elem data;
3 struct CSNode
4 *firstchild, *nextsibling;
5 } CSNode, *CSTree;
树和森林的遍历
一、树的遍历
二、森林的遍历
树的遍历可有两条搜索路径:
1 先根(次序)遍历:若树不空,则先访问根结点,然后依次先根遍历各棵子树。
2 后根(次序)遍历:若树不空,则先依次后根遍历各棵子树,然后访问根结点。
森林的遍历
1. 先序遍历 若森林不空,则
访问森林中第一棵树的根结点;
先序遍历森林中第一棵树的子树森林;
先序遍历森林中(除第一棵树余树构成的森林。
即:依次从左至右对森林中的每一棵树进行先根遍历。
2.中序遍历
若森林不空,则 中序遍历森林中第一棵树的子树森林; 访问森林中第一棵树的根结点; 中序遍历森林中(除第一棵树之外)余树构成的森林。
即:依次从左至右对森林中的每一棵树进行后根遍历。
6.6 哈 夫 曼 树 与 哈 夫 曼 编 码
一、最优树的定义
树的带权路径长度定义为: 树中所有叶子结点的带权路径长度之和 WPL(T) = wklk (对所有叶子结点)。
在所有含 n 个叶子结点、并带相同权值的 m 叉树中,必存在一棵其带权路径长度取最小值的树,称为“最优树”。
二、如何构造最优树
(哈夫曼算法) 以二叉树为例:
(1)根据给定的 n 个权值 {w1, w2, …, wn},构造 n 棵二叉树的集合 F = {T1, T2, … , Tn}, 其中每棵二叉树Ti中均只含一个带权值为 wi 的根结点,其左、右子树为空树;
(2)在 F 中选取其根结点的权值为最小的两棵二叉树,分别作为左、 右子树构造一棵新的二叉树,并 置这棵新的二叉树根结点的权值 为其左、右子树根结点的权值之 和;
(3)从F中删去这两棵树,同时加入 刚生成的新树;
(4)重复 (2) 和 (3) 两步,直至 F 中只 含一棵树为止。这棵树便是哈夫 曼树
前缀编码
在电文传输中,需要将电文中出现的每个字符进行二进制编码。
在设计编码时需要遵守两个原则:
(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,并从根到叶子方向形成该叶子 结点的编码。
哈夫曼树的存储结构
用一个大小为2n-1的向量来存储哈夫曼树中 的结点,其存储结构为:
typedef struct { //结点类型
int weight; //权值,不妨设权值均大于零
int lchild,rchild,parent; //左右孩子及双亲指针
}HTNode,*HuffmanTree;
厚积薄发,行胜于言@飞鸟各投林