哈夫曼树和哈夫曼编码
为什么要设计哈夫曼编码?
如果都是定长编码,将会有很多空间被浪费,运算速度也较低。
定长编码是指在编码系统中,每个符号的代码长度相等,如常用的ASCII码,每个符号的编码都是一个字节。
不定长编码应用于各种符号的使用频率差异较大的场合。其基本思想是利用各种符号出现的统计频率来编码,使经常出现的符号的编码较短,不常出现的符号的编码较长,目的是使信息经过编码后的编码文件长度尽可能短。因此不定长编码也称为统计编码。统计编码相比定长编码不仅节省磁盘空间,还能起到提高传递、运算速度的效果。
什么是哈夫曼树?
例:将学生的百分制成绩转换为五分制成绩:
≥90分: A,
80~89分: B,
70~79分: C,
60~69分: D,
<60分: E。
假设有10000个学生,5%的数据需要1次比较,15%的需要2次比较,40%需要3次,40%需要三次,那么总共比较的次数为:10000*(5% + 15%*2 + 40%*3 + 40%*4)= 31500次。
**叶子结点的权值:**对叶子结点赋予的一个有意义的数值量。
**二叉树的带权路径长度:**设二叉树具有n个带权值的叶子结点,从根结点到各个叶子结点的路径长度与相应叶子结点权值的乘积之和。
Huffman树:给定一组具有确定权值的叶子结点,带权路径长度最小的二叉树。
构造哈夫曼树的算法
给定N个权值分别为w1, w2,…, Wn的节点。构造哈夫曼树的算法描述如下:
1)将这N个结点分别作为N棵树仅含一个结点的二叉树,构成森林F. 2)构造一个新节点,并从F中选取两棵根结点权值最小的树作为新节点的左、右子树,并且将新节点的权值置为左、右子树上根结点的权值之和。 3)从F中删除刚才选出的两棵树,同时将新得到的树加入F中。 4)重复步骤2和3,直至F中只剩下一棵树为止。
哈夫曼树的特点:
- 权值越大的叶子结点越靠近根结点,而权值越小的叶子结点越远离根节点。
- 只有度为0(叶子结点)和度为2(分支结点)的结点,不存在度为1的结点。
例题:八个结点权值如下,构造哈夫曼树。
【答题思路】找森林中两个最小的,相加后结果放回森林,再找两个最小的。
- 2+3 = 5
- 5+6 = 11
- 7+10 =17
- 11+17 = 28
- 19+21 = 40
- 28+32 = 60
- 40+60 = 100 【答案】
哈夫曼树数据结构实现
设置一个数组(向量) huffTree[2n-1]保存哈夫曼树中各个结点的信息,数组(向量)元素的结点结构。
为什么是2n-1?
由二叉树性质有:叶子结点数 = 有两个分叉的结点树 + 1
哈夫曼树的叶子结点为n,且只有两个叉的分支结点,故分支结点为n-1,所以,总数为2n-1。
- data:编码值
- weight:权值域,保存该结点的权值;
- lchild:指针域,结点的左孩子结点在数组中的下标;
- rchild:指针域,结点的右孩子结点在数组中的下标;
- parent:指针域,该结点的双亲结点在数组中的下标。
线索二叉树
- 线索:将二叉链表中的空指针域指向前驱结点和后继结点的指针被称为线索;
- 线索化:使二叉链表中结点的空链域存放其前驱或后继信息的过程称为线索化;
- 线索二叉树:加上线索的二叉树称为线索二叉树。
目的:快速找到二叉树的前驱和后继。
解决方法:加上两个指针,前驱和后继
出现问题:但是这样会浪费内存空间,怎么办?
解决方法:许多节点并没有左右孩子,那么将前驱后继装到空闲的左右孩子里就可以了。
此时出现另外一个问题:怎么分辨是左右孩子,还是前驱后继呢?毕竟他们都放在一个地方。
解决方法:在左右孩子旁边加上bool标识,如果tag=0就是孩子指针,如果tag=1就是前驱后继。(bool比指针占空间小)
问题:那么这种空闲的指针域多不多呢?
任何一个n个节点的二叉树的空闲指针域有n+1个。
因为n个节点,链域有2n个;除了根节点外,每个节点都有一个父节点,形成n-1个有内容的链域。没内容的链域一共有 2n-(n-1) = n+1 个
如果一个结点有左右子女,前驱后继指针没地方放,就还是得遍历数组去找前驱后继。
结点结构:
enum flag{Child, Thread};//枚举
typedef struct BiThrNode
{
ElemType data;
struct BiThrNode *lchild, *rchild;
flag ltype, rtype;
}*BithrTree;