文章目录
一、树和二叉树的定义
1.树的定义及基本术语
树:是n个节点的有限集,它或为空树(n=0),或为非空树。对于非空树T:(1)有且仅有一个称之为根的节点;(2)除根节点以外的其余节点可分为m(m>0)个互不相交的有限集T1,T2,…,Tm,其中每一个集合本身又是一颗树,并且称为根的子树。
节点:树中的一个独立单元。包含一个数据元素和若干指向其子树的分支。
节点的度:节点拥有的子树数称为节点的度。
树的度:树的度是树内各节点度的最大值。
叶子:度为0的节点称为叶子或终端节点。
非终端节点:度不为0的节点称为非终端节点或分支节点。
双亲和孩子:节点的子树的根称为该节点的孩子,相应地,该节点称为孩子的双亲。
兄弟:同一个双亲的孩子之间互称兄弟。
祖先:从根到该节点所经分支上的所以节点。
子孙:以某节点为根的子树中的任一节点都称为该节点的子孙。
层次:节点的层次从根开始定义,根为第一层,根的孩子为第二层。以此类推。
堂兄弟:双亲在同一层的节点互为堂兄弟。
树的深度:树中节点的最大层次称为树的深度或高度。
有序树和无序树:如果将树中节点的各子树看成从左至右是有次序的(不能互换),则称该树为有序树,否则为无序树。
森林:m棵互不相交的树的集合。对树中每个节点而言,其子树的集合即为森林。
2.二叉树的定义
二叉树:是n个节点所构成的集合,它或为空树,或为非空树。对于非空树T:(1)有且仅有一个称之为根的节点;(2)除根节点以外的其余节点分为两个互不相交的子集T1和T2,分别称为T的左子树和右子树,且T1和T2本身又都为二叉树。
二叉树的子树有左右之分,且次序不能任意颠倒;二叉树每个节点至多只有两棵子树
满二叉树:深度为k且含有2k-1个节点的二叉树。即除了叶节点,每个节点都有两棵子树。
完全二叉树:深度为k的、有n个节点的二叉树,当且仅当其每个节点都与深度为看的满二叉树中的编号从1到n的节点一一对应时,称之为完全二叉树。即每层的节点都是从左到右依次占位的,中间不会有空的位置,且只有在填完一层的情况下才能填下一层
二、二叉树的性质和存储结构(重点)
1.性质
①性质一:在二叉树的第i层上至多有2i-1个节点。可以用第一层想想
②性质二:深度为k的二叉树至多有2k-1个节点。这个就是等差数列求和
③性质三:对任意一棵二叉树T,如果其终端节点数为n0,度为2的节点数为n2,则n0=n2+1。
总节点数可以表示为度为1的节点n1,度为2的节点n2,度为3的节点n3,即n=n0+n1+n2;再观察连接两节点之间的线数B=n-1,节点n2可以提供两条连接线,节点n1可以通过一条,而节点n0不能提供一条,所以B=n1+2n2。联立上述三条式子可以得出n0=n2+1的结论
④性质四:具有n个节点的完全二叉树的深度为log2n+1,向下取整。主要靠记,因为情况很多
想象两种极端情况,一个是在深度为k时,完全二叉树呈现出k-1及以上层都满的情况下,第k层只有一个节点;另一个是在深度为k+1时,完全二叉树呈现出k及以上层都满的情况下,第k+1层只有一个节点。可以得出结论假设深度为k,那么2k-1<=n<2k(由性质二得出)。经过计算有k-1<=log2n<k。
⑤性质五:如果对一棵有n个节点的完全二叉树的节点按层序编号(从上到下,从左到右),则对任一节点i,以下结论成立:
(1)如果i=1,则节点i是二叉树的根;如果i>1,则其双亲PARENT(i)是节点[i/2]
(2)如果2i>n,则节点i无左孩子;否则其左孩子LCHILD(i)是节点2i
(3)如果2i+1>n,则节点i右孩子;否则其右孩子RCHILD(i)是节点2i+1
2.存储结构
①顺序存储结构
#define MAXTSIZE 100
typedef TElemType SqBiTree[MAXTSIZE];
SqBiTree bt;
利用结论五将完全二叉树上的节点编号对应于数组中的序号
为了避免空间浪费,顺序存储结构只适用于完全二叉树。对于一般二叉树,链式存储结构更合适
②链式存储结构
typedef struct BiTNode{
TElemType data;
struct BiTNode *lchild,*rchild;
}BiTNode,*BiTree;
注意二叉链表和三叉链表它们均属于二叉树的数据结构,只不过三叉链表多了一个指向父母节点的指针域
由性质三可以得出在含有n个节点的二叉链表中有n+1个空链域
BiTree指针一般指向根节点
三、遍历二叉树和线索二叉树
3.1遍历二叉树
1.遍历二叉树算法的描述
遍历二叉树:
按某条搜索路径巡访树中每个节点,使得每个节点均被访问一次,且仅被访问一次。访问的含义很广,包括输出节点的信息,对节点进行修改和运算等。
算法种类:
遍历的实质是对二叉树进行线性化,若限定先左后右,则有先(根)序遍历、中(根)序遍历、后(根)序遍历这三种情况
以下是三种遍历方法的递归算法定义:
①先序遍历:
(1)访问根节点;(2)先序遍历左子树;(3)先序遍历右子树
②中序遍历:
(1)中序遍历左子树;(2)访问根节点;(3)中序遍历右子树
③后序遍历:
(1)后序遍历左子树;(2)后序遍历右子树;(3)访问根节点
下面给出中序遍历的递归算法:
【算法描述】
void InOrderTraverse(BiTree T)
{//中序遍历二叉树T的递归算法
if(T)//若二叉树非空
{
InOrderTraverse(T->lchild);//中序遍历左子树
cout<<T->data;//访问根节点
InOrderTraverse(T->rchild);//中序遍历右子树
}
}
先序和后序只需将cout语句调换位置就好
下面给出中序遍历的非递归算法:
【算法步骤】
①初始化一个空栈S,指针p指向根节点
②申请一个节点空间q,用来存放栈顶弹出的元素
③当p非空或者栈S非空时,循环执行以下操作:
·如果p非空,则使p进栈,p指向该节点的左孩子;
·如果p为空,则弹出栈顶元素并访问根节点,将p指向该节点的右孩子
栈内存放的是指针
【算法描述】
void InOrderTraverse(BiTree T)
{//中序遍历二叉树T的非递归算法
InitStack(S);p=T;
q=new BiTNode;//也可以用BiTNode* q来定义q,
while(p||!StackEmpty(S))//在p空并且栈内为空时结束循环
{
if(p)//p非空
{
Push(S,p);//根指针进栈
p=p->lchild;//根指针进栈,遍历左子树
}
else//p为空
{
pop(S,q);//退栈
cout<<q->data;//访问根节点
p=q->rchild;//遍历右子树
}
}
}
前序遍历的非递归算法在次基础上修改即可;后序遍历比较麻烦,左右根的顺序可能比较难实现,但根右左的顺序容易实现,所以我们可以用一个栈保存根右左的结果,然后在依次弹出栈内的元素达到逆序的效果。当然还需要一个栈作为根右左的递归工作栈
2.根据遍历序列确定二叉树
【要点】
①只有先序遍历和中序遍历、中序遍历和后序遍历这两种组合才能唯一确定一个二叉树
②先序中序的组合通过先序序列判断根节点(根据先序的特点第一个就是),再通过中序序列判断左右子树的子孙(在根节点的左边是左子树的子孙),然后再通过先序序列判断子树的根节点,以此类推
③中序后序的组合通过后序序列判断根节点(根据后序的特点最后一个就是),再通过中序序列判断左右子树的子孙(在根节点的左边是左子树的子孙),然后再通过后序序列判断子树的根节点,以此类推
【例子】
已知一棵二叉树的中序序列和后序序列分别是BDCEAFHG和DECBHGFA,请确认出该二叉树。
3.二叉树遍历算法的应用
(1)创建二叉树的存储结构——二叉链表
以先序遍历为例,输入先序遍历的序列(需要预先算出),以#作为空树的标志(可表示一整棵树都是空的也可以表示子树是空的),最后为了方便,节点元素均为单字母
【算法步骤】
①查找字符序列,读入字符ch
②如果ch是一个“#”字符,则表明该二叉树为空树,即T为NULL;否则执行以下操作:
·申请一个节点空间T;
·将ch赋给T->data;
·递归创建T的左子树
·递归创建T的右子树
【算法描述】
void CreateBiTree(BiTree &T)
{
cin>>ch;
if(ch=='#') T=NULL;//递归结束,建空树
else//递归创建二叉树
{
T=new BiTNode;//生成根节点
T->data=ch;//根节点数据域置为ch
CreateBiTree(T->lchild);//递归创建左子树
CreateBiTree(T->rchild);//递归创建右子树
}
}
可以想想ABC##DE#G##F###是什么二叉树
(2)复制二叉树
【算法步骤】
如果是空树,递归结束,否则执行以下操作:
·申请一个新节点空间,复制根节点;
·递归复制左子树
·递归复制右子树
复制函数的实现与二叉树先序遍历的实现非常类似
【算法描述】
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);//递归复制右子树
}
}
(3)计算二叉树的深度
二叉树的深度为树中节点的最大层次,二叉树的深度为左右子树深度的较大者加1
【算法步骤】
如果是空树,递归结束,深度为0,否则执行以下操作:
·递归计算左子树的深度记为m;
·递归计算右子树的深度记为n;
·如果m大于n,二叉树的深度为m+1,否则为n+1
【算法描述】
int Depth(BiTree T)
{
if(T==NULL) return 0;//如果是空树,深度为0,递归结束
else
{
m=Depth(T->lchild);//递归计算左子树的深度记为m
n=Depth(T->rchild);//递归计算右子树的深度记为n
if(m>n)return (m+1);//二叉树的深度为m与n的较大者加1
else return (n+1);
}
}
(4)统计二叉树中节点的个数
如果是空树,则节点个数为0,递归结束;否则,节点个数为左子树的节点个数加上右子树的节点个数再加上1
【算法描述】
int NodeCount(BiTree T)
{
if(T==NULL) return 0;//如果是空树,则节点个数为0,递归结束
else return NodeCount(T->lchild)+NodeCount(T->rchild)+1;
//否则节点个数为左子树的节点个数+右子树的节点个数+1
}
3.2线索二叉树
1.线索二叉树的基本概念
引入线索二叉树是为了保存在遍历二叉树过程中得到的有关前驱和后驱的信息
规定:若节点有左子树,则其lchild域指向其左孩子,否则指向前驱;若节点有右子树,则其rchild域指向其右孩子,否则指向后继。因此节点形式变成如下形式
lchild | LTag | data | RTag | rchild |
---|
其中:
L
T
a
g
=
{
0
,
l
c
h
i
l
d
域指示节点的左孩子
1
,
l
c
h
i
l
d
域指示节点的前驱
\begin{array}{l} LTag= \left\{\begin{matrix} 0,lchild域指示节点的左孩子\\ 1,lchild域指示节点的前驱 \\ \end{matrix}\right. \end{array}
LTag={0,lchild域指示节点的左孩子1,lchild域指示节点的前驱
R
T
a
g
=
{
0
,
r
c
h
i
l
d
域指示节点的右孩子
1
,
r
c
h
i
l
d
域指示节点的后继
\begin{array}{l} RTag= \left\{\begin{matrix} 0,rchild域指示节点的右孩子\\ 1,rchild域指示节点的后继 \\ \end{matrix}\right. \end{array}
RTag={0,rchild域指示节点的右孩子1,rchild域指示节点的后继
二叉树的二叉线索类型定义如下:
typedef struct BiThrNode
{
TElemType data;
struct BiThrNode *lchild,*rchild;
int LTag,RTag;
}BiThrNode,*BiThrTree;
这种节点构成的二叉链表叫做线索链表,其中指向节点前驱和后继的指针叫线索,对二叉树以某种次序遍历使其变为线索二叉树的过程加作线索化。
2.构造线索二叉树
线索化的过程即在遍历的过程中修改空指针的工程。不同的遍历次序会得到不同的线索二叉树,以下介绍中序线索化的算法
因为要记录先后关系,所以设一个指针pre始终指向刚刚访问过的节点,而指针p指向当前访问的节点
算法1 以节点p为根的子树中序线索化
【算法步骤】
①如果p非空,左子树递归线索化
②如果p的左孩子为空,则给p加上左线索,将其LTag置为1,让p的左孩子指针指向pre(前驱);否则将p的LTag置为0
③如果pre的右孩子为空,则给pre加上右线索,将其RTag置为1,让pre的右孩子指针指向p(后继);否则将pre的RTag置为0
④将pre指向刚访问过的节点p,即pre=p
⑤右子树递归线索化
【算法描述】
void InThreading(BiThrTree p)
{//pre是全局变量,初始化时其右孩子指针为空,便于在树的最左点开始建线索。
//若有头节点,则pre一开始指向头节点
if(p)
{
InThreading(p->lchild);//左子树递归线索化
if(!p->lchild)//p的左孩子为空
{
p->LTag=1;//给p加上左线索
p->lchild=pre;//p的左孩子指针指向pre
}
else p->LTag=0;
if(!pre->rchild)//pre的右孩子为空
{
pre->RTag=1;//给pre加上右线索
pre->rchild=p;//pre的右孩子指针指向p
}
else pre->RTag=0;
pre=p;//保持pre指向p
InThreading(p->rchild);//右子树递归线索化
}
}
算法1是对树中任意一个以节点p为根的子树进行中序线索化的过程
先序线索化和后序线索化只需对InTreading(p->lchild)和InTreading(p->rchild)进行位置的改变即可,参考遍历二叉树的递归算法
算法2 带头节点的二叉树中序线索化
【算法描述】
void InOrderThreading(BiThrTree &Thrt,BiThrTree 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;//头节点的左孩子指向根,pre初值指向头节点
InThreading(T);//调用算法1,对T为根的二叉树进行中序线索化
pre->rchild=Thrt;
pre->RTag=1;//算法1结束后,pre为最右节点,pre的右线索指向头节点
}
}
该线索主要是对头节点进行操作,使得二叉树在逻辑上成为一个双向链表
3.遍历线索二叉树
(1)在中序线索二叉树中查找
①查找p指针所指节点的前驱:
·若p->LTag为1,则p的左链指示其前驱;
·若p->LTag为0,则说明p有左子树,节点的前驱是遍历左子树时访问的最后一个节点(左子树中最右下的节点)
②查找p指针所指节点的后继:
·若p->RTag为1,则p的右链指示其后继;
·若p->RTag为0,则说明p有右子树,节点的后继是遍历右子树时访问的第一个节点(左子树中最左下的节点)
(2)在先序线索二叉树中查找
①查找p指针所指节点的前驱:
·若p->LTag为1,则p的左链指示其前驱;
·若p->LTag为0,则说明p有左子树。此时p的前驱有两种情况:若p是双亲的左孩子,则双亲节点为前驱;否则就是双亲节点的左子树上先序遍历最后访问的节点(一般是左子树中最右下的节点)
②查找p指针所指节点的后继:
·若p->RTag为1,则p的右链指示其后继;
·若p->RTag为0,则说明p有右子树。p的后继必为其左子树根(若存在)或右子树根,而且左子树根的优先级更高
(3)在后序线索二叉树中查找
①查找p指针所指节点的前驱:
·若p->LTag为1,则p的左链指针其前驱;
·若p->LTag为0,当p->RTag也为0时,p的右链指示其前驱;若p->RTag为1时,p的左链指示其前驱
②查找p指针所指节点的后继情况比较复制,分以下情况讨论:
·若p是二叉树的根,则后继为空
·若p是双亲的右孩子,则其后继为双亲节点
·若p是双亲的左孩子,且p没有右兄弟,则后继为双亲节点
·若p是双亲的左孩子,且p有右兄弟,则其后继为双亲右子树上按后序遍历列出的第一个节点(右子树中最左下的叶节点)
可见在先序线索化树上找前驱或者在后序线索化树上找后继都比较复杂,此时若需要可以多建立几个指针域
以下介绍遍历中序线索二叉树的算法。
【算法步骤】
①指针p指向根节点
②p为非空树或遍历未结束时,循环执行以下操作:
·沿左孩子向下,到达最左下节点p,它是中序的第一个节点;
·访问p
·沿右线索反复查找当前节点p的后继节点并访问后继节点,直至右线索为0或者遍历结束
·转向p的右子树(如果每这个就会陷入死循环)
【算法描述】
void InOrderTraverse_Thr(BiThrTree T)
{//T指向头节点,头节点的左链lchild指向根节点
p=T->lchild;//p指向根节点
while(p!=T)//空树或者遍历结构时,p==T
{
while(p->LTag=0) p=p->child;//沿左孩子向下
cout<<p->data;//访问其左子树为空的节点
while(p->RTag==1&&p->rchild!=T)
{
p=p->rchild;cout<<p->data;//沿右线索访问后继节点
}
p=p->rchild;//转向p的右子树
}
}
同样遵循一个左根右的顺序,指示根的访问直接由后继指出了。当到了一个有右子树的节点时,就轮到“右”的回合了,所以要进入到右子树中
【算法分析】
遍历线索二叉树的时间复杂度是O(n),空间复杂度是O(1),因为不用栈
四、树和森林
4.1树的存储结构
1.双亲表示法
以一组连续的存储单元存储树的节点(可以理解为数组),每个节点不仅有数据域,还多了设了一个parent域指向双亲节点的位置
类似于二叉树的顺序存储结构,只不过这里不用为树空出的节点位置而空出数组的位置
这种存储结构分别查找双亲,但求节点的孩子需要遍历整个结构
2.孩子表示法
把每个节点的孩子节点排列起来,看成一个线性表,以单链表作为存储结构;而n个头指针又组成一个线性表,采用顺序存储结构
3.孩子兄弟表示法
又称二叉树表示法。在链表中节点的两个链域分别指向该节点的第一个孩子节点和下一个兄弟节点,分别命名为firstchild域和nextsibling域
typedef struct CSNode{
ElemType data;
struct CSNode *firstchild,*nextsibling;
}CSNode,*CSTree;
4.2森林与二叉树的转换
从树的二叉链表表示的定义可知,任何一棵和树对应的二叉树,其根节点的右子树必为空。若把森林中的第二棵树的根节点看成第一棵树的根节点的兄弟,则同样可以导出森林和二叉树的对应关系
1.森林转换成二叉树
如果F={T1,T2,…,Tm}是森林 ,则可按如下规则将其转换成一棵二叉树B=(root,LB,RB):
(1)若F为空,即m=0,则B为空树;
(2)若F非空,即m>0,则B的根root即森林中第一棵树的根ROOT(T1);B的左子树LB是从T1中根节点的子树森林F1={T11,T12,…,T1m}转换而成的二叉树;其右子树RB是从森林F’={T2,T3,…,Tm}转换而成的二叉树
2.二叉树转换成森林
如果B=(root,LB,RB)是一棵二叉树,则可按如下规则将其转换成森林F={T1,T2,…,Tm}:
(1)若B为空,则F为空;
(2)若B非空,则F中第一棵树T1的根ROOT(T1)即为二叉树B的根root;T1中根节点的子树森林F1是由B的左子树LB转换而成的森林;F中除T1之外其余树组成的森林F’={T2,T3,…,Tm}是由B的右子树RB转换而成的森林
4.3树和森林的遍历
1.树的遍历
①先根遍历:
类似于二叉树中的先序遍历,即先访问根,然后再依次从左至右递归访问子树
②后根遍历:
类似于二叉树中的后序遍历,即先依次从左至右递归访问子树,然后再访问根
以树的存储结构中的树图为例
先根遍历的结果是:RADEBCFGHK
后根遍历的结果是:DEABGHKFCR
2.森林的遍历
(1)先序遍历森林:
若森林非空,则可按下述规则遍历:
①访问森林中第一棵树的根节点;
②先序遍历第一棵树的根节点的子树森林;
③先序遍历除第一棵树之后剩余的树构成的森林
类似于二叉树的先序遍历,先根后依次从左至右递归访问
(2)中序遍历森林:
若森林非空,则可按下述规则遍历:
①中序遍历森林中第一棵树的根节点的子树森林;
②访问第一棵树的根节点
③中序遍历除去第一棵树之后剩余的树构成的森林
其实就是后根遍历
以“森林与二叉树的转换”的图为例
先序遍历的结果是:ABCDEFGHIJ
后序遍历的结果是:BCDAFEHJIG
五、哈夫曼树及其应用
5.1哈夫曼树的基本概念
路径:从树中一个节点到另一个节点之间的分支构成这两个节点之间的路径
路径长度:路径上的分支数目称作路径长度
树的路径长度:从树根到每一叶子节点的路径长度之和
权:赋予某个实体的一个量,是对实体的某个或某些属性的数值化描述。(实体分为节点和变,所以有节点权和边权)
节点的带权路径长度:从该节点到树根之间的路径长度与节点上权值的乘积
树的带权路径长度:树中所有叶子节点的带权路径长度之和。记作WPL
哈夫曼树:假设有m个权值{w1,w2,…,wm}可以构造一棵含n个叶子节点的二叉树,每个叶子节点(只能是叶子节点有权值)的权值为wi,则其中带权路径长度WPL最小的二叉树称作最优二叉树或哈夫曼树。
可以发现,在哈夫曼树中权值越大的节点离根节点越近,也就是说从该节点到根的路径最短
5.2哈夫曼树的构造算法(重点)
基本思想就是在森林中每次都选出根节点权值最小的两个树使它们合并成一棵新的二叉树,选出来的两棵树分别作为新树根的左右孩子,树根的权值为左右孩子权值的和,以此循环执行,直到森林变为一棵树
1.哈夫曼树的存储方法
哈夫曼树是一种二叉树,所以可以采用前面介绍过的通用存储方式,而哈夫曼树没有度为1的节点(想想如何构造的就知道为什么了),根据二叉树的特性有以下结论,n个叶子节点的哈夫曼树共有2n-1个节点,因此可以存储在一个大小为2n-1的一维数组中。树中的每个节点还要包含其双亲信息和孩子节点的信息。
weight | parent | lchild | rchild |
---|
//哈夫曼树的存储表示
typedef struct{
int weight;
int parent,lchild,rchild;
}HTNode,*HuffmanTree;
为了方便实现,数组的0号位不适用,从1号位开始使用,所以数组大小为2n。将叶子节点集中存储在前面部分的n个位置,而后面的n-1个位置存储其余叶子节点。
2.构造哈夫曼树
【算法步骤】
①初始化:首先动态申请2n个单元;然后循环2n-1次,从1号单元开始,依次将1至2n-1所有单元中的双亲、左孩子、右孩子的下标都初始化为0;然后循环n次,输入前n个单元中叶子节点的权值。
②创建树:循环n-1次,通过n-1次的选择、删除与合并来创建哈夫曼树。选择是从当前森林中选择双亲为0且权值最小的两个树节点s1和s2;删除是指将节点s1和s2的双亲改为非0;合并就是将s1和s2的权值和作为一个新节点的权值依次存入数组的n+1号及之后的单元中,同时记录这个新节点左孩子的下标为s1,右孩子的小标为s2.
【算法描述】
void CreateHuffmanTree(HuffmanTree &HT,int n)
{
if(n<=1) return;
m=2*n-1;
HT=new HTNode[m+1];//0号单位为使用,所以需要分配m+1个单位,HT[m]表示根节点
for(i=1;i<=m;i++)//将1~m号单位中的双亲、左孩子、右孩子的下标都初始化为0
{HT[i].parent=0;HT[i].lchild=0;HT[i].rchild=0;}
for(i=1;i<=n;i++) cin>>HT[i].weight;//输入前n个单元中叶子节点的权值
/*--------初始化工作结束,下面开始创建哈夫曼树------------*/
for(i=n+1;i<=m;i++)
{//通过n-1次的选择、删除、合并来创建哈夫曼树
Select(HT,i-1,s1,s2);
//在1~i-1中选择两个双亲为0且权值最小的节点,并返回它们的序号s1和s2
HT[s1].parent=i;HT[s2].parent=i;
//得到新节点i,从森林中删除s1,s2,将s1和s2的双亲域改为i
HT[i].lchild=s1;HT[i].rchild=s2;//s1,s2分别作为i的左右孩子
HT[i].weight=HT[s1].weight+HT[s2].weight;//i的权值为左右孩子权值的和
}
}
3.哈夫曼编码
1.哈夫曼编码的主要思想
出现次数较多的字符编以较短的编码。并规定在哈夫曼树中左分支标记为0,右分支标记为1,则根节点到每个叶子节点路径上的0、1序列即相应字符的编码。
哈夫曼编码有以下性质:
(1)哈夫曼编码是前缀编码:前缀编码就是任一个编码都不是其他任何编码的前缀。因为在哈夫曼树中只有叶节点是具有信息的,而任何一个叶节点都不会是其他叶节点的双亲节点。
(2)哈夫曼编码是最优前缀码:因为出现频率(权值)越高的字符越靠近根。
2.哈夫曼编码的算法实现
依次以叶子为出发点,向上回溯至根节点为止。回溯时走左分支则生成代码0,走右分支则生成代码1。由于哈夫曼编码是变长编码,所以用一个指针数组来存放每个字符编码串。
//哈夫曼编码表的存储表示
typedef char **HuffmanCode;//动态分配数组存储哈夫曼编码表
用HuffmanCode定义一个数组HC用来存放各字符的哈夫曼编码,为了实现方便,数组的0号单元不使用,从1号开始,所以HC的大小为n+1。然后分配一个长度为n的一位数组cd来临时存放第i个字符的编码(字符编码长度一定小于n),最后复杂给HC[i]。
由于求解编码时是从叶节点向根节点出发的,所以得到的编码时是倒过的,为了让编码正向我们倒过来存放,即第一个编码存放在cd[n-2]中(cd[n-1]存放字符串结束标志’\0’),第二个编码存放在cd[n-3]中,依此类推。
【算法步骤】
①分配存储n个字符编码的编码表空间HC,长度为n+1;分配临时存放每个字符编码的动态数组空间cd,cd[n-1]置为‘\0’
②逐个求解n个字符的编码,循环n次,执行以下操作:
·设置变量start用于记录编码在cd中存放的位置,start初始时指向最后,即编码结束符位置n-1;
·设置变量c用于记录从叶子节点向上回溯至根节点所经过的节点的下标,c初始时为当前待编码字符的下标i,f用于记录i的双亲节点的下标;(c和f用于判断左右孩子)
·从叶节点向上回溯至根节点,求得字符i的编号,当f没有到达根节点时,循环执行以下操作:
->回溯一次start向前指一个位置,即–start;
->若节点c是f的左孩子,则生成代码0,否则生成代码1,生成的代码0或1保存在cd[start]中;
->继续向上回溯,改变c和f的值(c=f而f=HT[f].parent)
·根据数组cd的字符串长度为第i个字符编码分配空间HC[i],然后将数组cd中的编码复制到HC[i]中
③释放临时空间cd
【算法描述】
void CreateHuffmanCode(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;//start开始时指向最后,即编码结束符位置
c=i;f=HT[i].parent;//f指向节点c的双亲节点
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;//继续向上回溯
}
HC[i]=new char[n-start];
//为第i个字符编码分配空间(start拥有指向开头,所以分配的空间是n-1-start+1)
strcpy(HC[i],&cd[start]);//将两地址作为参数,来对HC[i]赋值
}
delete cd;//释放cd空间
}
3.文件的编码和译码
(1)编码
依次读入文件中的字符c,在哈夫曼编码表HC中找到次字符,将字符c转换为编码表中的编码串
(2)译码
依次读入文件的二进制码,从哈夫曼树的根节点(HT[m])出发,若读入0,则走向左孩子,否则走向右孩子。一旦到达某一叶子HT[i]时便译出相应的字符编码HC[i]。然后重新从根出发继续译码,直至文件结束。
六、案例分析与实现
案例1:利用二叉树求解表达式的值
【算法步骤】
①设变量lvalue和rvalue分别用以记录表达式树中左子树和右子树的值,初始均为0
②如果当前节点为叶子(节点为操作数),则返回该节点的数值,否则(节点为运算符)指向以下操作:
·递归计算左子树的值,记为lvalue
·递归计算右子树的值,记为rvalue
·根据当前节点运算符的类型,将lvalue和rvalue进行相应运算并返回
【算法描述】
int EvaluateExpTree(BiTree T)
{
lvalue=rvalue=0;
if(T->lchild=NULL&&T->rchild==NULL)
return T->data;
else
{
lvalue=EvaluateExpTree(T->lchild);
rvalue=EvaluateExpTree(T->rchild);
return GetValue(T->data,lvalue,rvalue);//这里只传值,但不改变值
}
}
总结
学习完本章后,读者应掌握二叉树的性质和存储结构,熟练掌握二叉树的前、中、后序遍历算法,掌握线索化二叉树的基本概率和构造方法;熟练掌握哈夫曼树和哈夫曼编码的构造方法;能够利用树的孩子兄弟表示法将一般的树结构转换为二叉树进行存储;掌握森林与二叉树之间的转换方法。