树和二叉树
树的定义
树(Tree)是n(n≥0)个结点的有限集。
若n=0,称为空树;
若n>0,则它满足如下两个条件:
- 有且仅有一个称之为根(root)的结点;
- 除根结点以外的其余结点可分为m(m>0)个互不相交的有限集T1, T2, …, Tm,其中每一个集合本身又是一棵树,并且称为根的子树(SubTree)。
树的基本术语
结点:数据元素+若干指向子树的分支
结点的度:分支的个数
树的度:树中所有结点的度的最大值
叶子结点:度为0的结点,终端结点
分支结点:度大于0的结点,非终端结点
双亲:上层的那个结点(直接前驱)
孩子:下层结点的子树的根(直接后继)
兄弟:同一双亲下的同层结点
堂兄弟:双亲位于同一层的结点(非同一双亲)
祖先:从根到该结点所经分支的所有结点
子孙:该结点下层子树中的任一结点
路径:由从根到该结点所经分支和结点构成
结点的层次:从根到该结点的层数(根结点算第一层)
有序树:子树之间存在确定的次序关系。
无序树:子树之间不存在确定的次序关系。
树的深度:指所有结点中最大的层数
森林:是m(m≥0)棵互不相交的树的集合
任何一棵非空树是一个二元组
Tree = (root,F) 其中:root 被称为根结点 F 被称为子树森林
二叉树
二叉树的定义
二叉树(Binary Tree)是n(n≥0)个结点所构成的集合,它或为空树(n = 0);或者由一个根节点及两颗互不相交的分别称作这个根的左子树和右子树的二叉树组成。
特点
- 每个结点最多有两孩子(二叉树中不存在度大于2的结点)
- 有序树(子树有序,不能颠倒)
二叉树的五种基本形态
二叉树的抽象类型定义
二叉树的基本操作
二叉树的性质
性质3: 对任何一棵二叉树,若它含有n0 个叶子结点、n2 个度为 2 的结点,则必存在关系式:n0 = n2+1
满二叉树
指的是深度为k且含有2k-1个结点的二叉树
特点:
1、每一层上的结点数都是最大结点数(即每层都满)
2、叶子节点全部在最底层
完全二叉树
树中所含的 n 个结点和满二叉树中编号为 1 至 n 的结点一一对应
特点
1、叶子只可能分布在层次最大的两层上。
2、对任意结点,如果其右子树的最大层次为i,
性质5:若对含 n 个结点的完全二叉树从上到下且从左至右进行 1 至 n 的编号,则对完全二叉树中任意一个编号为 i 的叶结点:
若 i=1,则该结点是二叉树的根,无双亲,否则,编号为 ⌊ i/2 ⌋ 的结点为其双亲结点;
若 2i>n,则该结点无左孩子,否则,编号为 2i 的结点为其左孩子结点;
若 2i+1>n,则该结点无右孩子结点,否则,编号为2i+1 的结点为其右孩子结点。
二叉树的存储结构
二叉树的顺序存储
实现:按满二叉树的结点层次编号,依次存放二叉树中的数据元素。
#define MAX_TREE_SIZE 100 // 二叉树的最大结点数
typedef TElemType SqBiTree[MAX_TREE_SIZE];
// 0号单元存储根结点
SqBiTree bt;
特点
1、结点间关系蕴含在其存储位置中
2、浪费空间,适于存满二叉树和完全二叉树
二叉树的链式存储
二叉链表
结点结构:
C语言描述
typedef struct BiTNode { // 结点结构
TElemType data;
struct BiTNode *lchild, *rchild; // 左右孩子指针
} BiTNode, *BiTree;
空指针数目
空指针数目= n+1
三叉链表
结构
C 语言的类型描述如下:
typedef struct TriTNode { // 结点结构
TElemType data;
struct TriTNode *lchild, *rchild; // 左右孩子指针
struct TriTNode *parent; //双亲指针
} TriTNode, *TriTree;
遍历二叉树
遍历: 顺着某一条搜索路径巡访二叉树中的结点,使得每个结点均被访问一次,而且仅被访问一次。
访问的含义很广,可以是对结点作各种处理,如:输出结点的信息、修改结点的数据值等,但要求这种访问不破坏原来的数据结构。
遍历目的: 得到数中所有节点的一个线性排列
遍历用途: 他是树结构插入、删除、修改、查找和排序运算的前提,是二叉树一切运算的基础和核心。
先序遍历(DLR)
先序遍历操作
若二叉树为空,则空操作
否则:访问根结点 (D)
前序遍历左子树 (L)
前序遍历右子树 (R )
先序遍历的算法描述
Status PreOrderTraverse(BiTree T){
if(T==NULL) return OK; //空二叉树
else{
visit(T->data); //访问根结点
PreOrderTraverse(T->lchild); //递归遍历左子树
PreOrderTraverse(T->rchild); //递归遍历右子树
}
}
中序遍历(LDR)
中序遍历操作
若二叉树为空,则空操作
否则:后序遍历左子树 (L)
后序遍历右子树 ®
访问根结点 (D)
中序遍历的C语言描述
Status InOrderTraverse(BiTree T){
if(T==NULL) return OK; //空二叉树
else{
InOrderTraverse(T->lchild); //递归遍历左子树
visit(T->data); //访问根结点
InOrderTraverse(T->rchild); //递归遍历右子树
}
}
后序遍历(LRD)
后序遍历操作定义:
若二叉树为空,则空操作;否则
1、后序遍历左子树;(L)
2、后序遍历右子树;®
3、访问根节点(D)
后序遍历的C语言描述
Status PostOrderTraverse(BiTree T){
if(T==NULL) return OK; //空二叉树
else{
PostOrderTraverse(T->lchild); //递归遍历左子树
PostOrderTraverse(T->rchild); //递归遍历右子树
visit(T->data); //访问根结点
}
}
遍历算法的分析
如果去掉输出语句,从递归的角度看,三种算法是完全相同的,或说这三种算法的访问路径是相同的,只是访问结点的时机不同。
从虚线的出发点到终点的路径上,每个结点经过3次。
第1次经过时访问 = 先序遍历
第2次经过时访问 = 中序遍历
第3次经过时访问 = 后序遍历
时间复杂度: O(n)
空间复杂度: O(n)
层次遍历
层次遍历: 对于一颗二叉树,从跟结点开始,按从上到下、从左到右的顺序访问每一个结点。
每一个结点仅仅访问一次。
二叉树层次遍历算法:
void LevelOrder(BiTree T){
InitQueue(Q); //初始化辅助队列
BiTree p;
Enqueue(Q,T); //根节点入队列
while(!IsEmpty(Q)){ //队列不空则循环
Dequeue(Q,p); //队头结点出队列
visit(p);
if(p->lchild != NULL) //左子树不空,则左子树根结点入队列
Enqueue(Q,p->lchild);
if(p->rchild != NULL) //右子树不空,则右子树根结点入队列
Enqueue(Q,p->rchild);
}
}
遍历二叉树的应用
1、根据遍历序列确定二叉树
前提:
若二叉树中各结点的值均不相同,则二叉树结点的先序序列、中序序列、后序序列都是唯一的。
由二叉树的先序序列和中序序列或由二叉树的后序序列和中序序列都能确定唯一一个二叉树
-
已知先序和中序序列求二叉树
**解题步骤(思路): **由先序序列确定根;再由中序序列确定左右子树。 -
已知后序和中序序列求二叉树
**解题步骤(思路):**由后序序列确定根;再由中序序列确定左右子树。
实例
已知一棵二叉树的中序序列和后序序列分别是BDCEAFHG 和 DECBHGFA,请画出这棵二叉树。
①由后序遍历特征,根结点必在后序序列尾部(A);
②由中序遍历特征,根结点必在其中间,而且其左部必全部是左子树子孙(BDCE),其右部必全部是右子树子孙(FHG);
③继而,根据后序中的DECB子树可确定B为A的左孩子,根据HGF子串可确定F为A的右孩子;以此类推。
2、二叉树的建立
操作
1、先序(或中序或后序)遍历二叉树,读入一个字符,若读入字符为空,则二叉树为空,若读入字符非空,则生成一个结点。
2、将算法中“访问结点”的操作改为:生成一个结点,输入结点的值。
构造二叉树的算法
Status CreateBiTree (BiTree &T){
scanf( &ch ) ;
if (ch==’’) T=NULL;
else{
if(!(T=(BiTNode *)malloc(sizeof(BiTNode))) exit(OVERFLOW);
T->data=ch; //生成根结点
CreateBiTree( T->lchild); //构造左子树
CreateBiTree( T->rchild); //构造右子树
}
return(OK);
} // CreateBiTree
3、复制二叉树
操作
1、申请新结点,复制根结点
2、递归复制左子树
3、递归复制右子树
算法
int copy(BiTree T,BiTree &NewT){
if(T==NULL){ //如果是空树返回0
newT=NULL; return0;}
else{
NewT=new BiTNode;
NewT->data = T->data;
Copy(T->lChild, NewT->lchild);
Copy(T->rChild, NewT->rchild);}
}
4、计算二叉树的深度
操作
1、二叉树的深度应为其左、右子树深度的最大值加1
2、如果是空树,则深度为0;
3、否则,递归计算左子树的深度记为m,递归计算右子树的深度记为n,二叉树的深度则为m与n的较大者加1(因为还要算上根节点那一层)。
计算二叉树的深度算法
int Depth (BiTree T ){ // 返回二叉树的深度
if ( !T ) depthval = 0;
else {
depthLeft = Depth( T->lchild );
depthRight= Depth( T->rchild );
depthval = 1 + (depthLeft > depthRight ? depthLeft : depthRight);
}
return depthval;
}
5、计算二叉树结点个数
操作
1、如果是空树,则结点个数为0;
2、否则,结点个数为左子树的结点个数+右子树的结点个数再+1
计算二叉树结点个数算法
int NodeCount(BiTree T){
if(T == NULL )
return 0;
else
return NodeCount(T->lchild)+NodeCount(T->rchild)+1;
}
6、计算叶子节点个数
操作
1、遍历二叉树,在遍历过程中查找叶子结点,并计数。
2、由此,需在遍历算法中增添一个“计数”的参数,并将算法中“访问结点”的操作改为:若是叶子,则计数器增1。
计算叶子节点个数算法
void CountLeaf (BiTree T, int& count){
if ( T ) {
if ((!T->lchild)&& (!T->rchild))
count++; // 对叶子结点计数
CountLeaf( T->lchild, count);
CountLeaf( T->rchild, count);
} // if
} // CountLeaf
线索二叉树
具有n个结点的二叉链表中,一共有2n个指针域;因为n个结点中有n-1个孩子,即2n个指针域中,有n-1个用来指示结点的左右孩子,其余n+1个指针域为空
为什么要研究线索二叉树
线索二叉树的相关术语
线索:指向结点前驱和后继的指针
线索二叉树:加上线索的二叉树(图形式样)
线索链表:加上线索的二叉链表
线索化:对二叉树以某种次序遍历使其变为线索二叉树的过程
线索化二叉树
1、利用n+1个空链域,存储指向该线性序列中的“前驱”和“后继” 的指针,称作“线索”
2、若结点有左子树,则lchild指向其左孩子;否则, lchild指向其直接前驱(即线索);
3、若结点有右子树,则rchild指向其右孩子;否则, rchild指向其直接后继(即线索) 。
线索二叉树的结点结构
LTag :若 LTag=0, lchild域指向左孩子;
若 LTag=1, lchild域指向其前驱。
RTag :若 RTag=0, rchild域指向右孩子;
若 RTag=1, rchild域指向其后继。
typedef struct BiThrNode {
TElemType data;
struct BiThrNode *lchild, *rchild; // 左右指针
PointerThr LTag, RTag; // 左右标志
} BiThrNode, *BiThrTree
先序线索二叉树
中序线索二叉树
后序线索二叉树
中序线索链表头结点
头结点:
1、lchild域指向二叉树的根节点
2、rchild域指向中序遍历时访问的最后一个结点
同时,令二叉树中序序列第一个结点的lchild和最后一个结点rchild域的指针指向头结点
树和森林
树的存储结构
1、双亲表示法
实现:
定义结构数组,存放树的结点,每个结点含两个域:
数据域:存放结点本身信息。
双亲域:指示本结点的双亲结点在数组中的位置。
特点: 找双亲容易,找孩子难。
结点结构
例子
C语言的类型描述:
结点结构
typedef struct PTNode {
Elem data;
int parent; // 双亲位置域
} PTNode;
树结构
#define MAX_TREE_SIZE 100
typedef struct {
PTNode nodes[MAX_TREE_SIZE];
int r, n;
// 根结点的位置和结点个数
} PTree;
2、孩子链表
实现: 把每个结点的孩子结点排列起来,看成是一个线性表,用单链表存。则n个结点有n个孩子链表(叶子的孩子链表为空表)。而n个头指针又组成一个线性表,用顺序表(含n个元素的结构数组)存储。
C语言的类型描述
孩子结点结构:
typedef struct CTNode {
int child; //孩子结点下标的位置
struct CTNode *next;
} *ChildPtr;
双亲结点结构:
typedef struct {
Elem data;
ChildPtr firstchild;
// 孩子链的头指针
} CTBox;
树结构
typedef struct {
CTBox nodes[MAX_TREE_SIZE];
int n, r;
// 结点数和根结点的位置
} CTree;
实例
特点:找孩子容易,找双亲难
如果即想找孩子,又想找双亲,可以将孩子链表改为带双亲的孩子链表(在结构数组中再增加一个表示双亲的成员)
3、孩子兄弟表示法
实现: 用二叉链表做树的存储结构,链表中每个节点的两个指针域分别指向其第一个孩子结点和下一个兄弟结点
结点结构
typedef struct CSNode{
Elem data;
struct CSNode *firstchild, *nextsibling;
} CSNode, *CSTree;
例子
树与二叉树的转换
由于树和二叉树都可以用二叉链表做存储结构,则二叉链表作媒介可以导出树与二叉树之间的一个对应关系
树转换成二叉树
加线: 兄弟之间加一连线
抹线:对每个结点,除了其左孩子外,去除其与其余孩子之间的连线
旋转,以树的根节点为轴心,将整棵树顺时针转45度
口诀:兄弟相连留长子
二叉树转换成树
加线:将双亲结点左
孩子p的右孩子,右孩
子的右孩子….所有右
孩子都与双亲结点相连
抹线:抹掉原二叉树中双亲与右孩子之间的连线
调整:将结点按层次排列
口诀:左孩右右连双亲,去掉原来右孩线
森林转换成二叉树
将各棵树分别转换成二叉树
将每棵树的根节点用线相连
以第一棵树根节点为二叉树的根,再以根节点为轴心,顺时针旋转构成二叉树型结构
口诀:树变二叉根相连
二叉树转换成森林
抹线:将二叉树中根节点与右孩子连线,以及沿右分支搜索到的所有右孩子间连线全部抹去,得到孤立二叉树
还原:将孤立二叉树还原为树
口诀:去掉全部右孩线,孤立二叉在还原
树和森林的遍历
树的遍历
1、先根(次序)遍历:
若树不空,则先访问根结点,然后依次先根遍历各棵子树。
2、后根(次序)遍历:
若树不空,则先依次后根遍历各棵子树,然后访问根结点。
3、 按层次遍历:
若树不空,则自上而下自左至右访问树中每个结点。
森林的遍历
将森林看作三部分
😗*1、森林中第一棵树的根节点
😗*2、森林中第一棵树的子树森林
😗*3、森林中其他树构成的森林
1、先序遍历
若森林不空,则
访问森林中第一棵树的根结点;
先序遍历森林中第一棵树的子树森林;
先序遍历森林中(除第一棵树之外)其余树构成的森林。
即:依次从左至右对森林中的每一棵树进行先根遍历
2、中序遍历
若森林不空,则
中序遍历森林中第一棵树的子树森林;
访问森林中第一棵树的根结点;
中序遍历森林中(除第一棵树之外)其余树构成的森林。
即:依次从左至右对森林中的每一棵树进行后根遍历
哈夫曼树
哈夫曼树的基本概念
路径: 从树中一个结点到另一个结点间的分支构成这两个结点间的路径。
结点间的路径长度: 两结点间路径上的分支数
树的路径长度: 树中每个结点的路径长度之和。记作:TL
结点数目相同的二叉树中,完全二叉树是路径长度最短的二叉树(但是路径长度最短的树不一定是完全二叉树)
两个树的路径长度之和一样,但是第二个数不是完全二叉树,所以说上面那个条件是充分条件。不是必要条件
权(weight): 将树中结点赋给一个有着某种含义的数值,则这个数值成为该结点的权。
**结点的带权路径长度:**从根结点到该结点的路径长度与结点上权的乘积
**树的带权路径长度:**树中所有叶子结点的带权路径长度之和
哈夫曼树: 最优树,带权路径长度(WPL)最短的树
注:"带权路径长度最短"是在"度相同"的树中比较而得的结果因此有最优二叉树,最优三叉树之称等等。
哈夫曼树: 最优二叉树,带权路径长度(WPL)最短的二叉树
特点: 哈夫曼树中权越大的结点离根越近
具有相同带权结点的哈夫曼树不唯一
哈夫曼树的构造
哈夫曼算法
1.根据给定的 n 个权值 {w1, w2, …, wn},构造 n 棵二叉树的集合F = {T1, T2, … , Tn},其中每棵二叉树Ti中均只含一个带权值为 wi 的根结点,其左、右子树为空树;
😗 构造森林全是根 😗
2.在 F 中选取其根结点的权值为最小的两棵二叉树,分别作为左、右子树构造一棵新的二叉树,并置这棵新的二叉树根结点的权值为其左、右子树根结点的权值之和
😗 选用两小造新树 😗
3.从F中删去这两棵树,同时加入刚生成的新树
😗 删除两小添新人 😗
4.重复 (2) 和 (3) 两步,直至 F 中只含一棵树为止。这棵树便是哈夫曼树
😗 重复2、3剩单根 😗
基本思想:使权大的结点靠近根
操作要点:对权值的合并、删除、与替换,总是合并当前值最小的两个
注意
1、初始森林中的n棵二叉树,每棵树有一个孤立的结点,它们既是根,又是叶子
2、n个叶子的哈夫曼树要经过n-1次合并,产生n-1个新结点。最终求得的哈夫曼树中共有2n-1个结点。
3、哈夫曼树是严格的二叉树,没有度数为1的分支结点。
哈夫曼树算法实现
结点结构
typedef struct { //结点类型
int weight; //权值,不妨设权值均大于零
int lchild,rchild,parent; //左右孩子及双亲指针
}HTNode,*HuffmanTree;
void HuffmanCoding(HuffmanTree &HT,HuffmanCode &HC,
int *w,int n){
if(n<=1) return; //分配哈夫曼树空间
m = 2*n-1;
HT=(HuffmanTree)malloc((m+1)*sizeof(HTNode));
for(i=1;i<=m;++i){ //将2n-1个元素的lch、rch、parent置为0
HT[i].lch=0; HT[i].rch=0; HT[i].parent=0;
}
for(;i<=m;i<=n,++i) scanf("%d",&HT[i].weight); //输入前n个元素的weight值
for(i=n+1;i<=m;i++){ //构造哈夫曼树
SelectMin(HT,i-1,s1,s2); //在HT[k]中选择两个双亲域为0,且权值最小的结点,并返回他们在HT中的序号s1和s2
HT[s1].parent = i; HT[s2].parent = i;//得到新结点i
HT[i].lchild = s1; HT[i].rchild = s2; //s1,s2分别作为i的左右孩子
HT[i].weight = HT[s1].weight + HT[s2].weight;
//i的权值为左右孩子权值之和
}
哈夫曼编码
void HuffmanCoding(HuffmanTree &HT,HuffmanCode &HC,
int *w,int n){
if(n<=1) return; //分配哈夫曼树空间
m=2*n-1; HT(HuffmanTree)malloc((m+1)*sizeof(HTNode));
for(p=HT+1,i=1;i<=n;++i,++p,++w) *p={*w,0,0,0} for(;i<=m;++i,++p) *p={0,0,0,0} //初始化哈夫曼树 for(i=n+1;i<=m;i++){ //构造哈夫曼树
SelectMin(HT,i-1,s1,s2); //在HT[k]中选择两个双亲域为0,且权值最小的结点,并返回他们在HT中的序号s1和s2
HT[s1].parent = i; HT[s2].parent = i;//得到新结点i
HT[i].lchild = s1; HT[i].rchild = s2; HT[i].weight = HT[s1].weight + HT[s2].weight;
//i的权值为左右孩子权值之和
for(i=1;i<=n;i++){
start=n-1; //start初始指向最后
for(c=i,f=HT[i].parent; f!=0; c=f,f=HT[f].parent) {
if(HT[f].lchild==c) cd[--start]=“0”; //c是f左孩子,生成0 else cd[--start]=“1”; //c是f右孩子,生成1
}
HC[i]=(char * )malloc((n-start)*sizeof(char)); //为第i个字符编码分配空间
strcpy(Hc[i],&cd[start]); //将编码从临时空间cd复制到HC当前行
}
free(cd);
}//函数结束