5.1.1树和二叉树的定义
树:是n(n>=0)个结点的有限集,或为空树(n==0),或为非空树
非空树满足:1.有且仅有一个称之为根的结点
2.除根节点之外的其余结点可分为m(m>0)个互不相交的有限集,其中每个集合本身又是一棵树,并且称为根的子树。(递归定义)
5.1.2树的基本术语:
1.结点:树中的一个独立单元,包含一个数据元素和若干个指向子树的分支
2.根结点:非空树中无前趋的结点
3.结点的度:结点拥有的子树个数为结点的度
4.树的度:树内各结点度的最大值
5.叶子:度为0的结点,也称为终端结点
6.非终端结点:度不为0的结点称为非终端结点或分支节点,除根节点之外的非终端结点也作内部结点
7.双亲和孩子:根结点的子树的根称为该结点的孩子,对应结点称为孩子的双亲
8.兄弟:同一个双亲的孩子之间互称兄弟
9.祖先:从根结点到该结点路径上的所有结点
10.子孙:以某结点为根的子树中的所有节点都为该结点的子孙
11.层次:结点的层次从根定义,根为第一层,以此类推
12.堂兄弟:双亲在同一层的结点互称堂兄弟
13.树的深度:树中结点的最大层次为树的深度
14.有序树和无序树:如果将树内各结点看成从左至右有次序(不能互换),则为有次序,否则为无次序
15.森林:是m(m>=0)棵互不相交的树的集合
一棵树可以看成特殊的森林——根结点删去,树就变成森林
给森林各子树加上一个双亲结点,森林就变成了树
5.1.3二叉树的定义(所有树都可以转化为唯一对应的二叉树)
二叉树是n(n>=0)个结点构成的集合,或为空树,或为非空树。对于非空二叉树,
满足1.有且仅有一个称为根的结点
2.除根节点外其余结点都分成两个互不相交的子集,分别成为树的左子树和右子树,且子树本身又是二叉树
二叉树和树的区别:
1.二叉树每个结点至多有两棵子树(不存在度>2的结点)
2.二叉树的子树有左右之分,次序不能任意颠倒(但并不是有序树)
注:二叉树不是树的特殊情况,而是两种不同的概念
子树需要区分左右,即使只有一颗子树
树当结点只有一个孩子时,无需区分左右次序
如果是树,则仅有两种。因为树的子树不区分左右
5.4二叉树的性质和存储结构
5.4.1二叉树的性质
性质1:二叉树的第i层上最多有2^(i-1)个结点,最少有一个结点
性质2:深度为k的二叉树至多有2^k-1个结点,最少有k个结点
性质3:对任何一颗二叉树,如果其叶子个数为n0,度为2的结点为n2,则有n0 = n2 + 1
满二叉树和完全二叉树
1.满二叉树:深度为k且有2^k-1个结点的二叉树
满二叉树的特点:1.每一层上的结点数都是最大结点数,即2^(i-1)
2.叶子结点全在最底层
对满二叉树进行编号:从上自下,从左至右进行连续编号,其中根结点为1
2.完全二叉树:深度为k,有n个结点的二叉树,当且仅当其每个结点的编号都和深度为k的满二叉树的从1到n的连续编号一一对应,称为完全二叉树
完全二叉树的特点:1.叶子只可能在最大的两层出现
2.对任一结点,若其右分支下子孙最大深度为l,则左分支子孙最大深度必为l或l+1
注:1.满二叉树一定是完全二叉树,但完全二叉树不一定是满二叉树
2.在满二叉树中,从最后一个结点连续去掉任意结点后,即得到一颗完全二叉树
性质4:具有n个结点的完全二叉树的深度为 log(2)(n)(取底)+1
性质5:对有n个结点的完全二叉树的结点按层序编号,则对任一结点i(1=
1.如果i=1,则结点为根,无双亲。如果i>1,则其双亲结点为(i/2)(取底)
2.若2i>n则该结点没有左孩子,否则左孩子是结点2i
3.若2i+1>n,则该结点没有右孩子,否则右孩子是结点2i+1.
5.4.2二叉树的存储结构
一.顺序存储结构
按满二叉树的结点层序编号,依次放入二叉树中的数据元素
#define MAXSIZE 100
typedef TEmeltype SqBiTree[MAXSIZE];
SqBiTree bt;
可以发现,顺序存储结构只适用于完全二叉树,对一般二叉树使用会造成极大的内存浪费
(以及哈希表会采用)
二.二叉树的链式存储结构(二叉链表和三叉链表)
表示二叉树的链表中,每个结点至少包含三个域:数据域,左指针域,右指针域。有时为了便于寻找双亲,也可以额外添加一个指向双亲的指针域。以上两种分别成为二叉链表和三叉链表
对于二叉链式二叉树,有n个结点,就有2n个链域。每个结点有且只有一个双亲,则只有n-1个链域存放指针,那么就有n+1个空链域
二叉链表的抽象数据类型定义:
typedef struct BiTNode
{
TElemtype data; //数据域
BiTNode *lchild; //左指针域
BiTNode *rchild; //右指针域
//BiNode *parent; //指向双亲的指针域
}BiTNode,*BiTree
5.5遍历二叉树和线索二叉树
5.5.1遍历二叉树
1.遍历二叉树算法描述:
遍历二叉树是指按照某条搜索路径巡防树中的每个结点,在不破坏原来数据结构的情况下访问每一个节点,且每个结点只访问一次。遍历的结果是将非线性结构的树中结点排成一个线性序列
遍历二叉树的递归算法
1.先序遍历:若二叉树为空,则空操作,否则
1.访问根结点
2.先序遍历左子树
3.先序遍历右子树
2.中序遍历:若二叉树为空,则空操作,否则
1.中序遍历左子树
2.访问根结点
3.中序遍历右子树
3.后序遍历:若二叉树为空,则空操作,否则
1.后序遍历左子树
2.后序遍历右子树
3.访问根结点
4.层序遍历:从上到下,从左到右依次遍历结点(广度优先)
算法5.1中序遍历的递归算法
算法描述:
status InOrderTraverse(BiTree T)
{
if(T)
{
InOrderTraverse(T->lchild);
visit(T);
InOrderTraverse(T->rchild);//只要更改三行顺序,即可得先序遍历或后序遍历
}
}
如果抹去visit()语句,三种遍历完全相同。即三种遍历访问路径相同,访问结点的时机不同
遍历二叉树的非递归算法
可以用栈来模拟系统栈,完成非递归算法遍历二叉树
基本思想(中序):1.建立一个栈
2.根结点进栈,遍历左子树
3.根结点出栈,输出根结点,遍历右子树
算法5.2中序遍历的非递归算法
算法步骤:1.初始化一个空栈S,指针p保存根结点
2.申请一个结点空间q,用来存放栈顶弹出的元素
3.当p非空或S非空,执行
1.如果p非空,则p进展,p指向该结点的左孩子
2.如果p为空,弹出栈顶元素并访问,p指向右孩子
算法描述:
void InOrderTraverse(BiTree T)//中序遍历
{
InitStack(S); //初始化一个栈
BiTree p = T; //p保存根结点
BiTree q = new BiTree; //前负责存放弹出的根结点
while(!P||!EmptyStack(S)) //p非空或栈非空
{
if(p) //p非空
{
Push(S,p); //根结点入栈
p = p->lchild; //遍历左子树
}
else
{
Pop(S,q); //根结点出栈
visit(q); //访问根结点
p = p->rchild; //遍历右子树
}
}
}
同样的,只需要改变相关语句的顺序就可以调整为先序遍历和后序遍历
二叉树的层序遍历
对于一棵二叉树,从根结点开始,从上到下,从左到右依次访问结点。可以用队列来模拟
基本思路:1.将根节点入队
2.队不空则循环:
1.从队列出列一个结点,访问它
2.该结点若有左孩子,左孩子入队,若有右孩子,右孩子入队
算法5.3:层序遍历二叉树
算法描述:
void LevelOrder(BiNode* T)
{
SqQueue Q; //定义一个队
InitQueue(Q); //初始化队
BiNode *p = new BiNode; //p保存根结点
enQueue(Q,*T); //根结点入队
while(!EmptyQueue(Q)) //队非空
{
deQueue(Q,p); //队头出队
visit(p->data); //访问出队的队头元素
if(p->lchild) //该结点有左孩子就把左孩子入队
{
enQueue(Q,p->lchild);
}
if(p->rchild) //有右孩子就把右孩子入队
{
enQueue(Q,p->rchild);
}
}
}
2.由遍历序列确定二叉树
若二叉树中结点值各不相同,则任意一颗二叉树的先序序列,中序序列和后序序列都是唯一的。因此,只要知道先序序列和中序序列,或后序序列和中序序列就能唯一确定二叉树
判定方法:1.由先序序列和后序序列就能判断第一个根结点是哪个
2.中序序列中,根结点在中间,其左面的全在左子树,右面的全在右子树
3.以此类推
3.二叉树遍历算法的应用
遍历二叉树中的访问可以包含许多操作,由此衍生出许多算法
算法5.4:先序建立二叉树
算法步骤:1.扫描字符序列,读入字符ch
2.如果字符为'#',则表明为空树,T==NULL,否则执行
(1)申请一个结点T
(2)将ch赋值给T->data
(3)递归创建T的左子树
(4)递归创建T的右子树
算法描述:
void CreateBiTree(BiTree &T)
{
cin >> ch; //输入字符
if(ch=='#') //字符为#则为空树
T=NULL; //结点置空
else
{
T = new BiTree; //申请结点空间
T->data = ch; //结点赋值 //相当于遍历二叉树中访问结点的操作
CreateBiTree(T->lchild); //递归建立左子树
CreateBiTree(T->rchild); //递归建立右子树
}
}
算法5.5:复制二叉树
算法步骤:如果是空树,递归结束,否则执行
1.申请一个新结点,复制根结点
2.递归复制左子树
3.递归复制右子树
算法描述:
void Cpye(BiTree T,BiTree &newT)
{
if(!T) //被复制树为空,新树也为空
{
newT = NULL;
return;
}
else
{
newT = new BiNode; //新树申请一个新结点
newT->data = T->data; //复制根结点值 //相当于访问根结点
Copy(T->lchild,newT->lchild); //递归复制左子树
Copy(T->rchild,newT->rchild); //递归复制右子树
}
}
算法5.6:计算二叉树的深度
算法步骤:如果是空树,返回0,否则执行
1.递归计算左子树深度为m
2.递归计算右子树深度为n
3.若m大,则深度为m+1,若n大,则深度为n+1(根结点)
算法描述:
int Depth(BiTree T)
{
if(!T) //空树返回0
return 0;
else
{
int m = Depth(T->lchild); //递归计算左子树深度
int n = Depth(T->rchild); //递归计算右子树深度
return m>n?m+1:n+1; //返回较大的加上根结点1
}
}
算法5.7:统计二叉树中结点个数
算法步骤:如果是空树,则返回0,否则,结点个数即是左子树结点数加上右子树结点数+1
算法描述:
int NodeCount(BiTree T)
{
if(T) return 0; //如果是空树则返回0
else
return NodeCount(T->lchild)+NodeCount(T->rchild)+1; //返回左子树结点加右子树结点+1
}
算法5.9求度为0的二叉树结点个数
算法步骤:如果为空树,则返回0,否则为左子树叶子个数加右子树叶子个数
算法描述:
int leafCount(BiTree T)
{
if(T==NULL) return 0;
if(!T->lchild&&!T->rchild) return 1;
else
return leafCount(T->lchild)+leafCount(T->rchild);
}
5.5.2线索二叉树
线索二叉树:遍历二叉树是以一定规则将二叉树中的结点排列成一个线性结构,由于n个结点的二叉链表中有n+1个空指针域,可以充分利用这些空域来存放结点遍历结果的前驱和后继
线索:指向结点前驱或后继的指针
线索二叉树:加上线索的二叉树
线索化:对二叉树以某种次序遍历使其变成线索二叉树的过程
其中:
Ltag = 0:lchild指针指向结点的左孩子(非空)
= 1:lchild指向结点的前驱(空)
Rtag = 0:rchild指针指向结点的右孩子(非空)
= 1:rchild指向结点的后继(空)
线索二叉树的类型定义:
typedef struct BiThrNode
{
ElemType data;
BiThrNode* lchild,*rchild;
int Ltag,Rtag; //值为1时表示空,即为线索
}BiThrNode,*BiThrTree;
2.构造线索二叉树(中序)
线索化的过程即在遍历的过程中修改空指针的过程
附设pre指向刚刚访问过的结点
算法5.10,以结点p为根的子树的中序线索化
算法步骤:1.如果p非空,左子树递归线索化
2.如果p的左孩子为空,则给p加上左线索,Ltag赋值为1,让p的左孩子指向pre,否则将p的Ltag置为0
3.如果p的右孩子为空,则给pre加上右线索,将其Rtag置为1,让pre的右孩子指向后继p,否则将pre的Rtag置为0
4.将pre指向p
5.右子树递归线索化
算法描述:
void InTreading(BiThrTree p)
{
static BiThrNode* pre = NULL //pre定义为静态变量,初始化为NULL
if(p)
{
InTreading(p->lchild);//左子树递归线索化
if(!p->lchild) //①p左子树为空,则为p添加前驱结点
{
p->Ltag = 1; //Ltag置为1
p->lchild = pre; //前驱指向pre
}
else p->Ltag = 0; //否则p的Ltag置为0
if(!pre&&!pre->rchild)//②,先判断前驱是否为空,防止出错。pre右子树为空,则为pre添加后继
{
pre->Rtag = 1; //Rtag置为0
pre->rchild = p; //后继指向p
}
else pre->Rtag = 0; //否则pre的Rtag置为0
pre = p; //③pre指向p
InTreading(p->rchild);//右子树递归线索化
}
}
注意:1.结束后,最后处理的结点的rchild为空,代表链表的终点。
同样,第一个结点的lchild为空,代表链表的起点。
最后p回溯到根结点,程序结束
2.关于pre指针的定义
①:全局变量(不推荐)
②:作为类的数据成员
③:定义为静态局部变量(推荐)
④:
算法5.11中序遍历线索二叉树查找前驱和后继
算法步骤:前驱:1.若p->Ltag为1,则p的左链指示前驱
2.若p->Ltag为0,则p的左子树的最右下角的元素为前驱结点
后继:1.若p->Rtag为1,则p的右链指示后继
2.若p->Rtag为0,则p的右子树的最左下角的元素为后继结点
算法描述:
TreeNode* previous(TreeNode* p) //找前驱
{
if(p->Ltag==1) //Ltag==1直接由lchild得到前驱
{
q = p->lchild;
}
else //Ltag==0,找p的左子树的最右下角的结点即为前驱
{
q = p->lchild; //左子树
while(q->Ltag==0) //只要有左子树,q就沿右子树向下。直到到了最右下角
{
q = q->rchild;
}
}
return q;
}
TreeNode* next(TreeNode* p) //找后继
{
if(p->Rtag==1) //Rtag==0直接由rchild得到后继
{
q = p->rchild;
}
else //Rtag==1,找p的右子树的最左下角的结点为后继
{
q = p->rchild; //右子树
while(q->Rtag==0) //只要右孩子不为空,q就沿着左孩子向下,直到到最左下角
{
q = q->lchild;
}
}
return q;
}
遍历线索二叉树
线索二叉树的遍历操作无需设栈,避免了频繁的进栈出栈。
算法5.12遍历中序线索二叉树
算法步骤:1.指针p指向根结点
2.p为非空树或遍历未结束时,循环执行:
1.沿左孩子向下,到达最左下角的结点*p,这是中序的第一个结点
2.访问*p
3.沿右线索反复查找当前结点*p的后继结点并访问后继结点,直到右线索为0或结束
4.转向p的右子树
算法描述:
void InOrderTraverse_Thr(BiTreTree T)
{
p = T->lchild; //p指向根结点
while(!p) //空树或遍历结束时,p==T
{
while(p->Ltag==0) p = p->lchild; //沿左孩子向下
visit(p); //访问左子树为空的结点
while(p->Rtag==1&&p->rchild!=T) //沿着线索访问后继,且非空或遍历未结束
{
p = p->rchild;
visit(p);
}
p = p->rchild; //转向p的右子树
}
}
5.6树和森林
5.6.1树的存储结构
1.双亲表示法
以一组连续的存储单元存储(结构数组)树的结点,每个结点除了数据域data外,还附设一个parent与用以指示其双亲结点的位置
结构示意图
类型定义:
//结点结构
typedef struct PTNode
{
ElemType data;
int parent;//双亲位置域
}PTNode;
//树结构
#define MAXSIZE 100;
typedef struct
{
PTNode nodes[MAXSIZE];
int r,n;//指示根结点的位置r和结点个数n
}PTree
2.孩子表示法
把每个结点的孩子结点排列起来,看成一个线性表,且以单链表做存储结构。则n个结点有哪个孩子链表(叶子的孩子链表为空表)。而n个头指针又组成一个线性表,为了便于查找,可采用顺序存储结构。
孩子表示法便于实行涉及孩子的操作,可以将孩子表示法和双亲表示法结合起来
结构示意图:
在头结点数组中添加一个parent域即可指示双亲位置
类型定义:
//孩子结点
typedef struct CTNode
{
int child;
struct CTNode* next;
}*ChildPtr;
//双亲结点
typedef struct
{
ElemType data;
ChildPtr firstChild;//孩子链表头指针
int parent;//附设双亲位置
}CTBox;
//树结构
typedef struct
{
CTBox node[MAXSIZE];
int n,r;//指示结点个数和根结点的位置
}CTree;
3.孩子兄弟表示法(常用方法)
又称二叉树表示法,以二叉树做树的存储结构。链表中结点的两个链域分别指示第一个孩子和下一个兄弟结点
结点结构示意图:
类型定义:
typedef struct CSNode
{
Elemtype data;
CSNode *firstchild,*nextsibling;//孩子兄弟链域
}CSNode,*CSTree;
树形结构示意图
5.6.2森林,树与二叉树的转换(树与孩子兄弟表示法的转换)
树<——>二叉树 以二叉链表为媒介
给定一棵树,可以找到唯一的二叉树与之对应
1.树——>二叉树
①加线:兄弟之间各加一条线
②抹线:对每个结点,除最左侧孩子外,去除其与其他孩子之间的关系
③调整:调整为标准二叉树形式
兄弟相连留长子
2.二叉树——>树
①加线:若p结点是双亲的左孩子,则将p的右孩子,右孩子的右孩子等一条直线上所有右孩子都与p的双亲连接起来
②抹线:抹掉原二叉树中双亲与右孩子之间的连接
③调整:按层序形成树结构
左孩右右连双亲,去掉原来右孩线
树和二叉树转换示意图:
3.森林——>二叉树
①将各树分别转换成二叉树
②将每棵树的根结点用线相连
③以第一棵树的结点为二叉树的根,再做调整
树变二叉根相连
4.二叉树——>森林
①抹线:将二叉树的根结点与其右孩子,以及右孩子的右孩子一条直线上所有右孩子的连线都抹掉,使之变成孤立的二叉树
②还原:将孤立的二叉树还原成树
5.6.3树和森林的遍历
1.树的遍历
(1)先根遍历:先访问树的根结点,再依次先根遍历树的每颗子树
(2)后根遍历:先依次后根遍历每颗子树,再遍历根结点
(3)层序遍历:从上到下,从左到右依次遍历
2.森林的遍历
(1)先序遍历森林
①:访问第一棵树的根结点
②:先序遍历第一棵树的子树森林
③:先序遍历除去第一棵树的剩余的树构成的森林
(2)中序遍历森林
①:中序遍历森林中第一棵树的子树森林
②:访问第一棵树的根结点
③:中序遍历除去第一棵树的剩余的树构成的森林
5.7哈夫曼树及其应用
5.7.1哈夫曼树的基本概念
哈夫曼树又称最优树,是一类带权路径长度最短的树
(1)路径:从树的一个结点到另一个结点之间的分支构成这两个结点之间的路径)
(2)路径长度:路径上的分支数目
(3)树的路径长度:从树根到每一结点的路径长度之和(记作TL)
(结点数目相同时,完全二叉树是树的路径长度最短的二叉树)
(4)权:赋予某个实体的一个量(在数据结构中实体有结点和边两大类,对应有结点权和边权)
(5)结点的带权路径长度:从该结点到树根的路径长度与该结点权的乘积
(6)树的带权路径长度:树中所有叶子结点的带权路径长度之和(WPL)
(7)哈夫曼树:假设有m个权值{w1,w2...wm}可以构造一颗含n个叶子结点的二叉树,每个叶子结点的权为wi,则其中带权路径长度WPL最小的二叉树为哈夫曼树
注:①带权路径长度最短是在度相同的树中比较的结果,因此有最优二叉树,最优三叉树
②满二叉树不一定是哈夫曼树
③哈夫曼树中,权值越大离根结点越近
④具有相同带权结点的哈夫曼树不唯一
5.7.2哈夫曼树的构造算法
1.哈夫曼树的构造过程
(1)根据给定的n个权值{w1,w2...wn},构造n棵只有根结点的二叉树,这n棵树构成森林F
(2)在F中选取两个根结点权值最小的树作为左右子树构成一棵新的二叉树,且置新二叉树根结点权值为左右子树权值之和
(3)在F中删除这两棵树,同时将新构造的树加入F中
(4)重复(2)(3)直到只剩一棵树为止,这棵树就是哈夫曼树
在构造哈夫曼树时,首先选择权小的,这样能保证权大的离根较近,这样自然会得到最短路径长度,这是典型的贪心算法
注:哈夫曼树的结点的度为0或2,没有度为1的树
2.哈夫曼树的算法实现
(1)初始n棵二叉树经过n-1次合并形成哈夫曼树
(2)经n-1次合并产生n-1个新结点,且这n-1个新结点都有两个孩子
(3)哈夫曼树共有2n-1个结点,且所有结点的度均不为1
哈夫曼树结点示意图:
哈夫曼树结点类型定义
typedef struct
{
ElemType weight; //结点的权值
int parent,lchild,rchild; //双亲,左孩子,右孩子下标
}HTNode,*HuffmanTree; //动态分配内存
注:哈夫曼树各节点存储在HuffmanTree动态分配的数组中,为了实现方便,数组的0号单元不使用,从1号单元开始使用,所以数组的大小为2n,将叶子结点集中于前面1—n个位置,后面n-1个为非叶子结点
算法5.13构造哈夫曼树
算法步骤:1.初始化:
(1)首先申请2n个单元,然后循环2n-1次,从1号单元开始,依次将1至2n-1所以单元的双亲,左孩子右孩子下标初始化为0。
(2)再循环n次,输入前n个单元中叶子结点的权值
2.创建树:
循环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; //小于1直接返回
int m = 2*n-1; //总结点数
HT = new HTNode[m+1]; //动态分配内存2n个空间,0下标不使用
for(int i = 1;i <= m;i++) //将所有结点双亲,左右孩子初始化为0
{
HT[i].parent = HT[i].lchild = HT[i].rchild = 0;
}
for(int i = 1;i <=n;i++) //输入叶子结点的权值
{
cin >> HT[i].weight;
}
——————————————————————————————————————//创建哈夫曼树
for(int i = n+1;i <= m;i++) //通过n-1次选择,删除,合并创建哈夫曼树
{
——————————————————————————————————————//选择
Select(HT,i-1,s1,s2); //在HT[k](1=
5.7.3哈夫曼编码
概念:(1)前缀编码:任意一个编码都不是其他编码的前缀(最左字串)的编码方案为前缀编码
前缀编码可以保证对压缩文件解码时不产生二义性
(2)哈夫曼编码:对一棵具有n个叶子结点的哈夫曼树,若左分支赋予0,右分支赋予1, 则从根到每个叶子的路径上,各分支的赋值分别构成一个二进制串,该二进制串称为 哈夫曼编码(总长最短的前缀码)
性质:(1)哈夫曼编码是前缀编码
没有一片树叶是另一片树叶的祖先,所以任一哈夫曼码都不会和人一起他哈夫曼码的 前缀部分完全重叠
(2)哈夫曼编码是最优前缀编码(树的带权路径长度最短)
主要思想:依次以叶子为出发点,向上回溯至根结点为止,回溯时走左分支生成代码0,走右分支 生成代码1
以指针数组存放每个字符编码的首地址
typedef char **HuffmanCode; //动态分配数组存储哈夫曼码
注:1.为了实现方便,数组的0号元素不使用,从1号开始使用,所以数组大小为n+1
2.由于每个字符编码长度不能事先确定,所以动态分配长度为n(字符编码长度一定小于n) 的一维数组cd,存放当前正在求解的第i个字符的编码
3.当第i个字符编码求解完毕后根据数组cd中字符串的长度给HT[i]分配空间,然后将cd中的编 码复制到HT[i]中
4.因为求解编码是从叶子到根,所以对于每个字符,得到的编码顺序是从右向左的,故将编码 向cd中存放的顺序也是从后向前的
算法5.13求哈夫曼编码
算法步骤:①分配存储n个字符编码的编码表HC,长度为n+1;分配临时存储每个字符编码的动态 数组空间cd,cd[n-1]置为'\0';
②逐个求解n个字符的编码,循环n次执行如下操作
(1)设置变量start用于记录编码在cd中的存放位置,start初始指向最后,即n-1
(2)设置变量c记录从叶子结点向上回溯所经过的结点下标,c初始时为当前待编码 字符的下标i,f记录i双亲的下标
(3)从叶子结点向上回溯至根结点,求字符i的编码。当f没有到达根结点时,循环
*回溯一次start向前一位
*若c是f左孩子,则生成代码0,若是右孩子,则生成代码1
*继续回溯,改变c,f的值
(4)根据数组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'; //数组最后一位置为'\0'
for(int i = 1;i <= n;i++) //对n个字符逐个编码
{
start = n-1; //start从最后一位开始
c = i,f = HT[i].parent; //f指向c的双亲结点
while(f!=0) //开始对第i个字符编码
{
--start; //start向前一位
if(HT[f].lchild==c) //如果c是其双亲结点的左孩子,生成'\0'
cd[start] = '0';
else //否则生成'\1'
cd[start] = '1';
c = f,f = HT[i].parent;//继续向上回溯
}
HC[i] = new char[n-start]; //为第i个字符分配空间
strcpy(HC[i],&cd[start]); //将求得的在cd中的编码复制到编码表第i个位置中
}
delete cd; //释放临时空间
}
编码表示意图:
文件的编码和译码(借助哈夫曼树)
(1)编码①依次读入文件中的字符
②在哈夫曼编码表HC中找到该字符
③将字符转换为编码表存放的编码串
(2)译码①依次读入文件的二进制码
②从哈夫曼树的根结点出发,若读入0,则走左孩子,若读入1,则走右孩子
③一旦到达某一叶子HT[i]即译出相应字符HC[i]
④然后重新从根出发继续译码,直到文件读取结束。
注:编码和译码必须是同一棵哈夫曼树