树
- 一、树
- 1、树的定义
- 2、树的基本术语
- 3、树结构和线性结构的比较
- 二、二叉树
- 1、二叉树的定义
- 2、二叉树的形态与树的形态
- 3、二叉树的性质
- 4 、二叉树的存储结构
- 5、遍历二叉树
- 6、二叉树的其他操作
- 7、线索二叉树
- 三、树与二叉树的转换
- 1、树转换成二叉树
- 2、二叉树变树
- 四、哈夫曼树
- 1、相关概率与定义
- 2、哈夫曼树的构造与算法
- 3、哈夫曼编码
学习视频
一、树
1、树的定义
(1)树的定义
树(Tree)n(n>=0)个有限数据元素的集合。当n=0时称为空树。当n>0时,是非空树,它满足以下两个条件:
- 有且仅有一个特定的称为根(Root)的结点;
- 其余结点可分为m (m≥0)个互不相交的有限集T1,T2,T3,…Tm,其中每一个集合本身又是一棵树,并称为根的子树(SubTree)。
(2)树的表示方法
树的逻辑结构表示主要有四种:树形表示法,嵌套集合表示法,凹入表示法和广义表表示法等
2、树的基本术语
- 森林:是m(mzO)棵互不相交的树的集合。把根结点删除树就变成了森林。一棵树可以看成是一个特殊的森林。给森林中的各子树加上一个双亲结点,森林就变成了树。树一定是森林,森林不一定是树。
- 有序树:树中结点的各子树从左至右有次序(最左边的为第一个孩子)。无序树:树中结点的各子树无次序。
3、树结构和线性结构的比较
线性结构 | 树结构 |
---|---|
第一个数据元素无前驱 | 根结点(只有一个)无双亲 |
最后一个数据元素无后继 | 叶子结点(可以有多个)无孩子 |
其它数据元素 | 其它结点—中间结点 |
一个前驱,一个后继 | 一个双亲,多个孩子 |
一对一 | 一对多 |
二、二叉树
1、二叉树的定义
(1)二叉树的定义
二叉树是n(n≥O)个结点的有限集,它或者是空集(n = 0),或者由一个根结点及两棵互不相交的分别称作这个根的左子树和右子树的二叉树组成。
特点:
- 1、每个结点最多有俩孩子(二叉树中不存在度大于2的结点)。
- 2、子树有左右之分,其次序不能颠倒。
- 3、二叉树可以是空集合,根订以有空的左子树或空的右子树。
*注意:*二叉树不是树的特殊情况,它们是两个概率
- 二叉树结点的子树要区分左子树和右子树,即使只有一棵子树也行区分,说明它是左子树,还是右子树。
- 二叉树结点的子树要区分左子树和右子树,即使只有一棵子树也行区分,说明它是左子树,还是右子树。
- 也就是二叉树每个结点位置或者说次序都是固定的,可以是空,但是不可以说它没有位置,而树的结点位置是相对于别的结点来说的,没有别的结点时,它就无所谓左右了
2、二叉树的形态与树的形态
案例︰利用二叉树求解表达式的值以二叉树表示表达式的递归定义如下:
- 若表达式为数或简单变量,则相应二叉树中仅有一个根结点,其数据域存放该表达式信息;
- 若表达式为“第一操作数运算符第二操作数”的形式,则相应的二叉树中以左子树表示第一操作数,右子树表示第二操作数,根结点的数据域存放运算符(若为一元运算符,则左子树为空),其中,操作数本身又为表达式。
3、二叉树的性质
(1)性质1:在二叉树的第i层上至多有2^(i-1)个结点(i≥1)。
第1层:2^0=1
第2层: 2^1=2
第3层:2^2=4
第4层:2^3=8
证:采用归纳法证明此性质。
归纳基:当i=1时,只有一个根结点,2^(i-1)=2*0 =1,命题成立。归纳假设:设对所有的j(1≤j<i),命题成立,
即第j层上至多7有2^(j-1)个结点。那么可以证明j=i时命题也成立。
归纳证明:由归纳假设可知,第i-1层上至多有2^(i-2)个结点。
由于二叉树每个结点的度最大为2,故在第i层上最大结点数为第i-1层上最大结点数的2倍,即:2x2^(i-2)=2的(i-1)次方 证毕。
(2)性质2:深度为k的二叉树至多有2^k-1个结点(k≥1)。
证:由性质1可知,深度为k的二叉树的最大结点数为:
(3)性质3:对任何一棵二叉树T,如果其叶子数为n0,度为2的结点数为n,则n0 = n2+ 1。
总边数为B B = n-1=B=n2×2+n1×1
总结点数为n n=n2×2+ n1x1+1又n = n2+ n 1+n0
(4)满二叉树
一棵深度为k且有2^k-1个结点的二叉树称为满二叉树。
特点:
- 每一层上的结点数都是最大结点数(即每层都满),
- 叶子节点全部在最底层
对满二叉树结点位置进行编号: - 编号规则:从根结点开始,自上而下,自左而右。
- 每一结点位置都有元素。
例1:思考:下图中的二叉树是满二叉树吗?
答:不是满二叉树,叶子不在同一层上。且最后一层结点个数不满!
满二叉树在同样深度的二叉树中结点个数最多
满二叉树在同样深度的二叉树中叶子结点个数最多
(5)完全二叉树
深度为k的具有n个结点的二叉树,当且仅当其每一个结点都与深度为k的满二叉树中编号为1~n的结点—一对应时,称之为完全二叉树。
注:在满二叉树中,从最后一个结点开始,连续去掉任意个结点,即是一棵完全二叉树.一定是连续的去掉!!!
例2:思考:左边的树是否是完全二叉树?
答:不是,I与J两结点不连续。
特点:
- 叶子只可能分布在层次最天的两层上。
- 对任一结点,如果其右子树的最大层次为i,则其左子树的最大层次必为i或i+1。
(6)性质4具有n个结点的完全二叉树的深度为
(7)性质5:如果对一颗有n个结点的完全二叉树的结点按层次有:
4 、二叉树的存储结构
(1)顺序存储结构
顺序存储一颗二叉树,先对该二叉树中的各结点进行编号,然后以各结点的编号为下标,把各结点的值存到一维数组中
例3:二叉树结点数值采用顺序存储结构,如图所示。画出二叉树表示:
(2)链式存储结构
- 二叉链表
typedef struct BiNode{TElemType data;struct BiNode *Ichild,*rchild;//左右孩子指针}BiNode,*BiTree;
例4:
- 三叉链表
typedef struct TriTNode{TelemType data;struct TriTNode *lchild,*parent,*rchild;
}TriTNode,*TriTree;
5、遍历二叉树
(1)遍历二叉树
- 遍历定义:顺着某一条搜索路径巡访二叉树中的结点,使得每个结点被访问一次,而且仅被访问一次(周游)。
注意:“访问”的含义很广,可以是对结点作各种处理,如:输出结点的信息、修改结点数值等,但要求各种访问不破坏原来的数据结构。 - 遍历目的:得到树中所有结点的一个线性排列。
- 遍历用途:它是树结构插入、删除、修改、查找和排序运算的前提,是二叉树一切运算的基础核心。
- 先序遍历(DLR)
若二叉树为空,则空操作;否则
访问根结点;
先序遍历左子树
先序遍历右子树
如图
- 先序遍历的算法实现
//先序遍历的算法实现(DLR)
void PreOrder(BT *T){if(T==NULL) return;//递归调用结束条件 else{printf("%c",T->data);//输出根结点 PreOrder(T->lchild);//先序遍历左子树 PreOrder(T->rchild);//先序遍历右子树 }
}
- 中序遍历(LDR)
若二叉树为空,则空操作;否则
中序遍历左子树
访问根结点
中序遍历右子树
如图:
- 中序遍历的算法实现
//中序遍历的算法实现(LDR)
void InOrder(Bt *T){if(T==NULL) return;//递归调用结束条件 else{InOrder(T->lchild);//中序遍历左子树 printf("%c",T->data);//输出根结点 InOrder(T->rchild);//中序遍历右子树 }
}
- 后序遍历(LRD)
若二叉树为空,则空操作;否则
后序遍历左子树
后序遍历右子树
访问根结点
如图:
- 后序遍历的算法实现
//后序遍历的算法实现(LRD)
void PostOrder(BT *T){if(T==NULL) return;//递归调用结束条件 else{PostOrder(T->lchild);//后序遍历左子树 PostOrder(T->rchild);//后序遍历右子树 printf("%c",T->data);//输出根结点 }
}
- 层次遍历
对于一颗二叉树,从根结点开始,按从上到下、从左到右的顺序访问每个结点每个结点只能被访问一次。 - 层次遍历的算法实现
//层次遍历的算法实现
void LevelOrder(BT *T){int f ,r;//定义队头指针 BT *p,*q[MAX];//定义循环队列,存放结点指针 p=T;if(p!=NULL){//若二叉树非空,则根结点地址入队 f=l;q[f]=p;r=2;}while(f!=r){//队列不为空 p=q[f];printf("%c",p->data);//访问队首结点 if(p->lchild!=NULL){q[r]=p->lchild;r=(r+1)%MAX;//将队首结点的左孩子入队 }if(p-<rchlid!=NULL){q[r]=p->rchlid;r=(r+1)%MAX;//将队首结点的右孩子入队 }f=(f+1)%MAX;}
}
例5:写出下图二叉树的各种遍历顺序
解:先序:ABDGCEHF;中序:DGBAEHCF;后序:GDBHEFCA
例6:用二叉树表示算术表达式,请写出下图所示二叉树的先序、中序和后序遍历顺序
解:先序:-+axb-cd/ef;中序:a+bxc-d-e/f;后序:abcd-x+ef/-
(2)恢复二叉树
例7:已知先序和中序序列求二叉树
- 已知二叉树的先序和中序序列,构造相对应的二叉树
先序:ABCDEFGHIJ
中序:CDBFEAIHGJ
分析:由先序序列确定根结点;由中序序列确定左右子树
解:由先序知根为A,则由中序知左子树为CDBFE,右子树为IHGJ
在由先序得B为A的左孩子,因为现在得到的右子树为IHGJ,由先序遍历的规则知:G为A的右孩子,在由中序得CD为B得左子树,FE为B的右子树;IH为G左子树,J为G的右子树。因为 中序CDB先序BCD必定得C为B的左孩子D为C的右孩子,又因为先序EF中序FE得 ,E必然是B的右孩子,F为E的左孩子,
在由先序GHI与中序IHG得H为G的左子树,I为H的左子树,即得如图的二叉树:
- 已知一颗二叉树的中序为:BDCEAFHG;后序为:DECBHGFA请画出这颗二叉树:
解:由后序遍历得 A必然为根,因为中序为BDCEAFHG得A的左子树为BDCE右子树为FHG,在由后序DECBHGF得B必然为A的左孩子,F必然为A的右孩子,因为中序BDCE,后序DECB得C为B的右子孩子B没有左孩子,D为C的左孩子,E为C的右孩子。在因为中序FHG和后序HGF,F必然没有左子树 得 G必然为H的根H为G的左孩子得如图:
6、二叉树的其他操作
(1)建立二叉树
BT *CreateBTree(){BT *t;char ch;scanf("%c",&ch);getchar();if(ch=='0'){t=NULL; }else{t=(Bt*)malloc(sizeof(BT));t->data=ch;printf("请输入%c结点的左孩子结点:",t->data);t->lchild=CreateBtree();printf("请输入%c结点的右孩子结点:",t->data);t->rchlid= CreateBtree();}return t;
}
(2)用广义表法输出二叉树
//用广义表法输出二叉树
void ShowBTree(BT *T){if(T!=NULL){//当二叉树非空时 printf("%c",T->data);//输出该结点数据域 if(T->lchlid!=NULL){//若其左子树非空 printf("(");//输出左括号 ShowBTree(T->lchild);//递归调用输出其左子树各个结点 if(T->rchlid!=NULL){//若其右子树非空 printf(",");//输出“,” ShowBTree(T->rchild);//递归调用输出其右子树各个结点 }printf(")");}elseif(T->rchild!=NULL){//若其左子树为空,右子树不为空 printf("(");//输出括号 ShowBTree(T->lchild);//递归调用输出其左子树各个结点 if(T->rchlid!=NULL){//若其右子树不为空 printf(",");//输出“,” ShowBTree(T->rchild);//递归调用输出右子树各个结点 }printf(")");}}
}
(3)统计二叉树叶子结点数目
//统计二叉树叶子结点数目
void Leafnum(BT *T){if(T){//若树不为空 if(T->lchild==NULL&&T->rchild==NULL)coun++;Leafnum(T->lchild);//递归统计T的左子树叶子结点 Leafnum(T->rchild);//递归统计T的右子树叶子结点 }
}
(4)求二叉树结点总数
//求二叉树结点总数
void Leafnum(BT *T){if(T){//若树不为空 coun++;Leafnum(T->lchild);//递归统计T的左子树叶子结点 Leafnum(T->rchild);//递归统计T的右子树叶子结点 }
}
(5)求二叉树的深度
//求二叉树的深度
int TreeDepth(BT *T){int ldep,rdep;if(T==NULL){return 0;else{ldep=TreeDepth(T->lchild);//递归统计T的左子树深度 rdep=TreeDepth(t->rchild);//递归统计T的右子树深度 if(ldep>rdep)return ldep+1;elsereturn rdep+1;}}
}
(6)复制二叉树
//复制二叉树
int Copy(BT *T,BT &NewT){if(T==NULL){NewT=new BiTNode; NewT->data=T->data;Copy(T->lchild,NewT->lchild);//递归复制左子树 Copy(T->rchild,NewT->rchild);//递归复制右子树 }
}
7、线索二叉树
利用二叉链表中的空指针域:
如果某个结点的左孩子为空,则将空的左孩子指针域改为指向其前驱;如果某结点的右孩子为空,则将空的右孩子指针域改为指向其后继
——这种改变指向的指针称为“线索”
加上了线索的二叉树称为线索二叉树(Threaded Binary Tree)对二叉树按某种遍历次序使其变为线索二叉树的过程叫线索化
为区分Irchid和rchild指针到底是指向孩子的指针,还是指向前驱或者后继的指针,对二叉链表中每个结点增设两个标志域 Itag和rtag,并约定:
为区分Irchid和rchild指针到底是指向孩子的指针,还是指向削驱现台后继的指针,对二叉链表中每个结点增设两个标志域 Itag和rtag,并约定:
ltag = 0 Ichild 指向该结点的左孩子
ltag = 1 Ichild 指向该结点的前驱
rtag = 0 rchild 指向该结点的右孩子
rtag = 1 rchild 指向该结点的后继
这样,结点的结构为:
typedef struct BiThrNode{int data;int Itag, rtag;struct BiThrNode *lchild,rchild;}BiThrNode,*BiThrTree ;
例8、画出以下二叉树对应的中序线索二叉树。
该二叉树中序遍历结果为: H,D, I,B,E, A, F,C, G
增设了一个头结点:
ltag=O, Ichild指向根结点,
rtag=1, rchild指向遍历序列中最后一个结点
遍历序列中第一个结点的lc域和最后一个结点的rc域都指向头结点
三、树与二叉树的转换
1、树转换成二叉树
①加线:在兄弟之间加一连线
②抹线:对每个结点,除了其左孩子外,去除其与其余孩子之间的关系
③旋转:以树的根结点为轴心,将整树顺时针转45°
树变二叉树:兄弟相连留长子
例9:将树转换成二叉树
2、二叉树变树
①加线:若p结点是双亲结点的左孩子,则将p的右孩子,右孩子的右孩子…沿分支找到的所有右孩子,都与p的双亲用线连起来
②抹线:抹掉原二叉树中双亲与右孩子之间的连线
③调整:将结点按层次排列,形成树结构
二叉树变树:左孩右右连双亲,去掉原来右孩线。
例10:将二叉树转换成树
四、哈夫曼树
哈夫曼树(Huffman Tree)是一种特殊的二叉树,这种树的所有的叶子结点都有权值,从而构造出带路径长度最短的二叉树,即哈夫曼树,又称最优树。
1、相关概率与定义
(1)路径:从树中一个结点到另一个结点之间的分支构成这两个结点间的路径。
(2)结点的路径长度:两结点间路径上的分支数。
(a)从A到 B,C,D, E, F, G,H,l的路径长度分别为1,1,2,2,3,3,4,4。
(b)从A到B,C,D, E, F, G,H,I的路径长度分别为1,1,2,2,2,2,3,3。
(3)树的路径长度:从树根到每一个结点的路径长度之和。记作:TL
TL (a)=0+1+1+2+2+3+3+4+4=20
TL (b)=0+1+1+2+2+2+2++3+3=16
结点数目相同的二叉树中,完全二叉树是路径长度最短的二叉树。
(4)权(weight):将树中结点赋给一个有着某种含义的数值,则这个数值称为该结点的权。
(5)结点的带权路径长度:从根结点到该结点之间的路径长度与该结点的权的乘积。树的带权路径长度:树中所有叶子结点的带权路径长度之和。记作:
例11:有4个结点a, b, c d,权值分别为7,5.2,4,构造以此4个结点为叶子结点的二叉树:
带权路径长度是:
(a)WPL=7x2+5x2+2x2+4x2= 36
(b)WPL=7x3+5x3+2x1+4×2= 46
- 哈夫曼树:最优树,带权路径长度(WPL)最短的树。
- “带权路径长度最短”是在“度相同”的树中比较而得的结果,因此有最优二叉树、最优三叉树之称等等。
- 哈夫曼树:最优二叉树,带权路径长度(WPL)最短的二叉树。因为构造这种树的算法是由哈夫曼教授于1952年提出的,所以被称为哈夫曼树,相应的算法称为哈夫曼算法。
- 满二叉树不一定是哈夫曼树,哈夫曼树中权越大的叶子离根越近
2、哈夫曼树的构造与算法
(1)哈夫曼算法(构造哈夫曼树的方法)
- 根据n个给定的权值{W1, W2…,Wn}构成n棵二叉树的森林F={T1,,T5, …,Tn},其中T只有一个带权为wi的根结点。构造森林全是根
- 在F中选取两棵根结点的权值最小的树作为左右子树,构造一棵新的二叉树,且设置新的二叉树的根结点的权值为其左右子树上根结点的权值之和。选用两小造新树
- 在F中删除这两棵树,同时将新得到的二叉树加入森林中。删除两小添新人
- 重复(2)和(3),直到森林中只有一棵树为止,这棵树即为哈夫曼树。
例12:有4个结点a, b, c, d,权值分别为7,5,2,4,构造哈夫曼树。
- 包含n棵树的森林要经过n-1次合并才能形成哈夫曼树,共产生n-1个新结点
- 包含n个叶子结点的哈夫曼树中共有2n-1个结点。
- 哈夫曼树的结点的度数为О或2,没有度为1的结点。
(2)哈夫曼树构造算法的实现
void CreatHuffmanTree (HuffmanTree HT, int n){//构造哈夫曼树——哈夫曼算法 if(n<=1)return;HT=new HTNode[mt1];//Q号单元未用,HT[m]表示根结点for(i=1;i<=m;++i){//将2n-1个元素的Ich、rch、parent置为0HT[i].lch=O; HT[i].rch=O; HT[i].parent=O;}for(i=1;j<=n;++i) cin>>HT[i].weight;//输入前n个元素的weight值//初始化结束,下面开始建立哈夫曼树for( i=n+1;i<=m; i++){//合并产生n-1个结点——构造Huffman树Select(HT, i-1, s1, s2);//在HT[k](1≤k≤i-1)中选择两个其双亲域为0,//且权值最小的结点,并返回它们在HT中的序号s1和s2HT[s1].parent=i;HT[s2] .parent=i;//表示从F中删除s1,s2HT[i].Ich=s1; HT[li].rch=s2 ;//s1,s2分别作为i的左右孩子 HT[i].weight=HT[s1].weight +HT[s2] .weight;//i的权值为左右孩子权值之和}
}
3、哈夫曼编码
在远程通讯中,要将待传字符转换成由二进制的字符串:
设要传送的字符为:
若将编码设计为长度不等的二进制编码,即让待传字符串中出现次数较多的字符采用尽可能短的编码,则转换的二进制字符串便可能减少。
问题:什么样的前缀码能使得电文总长最短?
——哈夫曼编码
方法:
- 统计字符集中每个字符在电文中出现的平均概率(概率越大,
要求编码越短)。 - 利用哈夫曼树的特点:权越大的叶子离根越近;将每个字符的概率值作为权值,构造哈夫曼树。则概率越大的结点,路径越短。3、在哈夫曼树的每个分支上标上O或1:结点的左分支标0,右分支标1
把从根到每个叶子的路径上的标号连接起来,作为该叶子代表的字符的编码。
例13:为什么哈夫曼编码能够保证是前缀编码?
答:因为没有一片树叶是另一片树叶的祖先,所以每个叶结点的编码就不可能是其它叶结点编码的前缀
例14:为什么哈夫曼编码能够保证字符编码总长最短?
答:因为哈夫曼树的带权路径长度最短,故字符编码的总长最短。 - 性质1哈夫曼编码是前缀码·
- 性质2哈夫曼编码是最优前缀码