一、树的基本术语
- 结点:树中的一个独立单元
- 结点的度:结点拥有的子树数称为结点的度
- 树的度:树的度是树内各结点度的最大值
- 叶子:度为0的结点称为叶子或终端结点
- 非终端结点:度不为0的结点称为非终端结点或分支结点,除根结点外,非终端结点也称为内部结点
- 双亲和孩子:结点的子树的根称为该结点的孩子,相应地,该结点称为孩子的双亲
- 兄弟:同一个双亲的孩子之间互称兄弟
- 祖先:从根到该结点所经分支上的所有结点
- 子孙:以某结点为根的子树中的任一结点都称为该结点的子孙
- 层次:结点的层次从根开始定义起,根为第一次,根的孩子为第二层
- 堂兄弟:双亲在同一层的结点互为堂兄弟
- 树的深度:树中结点的最大层次称为树的深度或高度
- 有序树和无序树:如果将该树中的各子树看成从左至右是有次序的,则称该树为有序树,否则为无序树
- 森林:是m(m>=0)棵互不相交的树的集合
二、二叉树的定义
- 有且只有一个称之为根的结点
- 除根节点以外的其余结点分为两个互不相交的子集
T1
和T2
,分别称为T的左子树和右子树,且T1
和T2
本身又都是二叉树 - 二叉树每个结点至多只有两棵子树(即二叉树中不存在度大于2的结点)
- 二叉树的子树有左右之分,其次序不能任意颠倒
二叉树可以有的5种基本形态
1.空二叉树
2.仅有根结点的二叉树
3.右子树为空的二叉树
4.左子树为空的二叉树
5.左右子树非空的二叉树
三、二叉树常见的两个应用
- 哈夫曼编码:使频率高的字符采用尽可能短的编码
- 表示表达式:无需括号,其结构却有效地表达了其运算符间的运算次序
四、二叉树的性质和存储结构
- 在二叉树的第i层上至多有2i-1 个结点(i>=1)
- 深度为k的二叉树至多有2k -1个结点
- 对任何一个二叉树T,如果其终端节点树为n0,度为2的结点数为n2,则
n0=n2+1
证明:
结点总数为 n=n0+n1+n2
B为分支总数 除根节点外 每个结点都有分支进入 则 n=B+1
这些分支都由度为1和2射出的 则 B=n1+2n2
即 n=n1+2n2+1=n0+n1+n2
得 n0=n2+1
满二叉树:深度为k且含有2k -1个结点的二叉树
完成二叉树:深度为k的,有n个结点的二叉树,当且仅当其每一个结点都与深度为k的满二叉树中编号从1至n的结点一一对应时,称之为完全二叉树
1.叶子结点只可能在层次最大的两层出现
2.对任一结点,若其右分支下的子孙的最大层次为l,则其左分支下的子孙最大层次必为l或l+1
- 具有n个结点的完全二叉树的深度为log2n(向下取整)+1
- 如果对一棵有n个结点的完全二叉树深度为(log2n(向下取整)+1)的结点按层序编号(从第一层到第log2n(向下取整)+1层,每层从左到右),则对任意结点i有:
1)如果i=1,则结点i是二叉树的根,无双亲;如果i>1,其双亲结点为i/2(向下取整)
2)如果2i>n,则结点i无左孩子(结点i为叶子结点);否则其左孩子是结点2i
3)如何2i+1>n,则结点i无右孩子,否则其右孩子是结点2i+1
五、二叉树的存储结构
顺序存储结构
- 对于完成二叉树,只要从根起按层序存储即可,依次自上而下、自左至右存储结点元素
- 对于一般二叉树,应将其每个结点与完成二叉树上的结点相对照,以”0“表示不存在的结点
链式存储结构
- 二叉树的链表中的结点至少包含3个域:数据域、左指针、右指针——二叉链表
- 有时为了便于找到结点的双亲,还可在结点结构中增加一个指向双亲的指针域——三叉链表
- 链表的头指针指向二叉树的根结点
- 在含有n个节点的二叉链表中有n+1个空链域
六、遍历二叉树和线索二叉树
二叉树
一、遍历二叉树
- 目的:将非线性结构的树中结点排成一个线性序列
- 四种遍历方法:先序遍历、中序遍历、后序遍历、层次遍历
先序:根->左->右
中序:左->根->右
后序:左->右->根
层次:第一层、第二层...
二、递归的遍历算法
void InOrderTraverse(BiTree T){
if(T){
//cout<<T->data<<" ";——先序遍历
InOrderTraverse(T->lchild);
//cout<<T->data<<" ";——中序遍历
InOrderTraverse(T->rchild);
//cout<<T->data<<" ";——后序遍历
}
}
三、非递归的中序遍历算法
步骤:
1.初始化一个空栈S,指针p指向根结点
2.申请一个结点空间q,用来存放栈顶弹出元素
3.当p非空或者栈非空时,循环指向以下操作
如果p非空,则将p进栈,p指向该结点的左孩子
如果p为空,则弹出栈顶元素并访问,将p指向该结点的右孩子
void InOrderTraverse(BiTree T){
InitStack(S);
BiTNode* p=T;
BiTNode* q=new BiTNOde;
while(p||!StackEmpth(S)){
if(p){
Push(S,p);
p=p->lchild;
}
else{
pop(S,q);
cout<<q->data;
p=q->rchild;
}
}
}
四、根据遍历序列确定二叉树
根据先序序列和中序序列 或 根据后序序列和中序序列
步骤:
确定根结点
用根节点将中序序列分为左右子树
用左右子树去 先序/后序 分割左右子树
从先序/后序 左右子树中 又找到根结点...
五、二叉树遍历算法的应用
- 先序遍历的顺序建立二叉链表
void CreateBiTree(BiTree &T){
char ch;
cin>>ch;
if(ch=='#') T=NULL;
else{
T=new BiTNode;
T->data=ch;
CreateBiTree(T->lchild);
CreateBiTree(T->rchild);
}
}
- 复制二叉树
void Copy(BiTree T.BiTree &NewT){
if(T==NULL){
NewT=NULL;
return;
}
else{
NewT=new BiTNode;
NewT->data=T->data;
Copy(T->lchild,NewT->lchild);
Copy(T->rchild,NewT->rchild);
}
}
- 计算二叉树的深度——在后序遍历二叉树的基础上进行运算的
int Depth(BiTree T){
if(T=NULL) return 0;
else{
m=Depth(T->lchild);
n=Depth(T->rchild);
m>n? return m+1:return n+1;
}
}
- 统计二叉树中结点的个数
int NodeCount(BiTree &T){
if(T=NULL) return 0;
else return NodeCount(T->lchild)+NodeCount(T->rchild)+1;
线索二叉树
一、基本概念
- 目的
当以二叉链表作为存储结构时,只能找到结点的左、右孩子信息,而不能直接得到结点在任一序列中的前驱和后继信息,这种信息只有在遍历的动态过程中才能得到,为此引入线索二叉树来保持这些在动态过程中得到的有关前驱和后继的信息 - 规定:
若结点有左子树,则其lchild域指示其左孩子,否则令lchild域指示其前驱;若结点有右子树,则其rchild域指示其右孩子,否则令rchild指示其后继 - 结构:
lchild - LTag - data - RTahg - rchild
(LTag 为0表示指示左孩子 为1表示指示前驱) - 存储
typedef struct BiThrNode{
TElemType data;
struct BiThrNode *lchild,*rchild;
int LTag,RTag;
}BiThrNode,*BiThrTree;
- 定义
以上述存储结构进行存储的结点 构成的二叉链表作为二叉树的存储结构,则叫线索链表,其中指向结点前驱和后继的指针,叫做线索。加上线索的二叉树称之为线索二叉树,对二叉树以某种次序遍历使其变为线索二叉树的过程叫做线索化。
二、构造线索二叉树
对二叉树按照不同的遍历次序进行线索化,可以得到不同的线索二叉树,包括先序线索二叉树、中序线索二叉树和后序线索二叉树。
- 以结点p为根的子树中序线索化
步骤:
2. 如果p非空,左子树递归线索化
3. 如果p的左孩子为空,则给p加上左线索,将其LTag置为1,让p的左孩子指针指向pre(前驱).否则将p的LTag置为0
4. 如果pre的右孩子为空,则给pre加上右线索,将其RTag置为1,让pre的右孩子指针指向p(后继),否则将pre的RTag置为0
5. 将pre指向刚访问过的结点p,即pre=p
6. 右子树递归线索化
void InThreading(BiThrTree p){
//pre是全局变量,初始化时其右孩子指针为空,便于在树的最左点开始建线索
if(p){
InThreading(p->lchild);
if(!p->lchild){
p->LTag=1;
p->lchild=pre;
}
else p->LTag=0;
if(!pre->rchild){
pre->RTag=1;
pre->rchild=p;
}
else pre->RTag=0;
pre=p;
InThreading(p->rchild);
}
}
- 带头结点的二叉树中序线索化
void InOrderThreading(BiThrTree &Thrt,BiThrTree T){
//中序遍历二叉树T,并将其中序线索化,Thrt指向头结点
Thrt=new BiThrNode;
Thrt->LTag=0;
Thrt->RTag=1;
Thrt->rchild=Thrt;//初始化右指针指向自己
if(!T) Thrt->lchild=Thrt;//若树为空,则左指针也指向自己
else{
Thrt->lchild=T;
pre=Thrt;
InThreading(T);//对以T为根的二叉树进行中序线索化
pre->rchild=Thrt;//pre为最右结点,pre的右线索指向头结点
pre->RTag=1;
Thrt->rchild=pre;//头结点的右线索指向pre
}
}
三、查找与遍历线索二叉树
有了结点的前驱和后继信息,线索二叉树的遍历和在指定次序下查找结点的前驱和后继算法都变得简单。
- 在中序线索二叉树中查找
1)查找p指针所指结点的前驱
· 若 p->LTag 为1,则p的左链指针其前驱
· 若 p->LTag 为0,则说明有p有左子树,结点的前驱是遍历左子树时最后访问的一个结点
2)查找p指针所指结点的后继
· 若 p->RTag 为1,则p的右链指示其后继
· 若 p->RTag 为0,则说明p有右子树,结点的后继是遍历其右子树时访问的第一个结点 - 在先序线索二叉树中查找
… - 在后序线索二叉树中查找
… - 遍历中序线索二叉树
步骤:
1.指针p指向根结点
2.p为非空树或遍历未结束时,循环执行以下操作:
沿左孩子向下,到达最左下结点*p,它是中序的第一个结点
访问*p
沿右线索反复查找当前结点*p的后继结点并访问后继结点,直至右线索为0或者遍历结束
转向p的右子树
void InOrderTraverse_Thr(BiThrTree T){
//T指向头结点,头结点的左链lchild指向根结点
//中序遍历二叉线索树T的非递归算法,对每个数据元素直接输出
p=T->lchild;
while(p!=T){
while(p->LTag==0) p=p->lchild;//沿左孩子左下
cout<<p->data;
while(p->RTag==1 && p->rchild!=T)//沿右线索访问后继结点
p=p->rchild;cout<<p->data;
p=p->rchild;//转向p的右子树
}
}
七、树和森林
树的三种表示
-
双亲表示法
描述:每个结点除了数据域data外,还附设一个parent域用以指示其双亲结点的位置
特点:在这次存储结构下,求结点的双亲十分方便,也很容易求树的根,但求结点的孩子时需要遍历整个结构 -
孩子表示法
描述:每个结点有多个指针域,其中每个指针指向一棵子树的根结点
特点:便于那些涉及孩子的操作的实现 -
双亲孩子表示法
描述:结合双亲表示法和孩子表示法 -
孩子兄弟法
描述:链表中结点的两个链域分别指向该结点的第一个孩子结点和下一个兄弟结点
特点:这种存储结构的优点是它和二叉树的二叉链表表示完全一样,便于将一般的树结构转换为二叉树进行处理,利用二叉树的算法来实现对树的操作
树和森林的遍历
- 树的遍历
1)先根遍历树:先访问树的根结点,然后依次先根遍历每棵子树
2)后根遍历树:先依次后根遍历每棵子树,然后访问根结点 - 森林的遍历
1)先序遍历森林
访问森林中第一棵树的根结点
先序遍历第一棵树的根结点的子树森林
先序遍历除去第一棵树之后剩余的树构成的森林
2)中序遍历森林
中序遍历森林中第一棵树的根结点的子树森林
访问第一棵树的根结点
中序遍历除去第一棵树之后剩余的树构成的森林
八、哈夫曼树及其应用
基本概念
在哈夫曼树中,权值越大的结点离根结点越近
哈夫曼树:又称最优树,是一类带权路径长度最短的树
路径:从树中一个结点到另一个结点直接的分支构成这两个结点之间的路径
路径长度:路径上的分支数目称作路径长度
权:赋予某个实体的一个量,是对实体的某个或某些属性的数值化描述
结点的带权路径长度:从该结点到树根之间的路径长度与结点上权的乘积
树的带权路径长度:树中所有叶子结点的带权路径长度之和
(待补充…)
九、利用二叉树求解表达式的解
表达式树的创建
- 初始化OPTR栈和EXPT栈,将表达式起始符”#“压入OPTR栈
- 扫描表达式,读入第一个字符ch,如果表达式没有扫描完毕至”#“或OPTR的栈顶元素不为”#“时,循环执行以下操作:
1)若ch不是运算符,则以ch为根创建一棵只有根结点的二叉树,且将该树根结点压入EXPT栈,读入下一字符ch
2)若ch是运算符,则根据OPTR的栈顶元素和ch优先级比较结果,做不同处理
若是小于,则ch压入OPTR栈,读入下一个字符ch
若是大于,则弹出OPTR栈顶的运算符,从EXPT栈弹出两个表达式子树的根结点,以该运算符为根结点,**以EXPT栈中弹出的第二个子树作为左子树,以EXPT栈中弹出的第一个子树作为右子树,创建一棵新二叉树,**并将该树根结点压入EXPT栈
若是等于,则OPTR的栈顶元素的”(“ 且 ch是”)“,这时弹出OPTR栈顶的”(“,相当于括号匹配成功,然后读入下一个字符ch
void InirExpTree(){
InitStack(EXPT);
InitStack(OPTR);
Push(OPTR,"#");
cin>>ch;
while(ch!="#"||GetTop(OPTR)!="#"){
if(In(ch)){//ch不是运算符
CreateExpTree(T,NULL,NULL,ch);
Push(EXPT,T);
cin>>ch;
}
else
switch(Precede(GetTop(OPTR),ch)){
case'<':
Push(OPTR,ch);cin>>ch;
break;
case'>':
Pop(OPTR,theta);
Pop(EXTR,b);Pop(EXTR,a);
CreateExpTree(T,a,b,theta);//以theta为根,a为左子树,b为右子树,创建一棵二叉树
Push(EXPT,T);
break;
case'=':
Pop(OPTR,x);cin>>ch;
break;
}
}
}
表达式树的求值——后序遍历二叉树的过程
- 设变量lvalue和rvalue分别用以记录表达式树中左子树和右子树的值,初始均为0
- 如果当前结点为叶子,则返回该结点的数值,否则执行以下操作:
1)递归计算左子树的值计为lvalue
2)递归计算右子树的值记为rvalue
3)根据当前结点运算符的类型,将lvalue和rvalue进行相应运算并返回
int EvaluateExpTree(BiTree T){
lvalue=rvalue=0;
if(T->lchild==NULL && T->rchild==NULL)
return T->data-'0';
else{
lvalue=EvaluateExpTree(T->lchild);
rvalue=EvaluateExpTree(T->rchild);
return Getvalue(T->data,lvalue,rvalue);
}
}