树
树的定义
在树中通常将数据元素称为结点(node)。
树(tree)是n(n≥0)个结点的有限集合。当n=0时,称为空树;任意一棵非空树满足以下条件:
(1)有且仅有一个特定的称为根(root)的结点;
(2)当n>1时,除根结点之外的其余结点被分成m(m>0) 个互不相交(结点不能属于多个子树,子树之间不能有关系)的有限集合,T1,T2, … ,Tm,其中每个集合又是一棵树,并称为这个根结点的子树(subtree)。
树的基本术语
-
(1)结点的度、树的度
某结点所拥有的子树的个数称为该结点的度(degree);树中各结点度的最大值称为该树的度。
对于一个拥有n个结点的树,其所有度的和为n-1。 -
(2)叶子结点、分支结点
度为0的结点称为叶子结点(leaf node),也称为终端结点;度不为0的结点称为分支结点(branch node),也称为非终端结点。 -
(3)孩子结点、双亲结点、兄弟结点
某结点的子树的根结点称为该结点的孩子结点(child node);反之,该结点称为其孩子结点的双亲结点(parent node);具有同一个双亲的孩子结点互称为兄弟结点(brothernode)。 -
(4)路径、路径长度
如果树的结点序列n1,n2…nk满足如下关系:结点:是结点ni+1的双亲(1≤i<k),则n1,n2…nk称为一条由n1至n的路径(path);路径上经过的边数称为路径长度(pathlength)。显然,在树中路径是唯一的。 -
(5)祖先、子孙
如果从结点x到结点y有一条路径,那么x就称为y的祖先(ancestor),y称为x的子孙(descendant)。显然,以某结点为根的子树中的任一结点都是该结点的子孙。 -
(6)结点的层数、树的深度(高度)、树的宽度
规定根结点的层数(level)为1,对其余任何结点,若某结点在第k层,则其孩子结点在第k十1层;树中所有结点的最大层数称为树的深度(depth),也称为树的高度:树中每一层结点个数的最大值称为树的宽度(breadth)。 -
(7)结点所在层数:根结点的层数为 1;对其余结点,若某结点在第 k 层,则其孩子结点在第 k+1 层
树的深度(高度):树中所有结点的最大层数
树的宽度:树中每一层结点个数的最大值 -
练习题
已知一颗度为4的树中,度为i(i>=1)的结点个数有i个,该树中有21个叶子结点。度总数为44+33+2*2=29,树结点总数为29+1,叶子结点为30-4-3-2=21.
线性结构和树结构的比较
树的抽象数据类型定义
ADT Tree
DataModel
树由一个根结点和若干棵子树构成,树中结点具有层次关系
Operation
InitTree:初始化一棵树
DestroyTree:销毁一棵树
PreOrder:前序遍历树
PostOrder:后序遍历树
LeverOrder:层序遍历树
endADT
树的存储结构
存储结构:数据元素及其逻辑关系在存储器中的表示
顺序存储:完全二叉树和满二叉树适用
树中结点之间的逻辑关系:
- 双亲表示法
用一维数组存储树中各个结点(一般按层序存储)的数据信息以及该结点的双亲在数组中的下标
适用于通过结点对双亲进行操作
template <typename DataType>
struct PNode
{
DataType data;
int parent;
};
- 孩子表示法
树的孩子表示法(child express)是一种基于链表的存储方法,即把每个结点的孩子排列起来,看成是一个线性表,且以单链表存储,称为该结点的孩子链表,则n个结点共有n个孩子链表(叶子结点的孩子链表为空表)。n个孩子链表共有个头指针,这n个头指针又构成了一个线性表,为了便于进行查找操作,可采用顺序存储(连续存储空间)。最后,将存放n个头指针的数组和存放n个结点数据信息的数组结合起来,构成孩子链表的表头数组。所以在孩子表示法中,存在两类结点:孩子结点和表头结点。
struct CTNode //孩子结点
{
int child;
CTNode* next;
}; //存放孩子链表的表头的数组
template <typename DataType>
struct CBNode //表头结点
{
DataType data;
CTNode* firstChild; //指向孩子链表的头指针
};
- 孩子兄弟表示法
树的孩子兄弟表示法(二叉链表):链表中的每个结点包括数据域和分别指向该结点的第一个孩子和右兄弟的指针
与树、森林与二叉树转换有关。
template <typename DataType>
struct TNode
{
DataType data;
TNode<DataType> *firstChild, *rightSib;
};
二叉树
二叉树的定义
- 二叉树(binary tree)是n(n≥0)个结点的有限集合,该集合或者为空集(称为空二叉树),或者由一个根结点和两棵互不相交的、分别称为根结点的左子树(left subtree)和右子树(right subtree)的二叉树组成。
- 二叉树具有如下特点:
①每个结点最多有两棵子树,所以二叉树中不存在度大于2的结点;
②二叉树是有序的,二叉树的左右子树不能任意颠倒,如果某结点只有一棵子树,一定要指明它是左子树还是右子树。
几种特殊的二叉树
斜树
左斜树:所有结点都只有左子树的二叉树
右斜树:所有结点都只有右子树的二叉树
斜树:左斜树和右斜树的统称
斜树有什么特点呢?
(1)每一层只有一个结点
(2)结点个数与其深度相同
斜树是树结构的特例,是从树结构退化成了线性结构
满二叉树
所有分支结点都存在左子树和右子树,并且所有叶子都在同一层上的二叉树
可以使用顺序存储
满二叉树有什么特点呢?
(1)叶子只能出现在最下一层
(2)只有度为0和度为2的结点
(3)在同样深度的二叉树中结点个数最多
(4)在同样深度的二叉树中叶子结点个数最多
满二叉树是树结构的特例,是最丰满的二叉树
完全二叉树
- 在满二叉树中,从最后一个结点开始,连续去掉任意个结点得到的二叉树
- 可以使用顺序存储结构
- 完全二叉树有什么特点呢?
(1)叶子结点只能出现在最下两层且最下层的叶子结点都集中在二叉树的左面
(2)完全二叉树中如果有度为1的结点只可能有一个,且该结点只有左孩子
(3)深度为k的完全二叉树在k-1层上一定是满二叉树
(4)在同样结点个数的二叉树中,完全二叉树的深度最小
二叉树的基本性质
适用所有二叉树的性质
- 在一棵二叉树中,如果叶子结点数为 n0,度为 2 的结点数为 n2,则有: n0=n2+1
- 二叉树的第 i 层上最多有2i-1个结点(i≥1)
- 一棵深度为 k 的二叉树中,最多有 2k-1个结点
适用完全二叉树的性质
- 具有 n 个结点的完全二叉树的深度为 ⌊log2n⌋ +1,(⌊⌋为向下取整)。
- 对一棵具有 n 个结点的完全二叉树中从 1 开始按层序编号,对于任意的序号为 i(1≤i≤n)的结点(简称结点 i),有:
(1)如果 i>1,则 结点 i 的双亲结点的序号为 i/2,否则结点 i 无双亲结点
(2)如果 2i≤n,则结点 i 的左孩子的序号为 2i,否则结点 i 无左孩子
(3)如果 2i+1≤n,则结点 i 的右孩子的序号为2i+1,否则结点 i 无右孩子
即根结点的编号为1;对于编号为的结点,
左孩子如果存在,则编号为2i;
右孩子如果存在,则编号为2i+1。
二叉树的遍历
-
从根结点出发,按照某种次序(前序(根)、后序(根)和层序(次)等访问)树中所有结点,并且每个结点仅被访问一次
-
树的前序遍历操作定义:
若二叉树为空,则空操作返回;否则:
(1)访问根结点
(2)前序遍历根结点的左子树
(3)前序遍历根结点的右子树
A B D E H I F C G -
树的中序遍历:
若二叉树为空,则空操作返回;否则:
(1)中序遍历根结点的左子树
(2)访问根结点
(3)中序遍历根结点的右子树 -
树的后序遍历操作定义:
若二叉树为空,则空操作返回;否则:
(1)后序遍历根结点的左子树
(2)后序遍历根结点的右子树
(3)访问根结点
D H I E F B G C A -
树的层序遍历操作定义:
从二叉树的根结点开始,从上至下逐层遍历,在同一层中,则按从左到右的顺序对结点逐个访问
A B C D E F G H I
- 统计叶子结点数目
方法1:LeafCount为全局变量,调用前初始化值为0
int LeafCount=0;{
void LeafNum (BiTree root)
{if (root!=NULL)
{
if(root->lchild==NULL&&root->rchild==NULL)
LeafCount++;
LeafNum(root->lchild);
LeafNum(root->rchild);
}
方法2:采用函数返回值
int leaf(BiTree root){
int LeafCount;
if(root==NULL)
LeafCount =0;
else if((root->lchild==NULL)&&(root->rchild==NULL))
LeafCount =1;
else
LeafCount =leaf(root->lchild)+leaf(root->rchild);
return LeafCount;
}
- 二叉树的高度:
int depth=0;
void PreTreeDepth(BiTree root,int h)
{
if(root-NULL)
if(h>depth)
depth=h;
PreTreeDepth(root->lchild,h+1);
PreTreeDepth(root->rchild,h+1);
}
int PostTreeDepth(BiTree root)
{
int hl,hr,max;
if(root!=NULL)
{ hl=PostTreeDepth(root->lchild);
hr=PostTreeDepth(root->rchild);
max=hl>hr?hl:hr;
return max+1;
}
else
return 0;
}
- 二叉树的恢复
- 可恢复二叉树的结点序列组合:
- 先序序列和中序序列
①由先序序列中的第一个结点确定根结点D。
②通过根结点D分割中序序列:D之前是左子树的中序序列,D之后是右子树的中序序列,同时获得左、右子树的结点个数。
③根据左子树的结点个数,分割先序序列:第一结点根D,之后是左子树的先序序列,最后是右子树的先序序列。 - 中序序列和后序序列
- 先序序列和中序序列
- 不可恢复二叉树的结点序列组合
先序序列和后序序列
- 可恢复二叉树的结点序列组合:
二叉树的顺序存储
二叉树的顺序存储结构是用一维数组存储二叉树的结点,结点的存储位置(下标)应能体现结点之间的逻辑关系——父子关系——而完全二叉树中结点的编号可以唯一地反映结点之间的逻辑关系
二叉树的顺序存储结构一般仅存储完全二叉树
二叉链表
- 二叉链表:二叉树的每个结点对应一个链表结点,链表结点存放结点的数据信息和指示左右孩子的指针
- 叶子结点的标志:左右孩子指针均为空
template <typename DataType> struct BiNode
{
DataType data;
BiNode< DataType > *lchild, *rchild;
};
- n 个结点的二叉链表有2n-(n-1) = n+1 个空指针
2n为n个结点有2n个指针,n-1为所占用的指针即减去根结点。
查找双亲的时间性能:O(n) - 三叉链表的存储方法
在二叉链表中增加一个指向双亲的指针域
二叉链表的类定义
InitBiTree:初始化一棵空的二叉树
CreatBiTree:建立一棵二叉树
DestroyBiTree:销毁一棵二叉树
PreOrder:前序遍历二叉树
InOrder:中序遍历二叉树
PostOrder:后序遍历二叉树
LeverOrder:层序遍历二叉树
template <typename DataType>
class BiTree
{
public:
BiTree( ){root = Creat(root);}
~BiTree( ){Release(root);}
void PreOrder( ){PreOrder(root);}
void InOrder( ){InOrder(root);}
void PostOrder( ){PostOrder(root);}
void LeverOrder( );
private:
BiNode<DataType> *Creat(BiNode<DataType> *bt);
void Release(BiNode<DataType> *bt);
void PreOrder(BiNode<DataType> *bt);
void InOrder(BiNode<DataType> *bt);
void PostOrder(BiNode<DataType> *bt);
BiNode<DataType> *root;
};
- 二叉树的前序遍历
template <typename DataType>
void BiTree<DataType> :: PreOrder(BiNode<DataType> *bt)
{
if (bt == null) return; //递归调用的结束条件
else {
cout << bt->data; //访问根结点bt的数据域
PreOrder(bt->lchild); //前序递归遍历bt的左子树
PreOrder(bt->rchild); //前序递归遍历bt的右子树
}
}
- 中序
PreOrder(bt->lchild); //中序递归遍历bt的左子树
cout << bt->data; //访问根结点bt的数据域
PreOrder(bt->rchild); //中序递归遍历bt的右子树
- 后序
PreOrder(bt->lchild); //后序递归遍历bt的左子树
PreOrder(bt->rchild); //后序递归遍历bt的右子树
cout << bt->data; //访问根结点bt的数据域
- 二叉树的层序遍历
1.队列Q初始化;
2.如果二叉树非空,将根指针入队:
3.循环直到队列Q为空
3.1q=队列Q的队头元素出队:
3.2访问结点q的数据域;
3.3若结点q存在左孩子,则将左孩子指针入队;
3.4若结点q存在右孩子,则将右孩子指针入队;
template <typename DataType>
void BiTree<DataType> :: LeverOrder( )
{
BiNode<DataType> *Q[100], *q = null;
int front = -1, rear = -1;
if (root == null) return;
Q[++rear] = root;
while (front != rear)
{
q = Q[++front]; cout << q->data;
if (q->lchild != null) Q[++rear] = q->lchild;
if (q->rchild != null) Q[++rear] = q->rchild;
}
}
- 二叉链表的建立
扩展二叉树:将二叉树中每个结点的空指针引出一个虚结点,其值为一特定值如 ‘#’
扩展二叉树的前序遍历序列:A B # D # # C # #
template <typename DataType>
BiNode<DataType> *BiTree<DataType> :: Creat(BiNode<DataType> *bt)
{
char ch;
cin >> ch; //输入结点的数据信息,假设为字符
if (ch == ‘#’) bt = null; //建立一棵空树
else {
bt = new BiNode<DataType>; bt->data = ch;
bt->lchild = Creat(bt->lchild); //递归建立左子树
bt->rchild = Creat(bt->rchild); //递归建立右子树
}
return bt;
}
- 二叉链表的销毁
二叉链表是动态存储分配,二叉链表的结点是在程序运行过程中动态申请的,在二叉链表变量退出作用域前,要释放二叉链表的存储空间
template <typename DataType>
void BiTree<DataType> :: Release(BiNode<DataType> *bt)
{
if (bt == null) return;
else{
Release(bt->lchild); //释放左子树
Release(bt->rchild); //释放右子树
delete bt; //释放根结点
}
}
森林
m(m≥0)棵互不相交的树的集合
- 对于树:删去根结点就变成了森林
对于森林:增加一个根结点,将森林中的每一棵树作为这个根结点的子树,则森林就变成了一棵树 - 森林的遍历:按照某种次序依次遍历构成森林的 m(m≥0)棵树
前序遍历序列:A B C D E F G H I J
后序遍历序列:B A D E F C H J I G
树、森林与二叉树的转换
- 树转换为二叉树
(1)加线——树中所有相邻兄弟之间加一条连线
(2)去线——对树中的每个结点,只保留它与第一个孩子结点之间的连线,删去它与其它孩子结点之间的连线。
(3)层次调整——以根结点为轴心,将树顺时针转动一定的角度,使之层次分明。
树的前序遍历等价于二叉树的前序遍历!ABEFCDG
树的后序遍历等价于二叉树的中序遍历!EFBCGDA
树的根结点没有兄弟->二叉树根结点的右子树必为空
-
森林转换为二叉树
将一个森林转换为二叉树的方法是
(1)将森林中的每棵树转换为二叉树
(2)将每棵树的根结点视为兄弟,在所有根结点之间加上连线
(3)按照二叉树结点之间的关系进行层次调整
ⅰ加线:树中所有相邻兄弟之间加一条连线各棵树的根结点间也视为兄弟);
ⅱ删线:每一个结点,仅保留其与第一个孩子之间的连线,删去与其他孩子结点之间的连线;
ⅱ.旋转:以第一棵二叉树的根结点为轴心,将整棵树顺时针旋转一定的角度,使之结构清晰,左右子树分明。
-
二叉树转换为树(森林)
将一棵二叉树还原为树或森林,具体转换方法是
(1)加线——若某结点 x 是其双亲 y 的左孩子,则把结点 x 的右孩子、右孩子的右孩子、……,与结点 y 连线
(2)去线——删去所有双亲结点与右孩子结点的连线
(3)层次调整——整理由(1)、(2)两步所得到的树(森林),使之层次分明。
树与森林的遍历
树的遍历
- 树的先根遍历
若树不空,则遍历过程为:
访问根结点;
从左至右依次先根遍历根结点的各棵子树。
void RootFirst(CSTree root)
if (root!=NULL)
Visit(root->data); /*访问根结点*/
p=root->FCh;
while (p!=NULL)
{ RootFirst(p) /*访问p子树*/
p=p->Nsib;}
}
}
void RootFirst(CSTree root)
{ if (root!=NULL)
{ Visit (root ->data); /*访问根结点*/
RootFirst (root->FCh); /*先根遍历首子树*/
RootFirst (root->Nsib); /*先根遍历兄弟树*/
}
}
- 树的后根遍历
若树不空,则遍历过程为:
从左至右依次后根遍历根结点的各棵子树;
访问根结点。
森林的遍历
- 森林的先序遍历
访问森林第一棵树的根结点;
先序遍历森林第一棵树根结点的子树森林;
先序遍历除第一棵树之外其余树构成的森林。 - 森林的中序遍历
中序遍历森林第一棵树根结点的子树森林;
访问森林第一棵树的根结点;
中序遍历除第一棵树之外其余树构成的森林
森林与二叉树遍历的对应关系
- 树的前序遍历等价于二叉树的前序遍历
树的后序遍历等价于二叉树的中序遍历
森林的前序遍历等价于二叉树的前序遍历
森林的中序遍历等价于二叉树的中序遍历
森林的后序遍历不常用
二叉树的非递归遍历
递归算法
优点:简洁、可读性强,而且其正确性容易得到证明,这给程序的编写和调试带来很大的方便。
不足:运行效率低,消耗的时间、空间资源都较多
算法思路
建栈,并初始化;
若遍历结点存在或者栈不为空
①若结点存在:结点入栈,指向其左孩子
②否则:出栈,访问结点,指向其右孩子
否则,遍历结束
- 中序遍历的非递归算法
void InOrder(BiTree root)
{ SqStack S;
InitStack(S);
BiNode *p;
p=root;
while(p||!StackEmpty(S)){
if(p){
Push(S,p); /*保存根结点*/
p=p->lchild; /*遍历左子树*/
}
else{
Pop(S,p); /*弹出根结点*/
visit(p->data); /*访问根结点*/
p=p->rchild; /*遍历右子树*/
}
}
}
- 前序遍历的非递归算法
if(p){
visit(p->data); /*访问根结点*/
Push(S,p); /*保存根结点*/
p=p->lchild; /*遍历左子树*/
}
else{
Pop(S,p); /*弹出根结点*/
p=p->rchild; /*遍历右子树*/
}
后序遍历的非递归算法比前序中序复杂
线索二叉树
目的:利用他的空孩子指针,指向它的遍历前驱和后继,即把空指针改为线索,以便实现不需要递归和栈,就能够完成对二叉树的遍历
在2n个指针域中只有n-1个指针域用来存储孩子结点的地址,存在n+1个空指针域,可以利用这些空指针指向该结点在某种遍历序列中的前驱和后继结点。这些指向遍历序列前驱、后继结点的指针称为线索;
把空指针修改为线索的过程称为线索化;
经过线索化的二叉树称为线索二叉树;
含有线索的二叉链表称为线索链表。
- 线索二叉树的结点结构
Ltag:
0 LChild域指示结点的左孩子
1 LChild域指示结点的遍历前驱
Rtag:
0 RChild域指示结点的右孩子
1 RChild:域指示结点的遍历后继
template <typename DataType>
Struct ThrNode{
DataType data;
int ltag,rtag;
ThrNode<DataType>*lchild,*rchild;
};
- 线索化的实现
- 伴随着遍历而进行的线索化过程中,设两个指针:p和pre,指针p指向当前访问结点,指针pre指向当前访问结点p的前驱结点。
- (pre和p)始终是动态变化的一对(前驱和后继)
- 修改指针:
p的Lchild指针为空,则修改为指向前驱结点pre;
pre的Rchild:指针为空,则修改为指向后继结点p。
- 中序线索化算法
void Inthread(BiTree p)
if (p!=NULL)
{ Inthread(p->LChild);
/*遍历左子树*/
if (p->LChild==NULL)
p->LChild=pre;p->Ltag=1;
if (pre!=NULL&&pre->RChild==NULL)
pre->RChild=p;pre->Rtag=1;
pre=p;
Inthread(p->RChild);/*遍历右子树*/
}
}
线索二叉树的遍历
- 中序线索二叉树的遍历
1.寻找中序遍历的第一个结点中序遍历的第一个结点是树中处于“最左下端”的结点,从根沿着左孩子指针找到没有左孩子的结点即可。
BiTNode InFirst(BiTree Bt)
{
BiTNode *p=Bt;
if (Ip)
return(NULL)方
while(p->Ltag==0)
p=p->LChild;
return p;
- 2.寻找当前节点的后继结点
BiTNode *InNext(BiTNode *p)
{ if(p->Rtag==1)Next=p->RChild;
else
{ for(q=p->RChild;q->Ltag==0;q=q->LChild);
Next=q;
}
return(Next);
}
- 3.中序线索二叉树的遍历算法
void TinOrder(BiTree bt)
{
BiTNode *p;
p=InFirst(bt);
while (p){
visit(p);
p=InNext(p);
}
}
哈夫曼算法(最优二叉树)
- 概念
- 路径:树中一个结点到另一个结点之间的分支序列。
- 路径长度:路径上分支的条数,
- 叶子结点的权值:对叶子结点赋予的一个有意义的数值量
- 带权路径长度:结点的权值与该结点到树根间路径长度的乘积。
- 二叉树的带权路径长度(WPL):从根结点到各个叶子结点的路径长度与相应叶子结点权值的乘积(所有叶子结点的带权路径长度)之和
- 最优二叉树(哈夫曼树):给定一组具有确定权值的叶子结点,带权路径长度最小的二叉树
- 特点:
(1)权值越大的叶子结点越靠近根结点
(2)只有度为 0 和度为 2 的结点,不存在度为 1 的结点 - 对于给定的N个叶子结点,构造哈夫曼树,其最终总的结点数一定是:2N-1。
- 哈夫曼算法的基本思想
- 初始化:由 n 个权值构造 n 棵只有一个根结点的二叉树,得到一个二叉树集合 F={T1,T2,…,Tn};
- 重复下述操作,直到集合 F 中只剩下一棵二叉树
2.1 选取与合并:在 F 中选取根结点的权值最小的两棵二叉树分别作为左右子树构造一棵新的二叉树,这棵新二叉树的根结点的权值为其左右子树根结点的权值之和;
2.2 删除与加入:在 F 中删除作为左右子树的两棵二叉树,并将新建立的二叉树加入到F中;
【算法——数据表示】
在使用哈夫曼树进行编码和译码时,既要用结点的双亲信息,又要用结点的孩子信息,所以采用静态三叉链表存储哈夫曼树。
采用数组还是链表呢?
须存储哪些关系呢?
struct ElemType
{
int weight; /*假定权值为整数*/
int parent, lchild, rchild;
};
函数原型:void HuffmanTree(ElemType huffTree[ ], int w[ ], int n )
哈夫曼算法
void HuffmanTree(ElemType huffTree[ ], int w[ ], int n )
{
int i, k, i1, i2;
for (i = 0; i < 2*n-1; i++) /*初始化,所有结点均没有双亲和孩子*/
{
huffTree[i].parent = -1; huffTree[i].lchild = -1; huffTree[i].rchild = -1;
}
for (k = n; k < 2*n-1; k++) /*n-1次合并*/
{
Select(huffTree, i1, i2); /*权值最小的两个根结点下标为i1和i2*/
huffTree[k].weight = huffTree[i1].weight + huffTree[i2].weight;
huffTree[i1].parent = k; huffTree[i2].parent = k;
huffTree[k].lchild = i1; huffTree[k].rchild = i2;
}
}
哈夫曼编码
- 在信息传输等实际应用中,需将文本中出现的字符进行二进制编码,传输过后,又要将二进制码翻译为原先的字符,这就是典型的编码与译码问题。
- 在编码的设计中,通常遵守两个原则:
(1)编码能够唯一地被译码。
(2)编码长度要尽可能的短。
利用哈夫曼树可以得到平均长度最短的编码,因此,在信息传输、数据压缩等方面,哈夫曼树有着广泛的应用。 - 编码:给每一个对象标记一个二进制位串来表示一组对象
等长编码:用长度相等的二进制位串表示一组对象
前缀编码:任何一个字符的编码都不是同一字符集中另一字符编码的前缀。
如果每个对象的使用频率不等,如何获得较高的编码效率?
不等长编码:表示一组对象的二进制位串的长度不相等
设计不等长编码时,必须考虑解码的唯一性 - 用哈夫曼树设计编码的设想是:
每个待编码的字符对应一个叶子结点;
每个字符的使用频率对应叶子的权值;
每个字符的编码对应根到叶子的路径。 - 构造哈夫曼编码:
对树的每条分支做标记:左分支标0,右分支标1
根到叶子结点路径所对应的0、1序列,构成该叶子结点对应字符的编码。
解码
前缀编码:在一组编码中,任一编码都不是其它任何编码的前缀
前缀(无歧义)编码保证了在解码时不会有多种可能
哈夫曼编码
- 哈夫曼编码的构造
由哈夫曼树构造哈夫曼编码,编码是由后向前逐位生成,需要从叶子出发,走一遍从叶子到根的路径,每经过一条分支,就得到一位哈夫曼编码值。
结点为双亲的左孩子,当前编码位为0,否则为1。
由叶子走到根,就逆向完成了叶子结点编码的构造。
例:一组字符{A, B, C, D, E, F, G}出现的频率分别是{9, 11, 5, 7, 8, 2, 3},设计最经济的编码方案
哈夫曼编码方案:
A:00
B:10
C:010
D:110
E:111
F:0110
G:0111
等长编码的长度:3
哈夫曼编码的平均长度:
(2×9+3×5+4×2+4×3+2×11+3×7+3×8)/45
= 2.67
哈夫曼编码算法
存储定义:
typedef char* Huffmancode [n+1];//Huffmancode 为char*[n+1]
char* cd;
int start;
# hc为Huffmancode指针数组
CrtHuffmanCode(HuffmanTree ht,Huffmancode hc,int n)
{ cd=(char* )malloc(n *sizeof(char));
cd[n-l]='0';
for(i=1;i<=n;i++)
{start=n-1;c=i;p=htcl.parent;
while( p!=0)
if(ht[p].Lchild ==c)cd[--start]='0';
else cd[--start] ='1';
c=p;p=ht[c].parent;}
hc[i]=(char *)malloc((n-start)*sizeof(char));
strcpy(hc[i],&cd[start]);}
free(cd);
}