一、树和二叉树的定义
1、树的定义
- 树形结构(非线性结构):结点之间有分支,具有层次关系
- 树是n(n≥0)个结点的有限集
- 若n=0,称为空树
- 若n>0,则它满足如下两个条件
- 有且仅有一个特定的称为根(Root)的结点
- 其余结点可分为m(m≥0)个互不相交的有限集T1,T2,T3,…,Tm,其中每一个集合本身又是一棵树,并称为根的子树(Sub Tree)
2、树的基本术语
- 结点:数据元素以及指向子树的分支
- 根结点:非空树无前驱结点的结点,只有一个
- 结点的度:结点拥有的子树数
- 树的度:树内各结点的度的最大值
- 叶子结点(终端结点):没有后继结点的结点
- 结点的子树的根称为该结点的孩子,该结点称为孩子的双亲
- 结点的祖先:从根到该结点所经分支上的所有结点
- 结点的子孙:从某结点为根的子树的任一结点
- 树的深度:树中结点的最大层次
- 有序树:树中结点的各子树从左至右有次序(最左边的为第一个孩子)
- 无序树:树中结点的各子树无次序
- 森林:是m(m≥0)棵互不相交的树的集合
3、二叉树的定义
- 二叉树是n(n≥0)个结点的有限集,它或者是空集(n=0),或者由一个根结点及两棵互不相交的分别称作这个根的左子树和右子树的二叉树组成
- 特点:
- 每个结点最多有两个孩子(二叉树中不存在度大于2的结点)
- 子树有左右之分,其次序不能颠倒
- 二叉树可以是空集合,根可以有空的左子树或空的右子树
二、二叉树的性质和存储结构
1、性质
- 在二叉树的第i层上至多有2i-1个结点(i≥1)
- 深度为k的二叉树至多有2k-1个结点(k≥1)
- 对任何一棵二叉树T,如果其叶子树为n0,度为2的结点数为n2,则n0=n2+1
- 具有n个结点的完全二叉树的深度为[log2n]+1
- 如果对一棵有n个结点的完全二叉树的结点按层序编号,则对任一结点i(1≤i≤n),有
- 如果i=1,则结点i是二叉树的根,无双亲;如果i>1,则其双亲是结点[i/2]
- 若2i>n,则结点i为叶子结点,无左孩子;否则,其左孩子是结点2i
- 如果2i+1>n,则结点i无右孩子;否则,其右孩子是结点2i+1
2、存储结构
typedef struct BiNode
{
TElemType data;
struct BiNode *Lchild,*Rchild;
}BiNode,*BiTree;
三、遍历二叉树和线索二叉树
- 遍历定义:顺着某一条搜索路径巡访二叉树中的结点,使得每个结点均被访问一次,而且仅被访问一次(又称周游)
- 目的:得到树中所有结点的一个线性排列
- 用途:它是树结构插入、删除、修改、查找和排序运算的前提,是二叉树一切运算的基础和核心
- 遍历规则:
- DLR——先(根)序遍历
- LDR——中(根)序遍历
- LRD——后(根)序遍历
- 若二叉树中各结点的值均不相同,则二叉树结点的先序序列、中序序列和后序序列都是唯一的
- 由二叉树的先序序列和中序序列,或由二叉树的后序序列和中序序列可以确定唯一一颗二叉树
1、遍历算法的实现
- 先序遍历
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
{
InOrderTraverse(T->lchild);
visit(T);
InOrderTraverse(T->rchild);
}
}
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;
}
- 后序遍历
status PostOrderTraverse(BiTree T)//递归
{
if(T==NULL)
return OK;
else
{
PostOrderTraverse(T->lchild);
PostOrderTraverse(T->rchild);
visit(T);
}
}
- 层次遍历
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);
if(p->lchild!=NULL)
enQueue(qu,p->lchild);
if(p->rchild!=NULL)
enQueue(qu,p->rchild);
}
}
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;
}
- 复制二叉树
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 LeadCount(BiTree T)
{
if(T=NULL)
return 0;
if(T->lchild==NULL&&T->rchild==NULL)
return 1;
else
return LeafCount(T->lchild)+LeafCount(T->rchild);
}
3、线索二叉树
- 线索:如果某个结点的左孩子为空,则将空的左孩子指针域改为指向其前驱;如果某结点的右孩子为空,则将空的右孩子指针域改为指向其后继的改变指向的指针
//结构体函数
typedef struct BiThrNode
{
int data;
int ltag,rtag;
struct BiThrNode *lchild,rchild;
}BiThrNode,*BiThrTree;
四、树的存储结构
1、树和森林
- 森林:是m(m≥0)棵互不相交的树的集合
2、双亲表示法
- 数据域:存放结点本身信息
- 双亲域:指示本结点的双亲结点在数组中的位置
ypedef struct PTNode
{
TElemType data;
int parent;
}PTNode;
#define MAX_TREE_SIZE 100
typedef struct
{
PTNode nodes[MAX_TREE_SIZE];
int r,n;
}PTree;
3、孩子链表
typedef struct CTNode //孩子结点结构
{
int child;
struct CTNode *next;
}*ChildPtr;
typedef struct //双亲结点结构
{
TElemType data;
ChildPtr firstchild;
}CTBox;
typedef struct //树结构
{
CTBox nodes[MAX_TREE_SIZE];
int n,r;
}CTree;
4、孩子兄弟表示法
typedef struct CSNode
{
ElemType data;
struct CSNode *firstchild,*nextsibling;
}CSNode,*CSTree;
五、树与二叉树的转换
1、将树转换成二叉树
- 加线:在兄弟之间加一连线
- 抹线:对每个结点,除了其左孩子外,去除其与其余孩子之间的关系
- 旋转:以树的根结点为轴心,将整树顺时针转45°
2、将二叉树转换成树
- 加线:若p结点是双亲结点的左孩子,则将p的右孩子,右孩子的右孩子…沿分支找到的所有右孩子,都与p的双亲用线连起来
- 抹线:抹掉原二叉树中双亲与右孩子之间的连线
- 调整:将结点按层次排序,形成树结构
六、森林与二叉树的转化
1、二叉树转换成森林
- 抹线:将二叉树中根结点与其右孩子连线,及沿有分支搜索到的所有右孩子间连线全部抹掉,使之变成孤立的二叉树
- 还原:将孤立的二叉树还原成树
七、树与森林的遍历
1、树的遍历
- 先根遍历:若树不空,则先访问根结点,然后依次先根遍历各棵子树
- 后根遍历:若树不空,则先依次访问后根遍历各棵子树,然后访问根结点
- 按层次遍历:若树不空,则自上而下自左至右访问树中每个结点
2、森林的遍历
- 先序遍历:若树不空,则
- 访问森林中第一棵树的根结点
- 先序遍历森林中第一棵树的子树森林
- 先序遍历森林中(除第一棵树外)其余树构成的森林
- 中序遍历:若树不空,则
- 中序遍历森林中第一棵树的子树森林
- 访问森林中第一棵树的根结点
- 中序遍历森林中(除第一棵树外)其余树构成的森林
八、哈夫曼树及其应用
1、哈夫曼树的基本概念
- 哈夫曼树:最优二叉树,带权路径长度最短的树
- 路径:从树中一个结点到另一个结点之间的分支构成这两个结点间的路径
- 结点的路径长度:两结点间路径上的分支数
- 数的路径长度:从树根到每一个结点的路径长度之和。记作TL
- 节点数目相同的二叉树中,完全二叉树是路径长度最短的二叉树
- 权(weight):将数中结点赋给一个有着某种含义的数值,则这个数值称为该结点的权
- 结点的带权路径长度:从根结点到该结点之间的路径长度与该结点的权的乘积
- 树的带权路径长度:树中所有叶子结点的带权路径长度之和
2、哈夫曼树的构造算法
- 根据n个给定的权值{w1,w2,…,wn}构成n棵二叉树的森林F={T1,T2,…,Tn},其中Ti只有一个带权为wi的根结点
- 在F中选取两颗根结点的权值最小的数作为左右子树,构造一颗新的二叉树,且设置新的二叉树的根结点的权值为其左右子树上根结点的权值之和
- 在F中删除这两棵树,同时将新得到的二叉树加入森林中
- 重复2/3,直到森林中只有一棵树为止,这棵树即为哈夫曼树
哈夫曼树的结点的度数为0或2,没有度为1的结点
包含n个叶子结点的哈夫曼树中共有2n-1个结点
包含n棵树的森林要经过n-1次合并才能形成哈夫曼树,共产生n-1个新结点
3、哈夫曼树构造算法的实现
采用顺序存储结构——一堆结构数组
结点类型定义
typedef struct
{
int weight;
int parent,lch,rch;
}HTNode,*HuffmanTree;
构造哈夫曼树
void CreatHuffmanTree(HuffmanTree HT,int n)
{
if(n<=1)
return;
m=2*n-1;
HT=new HTNode[m+1];
for(i=1;i<=m;++i)
{
HT[i].lch=0;HT[i].rch=0;HT[i].parent=0;
}
for(i=1;i<=n;++i)
scanf("s",HT[i].weight);
//&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&
// 初始化结束
//&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&
for(i=n+1;i<=m;i++)
{
select(HT,i-1,s1,s2);
HT[s1].parent=i; HT[s2].parent=i;
HT[i].lch=s1; HT[i].rch=s2;
HT[i]weight=HT[s1].weight+HT[s2].weight;
}
}
4、哈夫曼编码
-
将待传字符转换成二进制的字符串
-
A——00 B——01 C——10 D——11
-
若将编码设计为长度不等的二进制编码,即让待传字符串中出现次数较多的字符采用尽可能短的编码,则转换的二进制字符串便可能减少
关键:要设计长度不等的编码,则必须使任一字符的编码都不是另一个字符的编码的前缀——这种编码称做前缀编码
-
哈夫曼编码
-
统计字符集中每个字符在电文中出现的平均概率(概率越大,要求编码越短)
-
利用哈夫曼树的特点:权越大的叶子离根越近;将每个字符的概率值作为权值,构造哈夫曼树。则概率越大的结点,路径越短
-
在哈夫曼树的每个分支上标上0或1:
结点的左分支标0,右分支标1
把从根到每个叶子的路径上的标号连接起来,作为该叶子代表的字符的编码
性质:哈夫曼编码是前缀码;哈夫曼编码是最优前缀码
-
5、哈夫曼编码的算法实现
void CreatHuffmanCode(HuffmanTree HT,HuffmanCode &HC,int n)
{
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].parant;
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].parant;//继续向上回溯
}//求出第i个字符的编码
HC[i]=new char[n-start];//为第i个字符串编码分配空间
strcpy(HC[i],&cd[start]);//将求得的编码从临时空间cd复制到HC的当前行中
}
delete cd;//释放临时空间
}