一、树和二叉树的定义
1、树的定义
树 (Tree) 是 n(n≥0) 个结点的有限集,它或为空树 (n = 0),或为非空树
对于非空树 T:
- 有且仅有一个称之为根的结点
- 除根结点以外的其余结点可分为 m(m>0)个互不相交的有限集 T1 , T2 , …,Tm,其中每一个集合本身又是一棵树,并且称为根的子树 (SubTree)
2、树的基本术语
- 结点:树中的一个独立单元,包含一个数据元素及若干指向其子树的分支
- 结点的度:结点拥有的子树数称为结点的度
- 树的度:树的度是树内各结点度的最大值
- 叶子:度为 0 的结点称为叶子或终端结点
- 非终端结点:度不为 0 的结点称为非终端结点或分支结点。除根结点之外,非终端结点也称为内部结点
- 双亲和孩子:结点的子树的根称为该结点的孩子,相应地,该结点称为孩子的双亲
- 兄弟:同一个双亲的孩子之间互称兄弟
- 祖先:从根到该结点所经分支上的所有结点
- 子孙:以某结点为根的子树中的任一结点都称为该结点的子孙
- 层次:结点的层次从根开始定义起,根为 第一层,根的孩子为第二层
- 树中任一结点的层次等于其双亲结点的层次加 1
- 堂兄弟:双亲在同 一层的结点互为堂兄弟
- 树的深度:树中结点的最大层次称为树的深度或高度
- 有序树和无序树:如果将树中结点的各子树看成从左至右是有次序的(即不能互换),则称该树为有序树,否则称为无序树
- 在有序树中最左边的子树的根称为第一个孩子,最右边的称为最后一个孩子
- 森林:是 m(m≥0)棵互不相交的树的集合
- 对树中每个结点而言,其子树的集合即为森林
3、 二叉树的定义
二叉树 (Binary Tree) 是 n(n≥0) 个结点所构成的集合,它或为空树 (n= 0),或为非空树
对于非空树 T:
- 有且仅有一个称之为根的结点
- 除根结点以外的其余结点分为两个互不相交的子集 T1 和 T2, 分别称为 T 的左子树和右子树,且 T1 和 T2 本身又都是二叉树
二叉树与树的区别:
- 二叉树每个结点至多只有两棵子树(即二叉树中不存在度大于 2 的结点)
- 二叉树的子树有左右之分,其次序不能任意颠倒
二叉树的 5 种基本形态:
二、二叉树的性质和存储结构
1、二叉树的性质
- 在二叉树的第 i 层上至多有
个结点(i ≥ 1)
- 深度为 k 的二叉树至多有
个结点(k ≥ 1)
- 对任何一颗二叉树 T,如果其终端结点数为 n0 ,度为 2 的结点数为
,则
=
+ 1
2、特殊形态的二叉树
1)满二叉树
定义:深度为 k 且含有 个结点的二叉树。
特点:每一层上的结点数都是最大结点数,即每一层 i 的结点数都具有最大值
2)完全二叉树
定义:
深度为 k 的, 有 n 个结点的二叉树, 当且仅当其每一个结点都与深度为 k 的满二叉树中编号从 1 至 n 的结点一一对应时, 称之为完全二叉树
特点:
- 叶子结点只可能在层次最大的两层上出现
- 对任一结点, 若其右分支下的子孙的最大层次为 k , 则其左分支下的子孙的最大层次必为 k 或 k + 1
性质:
- 具有 n 个结点的完全二叉树的深度为
- 如果对一颗有 n 个结点的完全二叉树(其深度为
)的结点按层序编号(从第 1 层到第
层,每层从左到右),则对任一结点 i(1 ≤ i ≤ n),有
- 如果 i = 1,则结点 i 是二叉树的根,无双亲;如果 i > 1,则其双亲 PARENT(i) 是结点
- 如果 2i > n,则结点 i 无左孩子(结点 i 为叶子结点);否则其左孩子 LCHILD(i) 的结点 2i
- 如果 2i + 1 > n,则结点 i 无右孩子,否则其右孩子 RCHILD(i) 是结点 2i + 1
- 如果 i = 1,则结点 i 是二叉树的根,无双亲;如果 i > 1,则其双亲 PARENT(i) 是结点
3、二叉树的存储结构
1)顺序存储结构
顺序存储结构使用一组地址连续的存储单元来存储数据元素,为了能够在存储结构中反映出结点之间的逻辑关系,必须将二叉树中的结点依照一定的规律安排在这组单元中
对于完全二叉树,只要从根起按层序存储即可,依次自上而下、自左至右存储结点元素,即将完全二叉树上编号为 i 的结点元素存储在如下定义的一维数组中下标为 i -1 的分量中
对于一般二叉树,则应将其每个结点与完全二叉树上的结点相对照,存储在一维数组的相应分量中
在最坏的情况下, 一个深度为 k 且只有 k 个结点的单支树(树中不存在度为2 的结点)却需要长度为 的一维数组
#define MAXSIZE 100 // 二叉树的最大结点数
typedef TElemType SqBiTree[MAXSIZE]; // 0 号单元存储根结点
SqBiTree bt;
2)链式存储结构
typedef struct BiTNode{
TElemType data; // 结点数据域
struct BiTNode *lchild,*rchild; // 左右孩子指针
} BiTNode,*BiTree;
三、遍历二叉树和线索二叉树
1、遍历二叉树
1)遍历二叉树算法描述
遍历二叉树 (traversing binary tree) 是指按某条搜索路径巡访树中每个结点,使得每个结点均被访问一次,而且仅被访问一次
遍历二叉树的递归算法定义:
先序遍历二叉树:根左右
中序遍历二叉树:左根右
后序遍历二叉树:左右根
Ⅰ、中序遍历的递归算法
void InOrderTraverse(BiTree T){
if(T){ // 二叉树非空
InOrderTraverse(T->lchild); // 中序遍历左子树
cout<<T-data; // 访问根结点
InOrderTraverse(T->rchild); // 中序遍历右子树
}
}
Ⅱ、中序遍历的非递归算法
步骤:
- 初始化一个空栈 S,指针 p 指向根结点
- 申请一个结点空间 q,用来存放栈顶弹出的元素
- 当 p 非空或者栈 S 非空时,循环执行以下操作:
- 如果 p 非空,则将 p 进栈,p 指向该结点的左孩子
- 如果 p 为空,则弹出栈顶元素并访问,将 p 指向该结点的右孩子
void InOrderTraverse(BiTree T){
InitStack(S);
p = T;
q = new BiTNode;
while(p || !StackEmpty(S)){
if(p){ // p 非空
Push(S,p); // 根指针进栈
p = p->lchild; // 根指针进栈,遍历左子树
} else { // p 为空
Pop(S,q); // 弹栈
cout<<q->data; // 访问根结点
p = q->rchild; // 遍历右子树
}
}
}
无论是递归还是非递归遍历二叉树,因为每个结点被访问一次,则不论按哪一种次序进行遍历,对含 n 个结点的二叉树,其时间复杂度均为 O(n)。所需辅助空间为遍历过程中栈的最大容量, 即树的深度,最坏情况下为 n, 则空间复杂度也为 O(n)。
2)根据遍历序列确定二叉树
由二叉树的先序序列和中序序列,或由其后序序列和中序序列均能唯一地确定一棵二叉树
3)二叉树遍历算法的应用
Ⅰ、创建二叉树的存储结构 —— 二叉链表
先序遍历的顺序建立二叉链表:
步骤:
扫描字符序列,读入字符 ch
如果 ch 是一个 “#” 字符,则表明该二叉树为空树,即 T 为 NULL;否则执行以下操作:
- 申请一个结点空间 T
- 将 ch 赋给 T->data
- 递归创建 T 的左子树
- 递归创建 T 的右子树
void CreateBiTree(BiTree &T){
// 按先序次序输入二叉树中结点的值( 一个字符), 创建二叉链表表示的二叉树 T
cin>>ch;
if(ch == '#')
T = NULL; // 递归结束,创建空树
else{ // 递归创建二叉树
T = new BiTNode; // 生成根结点
T->data =ch; // 根结点数据域置为 ch
CtreateBiTree(T->lchild); // 递归创建左子树
CtreateBiTree(T->rchild); // 递归创建右子树
}
}
Ⅱ、复制二叉树
步骤:
如果是空树,递归结束,否则执行以下操作:
- 申请一个新节点空间,复制根结点
- 递归复制左子树
- 递归复制右子树
void Copy(BiTree T,BiTree &NewT){
// 复制一棵和T完全相同的二叉树
if(T == NULL){
NewT = NULL; // 空树,递归结束
return;
} else {
NewT = new BiTNode;
NewT->data = T->data; // 复制根结点
Copy(T->lchild,New->lchild); // 递归复制左子树
Copy(T->rchild,New->rchild); // 递归复制右子树
}
}
Ⅲ、计算二叉树的深度
二叉树的深度为树中结点的最大层次, 二叉树的深度为左右子树深度的较大者加 1
步骤:
如果是空树,递归结束,深度为 0,否则执行以下操作:
- 递归计算左子树的深度为 m
- 递归计算右子树的深度为 n
- 如果 m 大于 n,二叉树的深度为 m + 1,否则为 n + 1
int Depth(BiTree T){
// 计算二叉树T的深度
if(T == NULL)
return 0; // 空树,深度为 0,递归结束
else{
m = Depth(T->lchild); // 递归计算左子树的深度为 m
n = Depth(T->rchild); // 递归计算右子树的深度为 n
if(m > n) // 二叉树的深度为 m 与 n 的较大者加 1
return (m + 1);
else
return (n + 1);
}
}
Ⅳ、统计二叉树中结点的个数
如果是空树,则结点个数为 0; 否则,结点个数为左子树的结点个数加上右子树的结点个数再加上 1
int NodeCount(BiTree T){
// 统计二叉树T中结点的个数
if(T == NULL) // 如果是空树,则结点个数为0, 递归结束
return 0;
else
// 否则结点个数为左子树的结点个数 + 右子树的结点个数 + l
return NodeCount(T->lchild) + NodeCount(T->rchild) + 1;
}
2、线索二叉树
1)线索二叉树的基本概念
若结点有左子树,则其 lchild 域指示其左孩子,否则令 lchild 域指示其前驱
若结点有右子树,则其 rchild 域指示其右孩子,否则令 rchild 域指示其后继
为了避免混淆,尚需改变结点结构,增加两个标志域:
其中:
二叉树的二叉线索类型定义如下:
typedef struct BiThrNode{
TElemType data;
struct BiThrNode *lchild,*rchild; // 左右孩子指针
int LTag,RTag; // 左右标识
}BiThrNode,*BiThrTree;
这种结点结构构成的二叉链表作为二叉树的存储结构,叫做线索链表,其中指向结点前驱和后继的指针,叫做线索。加上线索的二叉树称之为线索二叉树 (Threaded Binary Tree)。对二叉树以某种次序遍历使其变为线索二叉树的过程叫做线索化
2)构造线索二叉树
Ⅰ、以结点 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是全局变址,初始化时其右孩子指针为空,便于在树的最左点开始建线索
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); // 右子树递归线索化
}
}
Ⅱ、带头结点的二叉树中序线索化
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; // 头结点的左孩子指向根,pre 初值指向头结点
InThreading(T); // 调用上文算法, 对以T为 根的二叉树进行中序线索化
pre->rchild = Thrt; // pre为 最右结点,pre 的右线索指向头结点
pre->RTag = 1;
Thrt->rchild = pre; // 头结点的右线索指向 pre
}
}
Ⅲ、遍历线索二叉树
A、在中序线索二叉树中查找
- 查找 p 指针所指结点的前驱
- 若 p->LTag 为 1,则 p 的左链指示其前驱
- 若 p->LTag 为 0,则说明 p 有左子树,结点的前驱是遍历左子树时最后访问的一个结点(左子树中最右下的结点)
- 查找 p 指针所指结点的后继
- 若 p->RTag 为 1,则 p 的右链指示其后继
- 若 p->RTag 为 0,则说明 p 有右子树。根据中序遍历的规律可知,结点的后继应是遍历其右子树时访间的第一个结点,即右子树中最左下的结点
B、在先序线索二叉树中查找
- 查找 p 指针所指结点的前驱
- 若 p->LTag 为 1,则 p 的左链指示其前驱
- 若 p->LTag 为 0,则说明 p 有左子树。此时 p 的前驱有两种情况:
- 若 *p 是其双亲的左孩子,则其前驱为其双亲结点
- 否则应是其双亲的左子树上先序遍历最后访问的结点
- 查找 p 指针所指结点的后继
- 若 p->RTag 为 1,则 p 的右链指示其后继
- 若 p->RTag 为 0,则说明 p 有右子树。根据先序遍历的规律可知,*p 的后继必为其左子树根(若存在)或右子树根
C、在后序线索二叉树中查找
- 查找 p 指针所指结点的前驱
- 若 p->LTag 为 1,则 p 的左链指示其前驱
- 若 p->LTag 为 0
- 当 p->RTag 为 0 时,则 p 的右链指示其前驱
- 当 p->RTag 为 1 时,则 p 的左链指示其前驱
- 查找 p 指针所指结点的后继情况比较复杂,分以下情况讨论:
- 若 *p 是二叉树的根, 则其后继为空;
- 若 *p 是其双亲的右孩子, 则其后继为双亲结点;
- 若 *p 是其双亲的左孩子, 且 *p 没有右兄弟, 则其后继为双亲结点;
- 若 *p 是其双亲的左孩子,且 *p 有右兄弟,则其后继为双亲的右子树上按后序遍历列出的第一个结点( 即右子树中 “最左下” 的叶结点)
遍历中序线索二叉树
遍历线索二叉树的时间复杂度为 O(n), 空间复杂度为 O(1), 这是因为线索二叉树的遍
历不需要使用栈来实现递归操作
步骤:
- 指针 p 指向根结点
- p 为非空树或遍历未结束时,循环执行以下操作:
- 沿左孩子向下,到达最左下结点 *p,它是中序的第一个结点
- 访问 *p
- 沿右线索反复查找当前结点 *p 的后继结点并访问后继结点,直至右线索为 0 或者遍历结束
- 转向p的右子树
void InOrderTraverse_Thr(BiThrTree T){
// T 指向头结点,头结点的左链 lchild 指向根结点
// 中序遍历二叉线索树 T 的非递归算法,对每个数据元素直接输出
p = T->lchild; // p指向根结点
while(p != T){ // 空树或遍历结束时,p == T
while(p->LTag == 0)
p = p->lchild; // 沿左孩子向下
cout<<p->data; // 访问其左子树为空的结点
while(p->RTag == 1 && rchild != T){
p = p->rchild;
count<<p->data; // 沿右线索访问后继结点
}
p = p->rchild; // 转向 p 的右子树
}
}
四、树和森林
1、树的存储结构
1)双亲表示法
以一组连续的存储单元存储树的结点,每个结点除了数据域data外,还附 设一个 parent 域用以指示其双亲结点的位置, 其结点形式如图所示
这种存储结构利用了每个结点(除根以外)只有唯一的双亲的性质。在这种存储结构下,求结点的双亲十分方便,也很容易求树的根,但求结点的孩子时需要遍历整个结构
2)孩子表示法
由于树中每个结点可能有多棵子树,则可用多重链表,即每个结点有多个指针域,其中每个指针指向一棵子树的根结点
3)孩子兄弟法
又称二叉树表示法,或二叉链表表示法,即以二叉链表做树的存储结构
链表中结点的两个 链域分别指向该结点的第一个孩子结点和下一个兄弟结点,分别命名为firstchild 域和 nextsibling 域,其结点形式如图所示
这种存储结构的优点是它和二叉树的二叉链表表示完全一样, 便于将一般的树结构转换为
二叉树进行处理,利 用二叉树的算法来实现对树的操作
typedef struct CSNode{
ElemType data;
struct CSNode *firstchild,*nextsibling;
}CSNode,CSTree;
树的二叉树链表表示法
2、森林与二叉树的转换
从树的二叉链表表示的定义可知,任何一棵和树对应的二叉树,其根结点的右子树必空
若把森林中第二棵树的根结点看成是第一棵树的根结点的兄弟,则同样可导出森林和二叉树的对应关系
森林与二叉树之间的对应关系:
1)森林转换成二叉树
如果 F = { T1 , T2 , … ,Tm}是森林,则可按如下规则转换成一棵二叉树 B = (root, LB,RB)
- 若 F 为空,即 m = 0,则 B 为空树;
- 若 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}
- 若 B 为空,则 F 为空;
- 若 B 非空,则 F 中第一棵树 T1 的根 ROOT(T1) 即为二叉树 B 的根 root; T1 中根结点的子树森林 F1 是由 B 的左子树 LB 转换而成的森林;F 中除 T1 之外其余树组成的森林 F' = {T2, T3 , …,Tm}是由 B 的右子树 RB 转换而成的森林
3、树和森林的遍历
1)树的遍历
由树结构的定义可引出两种次序遍历树的方法:
一种是先根(次序)遍历树,即:先访问树的根结点,然后依次先根遍历根的每棵子树
另一种是后根(次序)遍历,即先依次后根遍历每棵子树,然后访问根结点
2)森林的遍历
A、先序遍历森林
若森林非空,则可按下述规则遍历:
- 访问森林中第一棵树的根结点
- 先序遍历第一棵树的根结点的子树森林
- 先序遍历除去第一棵树之后剩余的树构成的森林
B、中序遍历森林
若森林非空,则可按下述规则遍历:
- 中序遍历森林中第一棵树的根结点的子树森林
- 访问第一棵树的根结点
- 中序遍历除去第一棵树之后剩余的树构成的森林
五、哈夫曼树及其应用
1、哈夫曼树的基本概念
哈夫曼 (Huffman) 树又称最优树,是一类带权路径长度最短的树
路径:从树中一个结点到另一个结点之间的分支构成这两个结点之间的路径
路径长度:路径上的分支数目称作路径长度
树的路径长度:从树根到每一结点的路径长度之和
权:赋予某个实体的一个量,是对实体的某个或某些属性的数值化描述
结点的带权路径长度:从该结点到树根之间的路径长度与结点上权的乘积
树的带权路径长度:树中所有叶子结点的带权路径长度之和,通常记作
哈夫曼树:假设有 m 个权值 {w1, w2, … ,wm},可以构造一棵含 n 个叶子结点的二叉树, 每个叶子结点的权为 wi,则其中带权路径长度 WPL 最小的二叉树称做最优二叉树或哈夫曼树
- 在哈夫曼树中,权值越大的结点离根结点越近
2、哈夫曼树的构造算法
1)哈夫曼树的构造过程
- 根据给定的 n 个权值 {w1, w2, … ,wn},构造 n 棵只有根结点的二叉树,这 n 棵二叉树构成 一个森林F
- 在森林 F 中选取两棵根结点的权值最小的树作为左右子树构造一棵新的二叉树,且置新的二叉树的根结点的权值为其左 、右子树上根结点的权值之和
- 在森林 F 中删除这两棵树,同时将新得到的二叉树加入 F 中
- 重复 2 和 3,直到F只含一棵树为止,这棵树便是哈夫曼树
在构造哈夫曼树时,首先选择权小的,这样保证权大的离根较近,在计算树的带权路径长度时,便是最小带权路径长度,这种生成算法是一种典型的贪心算法
2)哈夫曼算法的实现
哈夫曼树的存储表示
typedef struct{
int weight; // 结点的权值
int parent,lchild,rchild; // 结点的双亲、左孩子、右孩子的下标
} HTNode,*HuffmanTree; // 动态分配数组存储哈夫曼树
步骤:
- 初始化
- 首先动态申请 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
- 循环 n - 1 次,通过 n - 1 次的选择、删除与合并来创建哈夫曼树
void CtreateHuffmanTree(HuffmanTree &HT,int n){
/*------------------------初始化------------------------*/
// 构造哈夫曼树 HT
if(n <= 1)
return;
m = 2 * n - 1;
HT = new HTNode[m + 1]; // 0号单元未用,所以需要动态分配 m+l 个单元,HT[m]表示根结点
for(i = 1;i <= m;++i){ // 将l~m号单元中的双亲、左孩子,右孩子的下标都初始化为0
HT[i].parent = 0;
HT[i].lchild = 0;
HT[i].rchild = 0;
}
for(i = 1;i <= n;++i){ // 输人前 n 个单元中叶子结点的权值
cin>>HT[i].weight;
}
/*----------------------创建哈夫曼树----------------------*/
for(i = n + 1;i <= m;++i){
// 通过 n - 1 次的选择、删除、合并来创建哈夫曼树
Select(HT,i-1,s1,s2);
// 在HT[k](i≤k≤i-1)中选择两个其双亲为0且权值最小的结点,并返回它们在HT中的序号s1和s2
HT[s1].parent = i;
HT[s2].parent = i;
// 得到新结点i,从森林中删除s1,s2,将s1和s2的双亲域由0改为1
HT[i].lchild = s1;
HT[i].rchild = s2; // sl, s2分别作为i的左右孩子
HT[i].weight = Ht[s1].weight + HT[s2].weight; // i 的权值为左右孩子权值之和
}
}
3、哈夫曼编码
1)哈夫曼编码的主要思想
基本思想:为出现次数较多的字符编以较短的编码
为确保对数据文件进行有效的压缩和对压缩文件进行正确的解码,可以利用哈夫曼树来设计二进制编码
前缀编码:如果在一个编码方案中,任一个编码都不是其他任何编码的前缀(最左子串),则称编码是前缀编码
哈夫曼编码:对一棵具有 n 个叶子的哈夫曼树,若对树中的每个左分支赋予 0, 右分支赋予 1,则从根到每个叶子的路径上,各分支的赋值分别构成一个二进制串,该二进制串就称为哈夫曼编码
性质:
- 性质1:哈夫曼编码是前缀编码
- 性质2:哈夫曼编码是最优前缀编码
2)哈夫曼编码的算法实现
在构造哈夫曼树之后,求哈夫曼编码的主要思想是:
依次以叶子为出发点,向上回溯至根结点为止。回溯时走左分支则生成代码 0, 走右分支则生成代码 1
哈夫曼表的存储表示
typedef char **HuffmanCode; // 动态分配数组存储哈夫曼编码表
根据哈夫曼树求哈夫曼编码
步骤:
- 分配存储 n 个字符编码的编码表空间 HC,长度为 n + 1;分配临时存储每个字符编码的动态数组空间cd,cd[n - 1] 置为 ‘\0’
- 逐个求解 n 个字符的编码,循环 n 次,执行以下操作:
- 设置变量 start 用于记录编码在 cd 中存放的位置,start 初始时指向最后,即编码结束符位置 n - 1
- 设置变量 c 用于记录叶子结点向上回溯至根结点所经过的结点下标,c初始时为当前待编码字符的下标 i,f 用于记录 i 的双亲结点的下标
- 从叶子结点向上回溯至根结点,求得字符 i 的编码,当 f 没有达到根结点时,循环执行以下操作:
- 回溯一次 start 向前指一个位置,即 --start
- 若结点 c 是 f 的左孩子,则生成代码 0;否则生成代码 1,生成的代码 0 或 1 保存在 cd[start] 中
- 继续向上回溯,改变 c 和 f 的值
- 根据数组 cd 的字符串长度为第 i 个字符编码分配空间 HC[i],然后将数组 cd中的编码复制到 HC[i] 中
- 释放临时空间 cd
void CreatHuffmanCode(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; // start 开始时指向最后,即编码结束符位置
c = i; // f 指向结点 c 的双亲结点
f = HT[i].parent; // 从叶子结点开始向上回溯, 直到根结点
while(f != 0){
--start; // 回溯一次 start 向前指一个位置
if(HT[f].lchild == c)
cd[start] = '0'; // 结点c是f的左孩子, 则生成代码0
else
cd[strat] = '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; // 释放临时空间
}
3、文件的编码和译码
1)编码
有了字符集的哈夫曼编码表之后,对数据文件的编码过程是:
依次读入文件中的字符 c, 在哈夫曼编码表 HC 中找到此字符,将字符 c 转换为编码表中存放的编码串
2)译码
对编码后的文件进行译码的过程必须借助于哈夫曼树。具体过程是:
依次读入文件的二进制码,从哈夫曼树的根结点(即HT[m])出发,若当前读入 0, 则走向左孩子,否则走向右孩子。 一旦到达某一叶子 HT[i] 时便译出相应的字符编码 HC[i]。然后重新从根出发继续译码,直至文件结束
一 叶 知 秋,奥 妙 玄 心