树和二叉树的定义
树的定义

-
树还可以表示为嵌套集合(类似韦恩图)、广义表、凹入表示(类似书的目录)。
树的基本术语

-
树的深度:树中结点的最大层次。
-
有序树:树中结点的各子树从左至右有次序。

二叉树的定义
使用二叉树的原因
二叉树的规律性强,且所有的树都可以转化为唯一对应的二叉树,实现较为简易的运算。
二叉树的定义和特点

二叉树和树是不同的概念

-
虽然二叉树和树的概念不同,但是有关树的术语对于二叉树都适用。
-
树和二叉树是不同的概念,所以二叉树不是树的特殊情形!!
二叉树的性质和存储结构
二叉树的性质1、2、3

-
第i层上至少有1个结点。

-
深度为k时至少有k个结点。


两种特殊形式的二叉树
满二叉树


完全二叉树


完全二叉树的性质(二叉树的性质4、5)


二叉树的存储结构
二叉树的顺序存储
-
实现:按满二叉树的结点层次编号,在数组中依次存放二叉树中的数据元素;
#define MAXSIZE 100
Typedef TElemType SqBiTree[MAXSIZE]
SqBiTree bt;
-
缺点:若二叉树为右单支树,则存储密度过低,空间浪费大;
-
适用于存储满二叉树和完全二叉树。
二叉树的链式存储
-
图示:

typedef struct BiNode{//二叉链表
TElemType data;
struct BiNode *lchild,*rchild;//左右孩子指针
}BiNode,*BiTree;

typedef struct TriTNode{//三叉链表,用于在某个结点寻找双亲
TelemType data;
struct TriNode *lchild,*parent,*rchild;//增加了指向双亲的指针
}TriTNode,*TriTree;
遍历二叉树和线索二叉树
遍历二叉树
由遍历二叉树确定遍历序列
遍历
-
定义:顺着某一条搜索路径巡访问二叉树中的结点,使得每个结点均被访问一次,而且仅被访问一次(又称周游)。
-
目的:得到树中所有结点的一个线性序列。
-
用途:是树结构插入、删除、修改、查找和排序运算的前提,是二叉树一切运算的基础和核心。
-
方法:共三种。设访问根结点为D,遍历左子树为L,遍历右子树为R,且规定先左后右,则又三种情况:DLR(先序遍历)、LDR(中序遍历)、LRD(后序遍历)。
遍历二叉树算法描述

由遍历序列确定二叉树
已知先序和中序序列求而二叉树

已知中序和后序序列求二叉树

二叉树递归遍历算法
先序遍历算法
Status PreOrderTraverse(BiTree T){//使用二叉链表
if(T==NULL) return OK;//空二叉树
else{
visit(T);//访问根结点
PreOrderTraverse(T->lchild);//递归遍历左子树
PreOrderTraverse(T->rchild);//递归遍历右子树
}
}
中序遍历算法
Status InOrderTraverse(BiTree T){
if(T==NULL) return OK;
else{
PreOrderTraverse(T->lchild);
visit(T);
PreOrderTraverse(T->rchild);
}
}
后序遍历算法
Status PostOrderTraverse(BiTree T){
if(T==NULL) return OK;
else{
PreOrderTraverse(T->lchild);
PreOrderTraverse(T->rchild);
visit(T);
}
}
遍历算法的分析


二叉树非递归算法
中序遍历的非递归算法
Status InOrderTraverse(BiTree T){
BiTree p;
InitStack(S);
P=T;
while(p||!StackEmpty(S)){
if(p){
Push(S,p);
p=p->lchild;
}
else{
Pop(S,q);
printf("%c",q->data);
p=q->rchild;
}
}
return OK;
}
二叉树的层次遍历
-
层次遍历:对于一棵二叉树,从根结点开始,按从上到下、从左到右的顺序访问每一个结点。使用队列来实现。
typedef struct{
BTNode data[MAXSIZE];//存放队中元素
int front,rear;//队头和队尾指针
}SqQueue;//顺序循环队列类型
void LevelOrder(BTNode *b){
BTNode *p;
SqQueue *qu;
InitQueue(qu);//初始化队列
enQueue(qu,b);//根结点指针进入队列
while(!QueueEmpty(qu)){//队不为空,则循环
deQueue(qu,p);//出队结点p
printf("%c",p->data);//访问结点p
if(p->lchild!=NULL) enQueue(qu,p->lchild);//有左孩子时将其进队
if(p->rchild!=NULL) enQueue(qu,p->rchild);//有右孩子时将其进队
}
}
二叉树遍历算法的应用
二叉树的建立
-
按先序遍历序列建立二叉树的二叉链表,按顺序读入字符ch:ABC##DE#G##F###
Status CreateBiTree(BiTree &T){
cin>>ch;
if(ch=="#") T=NULL;
else{
if(!(T=new BiTree)) exit(OVERFLOW);//开辟出链表空间
T->data=ch;//生成根节点
CreateBiTree(T->lchild);//构造左子树
CreateBiTree(T->rchild);//构造右子树
}
return OK;
}
复制二叉树
int Copy(BiTree T,BiTree &NewT){
if(T=NULL){
NewT=NULL;return 0;
}
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);
if(m>n) return(m+1);
else return(n+1);
}
}
计算二叉树结点总数
int NodeCount(BiTree T){
if(T=NULL)
return 0;
else
return NodeCount(T->lchild)+NodeCount(T->rchild)+1;
}
计算二叉树叶子总数
int LeafCount(BiTree T){
if(T=NULL)
return 0;
if(T->lchild==NULL&&T->rchild==NULL)
return 1;
else
return LeafCount(T->lchild)+LeafCount(T->rchild);
}
线索二叉树
-
用于寻找特定遍历序列中二叉树结点的前驱和后继;
-
利用二叉链表中的空指针域,左空则将其指向前驱,右空则将其指向后继;
-
改变指向的指针称为线索,加上线索的二叉树称为线索二叉树,对二叉树按某种遍历次序使其变为线索二叉树的过程叫线索化。
-
为区分lchild指针和rchild指针到底是指向孩子还是指向前驱后继,对二叉链表中每个结点增设两个标志域ltag和rtag:


typedef struct BiThrNode{
int data;
int ltag,rtag;
struct BiThrNode *lchild,*rchild;
}BiThrNode,*BiThrTree;
-
在中序遍历的情况下,序列头尾结点会各剩下一个空指针域,为避免这种悬空态,增设一个头结点,对于这个头结点来说:

树和森林
树的存储结构
双亲表示法
-
实现:定义结构数组,存放树的结点,每个结点含两个域;
-
数据域:存放结点本身信息;
-
双亲域:指示本结点的双亲结点在数组中的位置;
-
特点:找双亲容易,找孩子难。

typedef struct PTNode{//结点结构定义
TElemType data;
int parent;//双亲位置域
}PTNode;
#define MAX_TREE_SIZE 100
typedef struct{//树结构定义
PTNode nodes[MAX_TREE_SIZE];
int r,n;//根结点的位置和结点个数
}PTree;
孩子链表
-
实现:用单链表存储指向每一个结点的指针,每个非叶子结点的指针又作为一个链表的头指针,后接它的孩子结点的指针,如下:

孩子兄弟表示法
-
又称二叉树表示法、二叉链表表示法
-
实现:用二叉链表作树的存储结构,链表中每个结点的两个指针域分别指向其第一个孩子结点和下一个兄弟结点。
typedef struct CSNode{
ElemType data;
struct CSNode *firstchild,*nextsibling;
}CSNode,*CSTree;

树与二叉树的转换
将树转换成二叉树
-
树变二叉树:兄弟相连留长子
-
加线:在兄弟之间加一条线
-
抹线:对每一个结点,除了其左孩子外,去除与其余孩子之间的关系
-
旋转:以树的根结点为轴心,将横线顺时针转45°

将二叉树转换成树
-
左孩右右连双亲,去掉原来右孩线。
-
加线:若某结点是双亲结点的左孩子,则将其右孩子、右孩子的右孩子、......沿分支找到的所有右孩子,都与该结点的双亲用线连起来
-
抹线:抹掉原二叉树中双亲与右孩子之间的连线
-
调整:将结点按层次排列,形成树结构

森林与二叉树的转换
将森林转换成二叉树
-
树变二叉根相连
-
将各棵树分别转换成二叉树
-
将每棵树的根结点用线相连
-
以第一棵树根结点为二叉树的根,再以根结点为轴心,顺时针旋转,构成二叉树型结构

将二叉树转换成森林
-
去掉全部右孩线,孤立二叉再还原
-
抹线:将二叉树中根结点与其右孩子连线、及沿右分支搜索到的所有右孩子间连线全部抹掉,使之变成孤立的二叉树
-
还原:将孤立的二叉树还原成树

树与森林的遍历
树的遍历
先序、后序、按层次
森林的遍历
-
先序遍历:依次从左至右对森林中的每一课树进行先序遍历
-
后序遍历:依次从左至右对森林中的每一课树进行后序遍历
哈夫曼树及其应用
-
引例:


哈夫曼树的基本概念
-
路径:从树中一个结点到另一个结点之间的分支构成这两个结点间的路径。
-
结点的路径长度:两结点路径上的分支数。
-
树的路径长度:从树根到每一个结点的路径长度之和,记作:TL

-
权(Weight):将树中结点赋给一个有着某种含义的数值,则这个数值称为该结点的权。
-
结点的带权路径长度:从根结点到该结点之间的路径长度与该结点的权的乘积。
-
树的带权路径长度:树中所有叶子结点的带权路径长度之和,



-
满二叉树不一定是哈夫曼树;
-
哈夫曼树中权越大的叶子离根越近;
-
具有相同带权结点的哈夫曼树不唯一。
哈夫曼树的构造算法
哈夫曼算法


-
在哈夫曼算法中,初始时有n棵二叉树,要经过n-1次合并最终形成哈夫曼树。
-
经过n-1次合并产生n-1个新结点,且这n-1个新结点都是具有两个孩子的分支结点。
-
可见,哈夫曼树中共有2n-1个结点,且其所有的分支结点的度均不为1.
哈夫曼树构造算法的实现

typedef struct{
int weight;
int parent,lch,rch;
}HTNode,*HuffmanTree;
void CreatHuffmanTree(HuffmanTree HT,int n){//构造哈夫曼树——哈夫曼算法
if(n<=1) return;
m=2*n-1;//数组共2n-1个元素
HT=new HTNode[m+1];//0号单元未用,HT[m]表示根结点
for(i=1;i<=m;i++){
HT[i].lch=0;
HT[i].rch=0;
HT[i].parent=0;
}
for(i=1;i<=n;i++)
cin>>HT[i].weight;
//初始化结束,下面开始建立哈夫曼树
for(i=n+1;i<=m;i++){//合并产生n-1个结点————构造哈夫曼树,从第n+1个空间开始暂存合并的结点权值
Select(HT,i-1,s1,s2);//在HT[k](1<=k<=i-1)中选择两个其双亲域为0且权值最小的结点,并返回它们在HT中的序号s1和s2
HT[s1].parent=i;
HT[s2].parent=i;//表示从F中删除s1和s2
HT[i].lch=s1;
HT[i].rch=s2;//s1和s2分别作为i的左右孩子
HT[i].weight=HT[s1].weight+HT[s2].weight;//i的权值为左右孩子权值之和
}
}

哈夫曼编码
哈夫曼编码的概念




哈夫曼编码的算法实现
void CreateHuffmanCode(HuffmanTree HT,HuffmanCode &HC,int n){//从叶子到根逆向求每个字符的哈夫曼编码,存储在编码表HC中
HC=new char*[n+1];//分配n个字符编码的头指针矢量
cd=new char[n];//分配临时存放编码的动态数组空间
cd[n-1]='\0';//编码结束符
for(i=1;i<=n;i++){//逐个字符求哈夫曼编码
start=n-1;
c=i;
f=HT[i].parent;
while(f!=0){//从叶子结点开始向上回溯,直到根结点
start--;//回溯一次start向前指一个位置
if(HT[f].lchild==c) cd[start]='0';//结点c是f的左孩子,则生成代码0
else cd[start]='1';//结点c是f的右孩子,则生成代码1
c=f;
f=HT[f].parent;//继续向上回溯
}//求出第i个字符的编码
HC[i]=new char[n-start];//为第i个字符串编码分配空间
strcpy(HC[i],&cd[start]);//将求得的编码从临时空间cd复制到HC的当前行中
}
delete cd;//释放临时空间
}
文件的编码和解码
-
编码:
-
输入各字符及其权值
-
构造哈夫曼树HT[i]
-
进行哈夫曼编码
-
查HC[i],得到各字符的哈夫曼编码
-
解码:
-
构造哈夫曼树
-
依次读入二进制码
-
读入0,则走向左孩子;读入1,则走向右孩子
-
一旦到达某叶子时,即可译出字符
-
然后再从根出发继续译码,直到结束
习题
课本习题
-
(3)一棵完全二叉树上有1001 个结点,其中叶子结点的个数是( ) 。
A. 250 B . 500 C . 254 D . 501
答案:D
解析:遇到和结点数量有关的题目,首先想到的就是根据结点度数不同得到的两个等式,
n0+n1+n2=n(题目中n为1001)
n2+1=n0
同时还注意到,题目中的是一棵完全二叉树,对于完全二叉树而言,度为1的结点为0个或1个: n1=1 或n1=0
联立三个式子,容易得到n1=1,故n0=501。
-
(10)一棵非空的二叉树的先序遍历序列与后序遍历序列正好相反,则该二叉树一定满足( ) 。
A.所有的结点均无左孩子 B .所有的结点均无右孩子
C.只有一个叶子结点 D .是任意一棵二叉树
答案:C
解析:首先排除掉显然错误的D,而A和B如果其中一个正确的话,那么另一个也没错,所以两个也排除掉,剩下只有C了,可以记住这个结论。
-
(11)设哈夫曼树中有199 个结点,则该哈夫曼树中有( )个叶子结点。
A. 99 B. 100
C. 101 D. 102
答案:B
解析:在哈夫曼树中只有度为0(叶子结点)和度为2 的结点。设叶子结点的个数为n0,度为2 的结点的个数为n2,由二叉树的性质n0=n2+1 ,则总结点数n=n0+n2=2*n0-1 ,得到n0=100 。
-
(1)试找出满足下列条件的二叉树
① 先序序列与后序序列相同②中序序列与后序序列相同
③ 先序序列与中序序列相同④中序序列与层次遍历序列相同
先序遍历:“根—左子树—右子树” ,中序遍历:“左子树—根—右子树” ,后序遍历: “左子树—右子树―根",根据以上原则有:
1)若先序序列与后序序列相同,则或为空树,或为只有根结点的二叉树.
2)若中序序列与后序序列相同,则或为空树,或为任一结点至多只有左子树的二叉树.
3)若先序序列与中序序列相同,则或为空树,或为任一结点至多只有右子树的二叉树.
4)若中序序列与层次遍历序列相同,则或为空树,或为任一结点至多只有右子树的二叉树
期末题库
-
二叉树的基本形态有几种?
答案:5种
解析:
1.空树
2.只有一个根结点
3.根结点只有左子树
4.根结点只有右子树
5.根结点既有左子树又有右子树
以下说法正确的是( )。 | A. 若一个树叶是某二叉树前序遍历序列中的最后一个结点,则它必是该子树后序遍历序列中的最后一个结点。 | B. 若一个树叶是某二叉树前序遍历序列中的最后一个结点,则它必是该子树中序遍历序列中的最后一个结点。 | C. 在二叉树中,具有两个子女的父结点,在中序遍历序列中,它的后继结点最多只能有一个子女结点。 | D. 在二叉树中,具有一个子女的父结点,在中序遍历序列中,它没有后继子女结点。 |
答案:D
解析:A和B可以用最基本只有两三个结点的二叉树来判断,显然都是错的;C是对的;D不一定对。