简介
哈弗曼树的出现主要是用来对字符编码,压缩空间的,编码时,让使用频率高的用短码,使用频率低的用长码,以优化整个报文编码,使用频率或者次数可以看做权值,因此:哈夫曼树又称最优二叉树,是一种带权路径长度最短的二叉树
下面是哈弗曼树的实例图,红色节点为终端带权节点(基础节点),蓝色为后生成的非终端节点(如果基础节点为n,生成哈弗曼树总结点则为2n-1个)
数据压缩
实现哈夫曼编码的方式主要是创建一个二叉树和其节点。这些树的节点可以存储在数组里,数组的大小为符号数的大小n,而节点分别是终端节点(叶节点、基础节点)与非终端节点(内部节点、根据哈夫曼算法后生成的节点)。
一开始,所有的节点都是终端节点,节点内有三个字段:
1.符号
2.权重
3.指向父节点的链接
而非终端节点内有四个字段:
1.权重
2.指向父节点的链接
3.指向两个子节点的链接
4.指向父节点的链接
基本上,我们用'0'与'1'分别代表指向左子节点与右子节点,最后为完成的二叉树共有n个终端节点与n-1个非终端节点,去除了不必要的符号并产生最佳的编码长度。
过程中,每个终端节点都包含着一个权重,两两终端节点结合会产生一个新节点,新节点的权重是由两个权重最小的终端节点权重之总和;新节点也加入生成节点的队列中,且已经两两结合的点不二次参与,持续进行此过程直到只剩下一个节点为止。
数据解压缩
(作为扩展,摘抄自百度百科)
哈夫曼码树的解压缩就是将得到的前置码转换回符号,通常借由树的追踪,将接收到的比特串一步一步还原。但是要追踪树之前,必须要先重建哈夫曼树;某些情况下,如果每个符号的权重可以被事先预测,那么哈夫曼树就可以预先重建,并且存储并重复使用,否则,发送端必须预先发送哈夫曼树的相关信息给接收端。
最简单的方式,就是预先统计各符号的权重并加入至压缩之比特串,但是此法的运算量花费相当大,并不适合实际的应用。若是使用Canonical encoding,则可精准得知树重建的数据量只占B2^B比特(其中B为每个符号的比特数(bits))。如果简单将接收到的比特串一个比特一个比特的重建,例如:'0'表示父节点,'1'表示终端节点,若每次读取到1时,下8个比特则会被解读是终端节点(假设数据为8-bit字母),则哈夫曼树则可被重建,以此方法,数据量的大小可能为2~320字节不等。虽然还有很多方法可以重建哈夫曼树,但因为压缩的数据串包含"traling bits",所以还原时一定要考虑何时停止,不要还原到错误的值,如在数据压缩时时加上每笔数据的长度等。
哈夫曼编码示例
如上图所式,则为编码后的哈夫曼树:连线箭头为带权路径,权值从根节点到叶子节点递减,那么下图的左右两个哈夫曼树的终端节点编码如下:
左侧哈弗曼树:A: 00 B:01 C:10 D:11
右侧哈弗曼树:A:000 B:001 C:01 D:1
哈弗曼编码的过程,由于带权问题,解决了编码时的前缀重复问题,方便解码时识别,同时将字符编码长度大幅度缩小,可谓一举两得。
哈弗曼树的实现
哈夫曼树的实现,先让节点间不重复地两两结合,生成新节点直到只有一个新节点位置;由于实际上生成哈弗曼树的总结点个数总是为2n-1(n为终端节点或基础节点),可以根据这个特征,每次筛选出来两个最小的,直到生成n-1个新节点则结束,新节点按生成顺序排到老节点之后。
下面的算法就是根据此特性来生成的
初始化
定义一个数组结构的基础结构体,里面父指针和孩子指针均使用索引,因为操作都是在一个2n-1大小的数组中处理的(另外这里忽略了字符,算法里面用不到,实际也可以根据需要来处理)
typedef struct {
int parent; //父节点索引
int weight; //权值
int lchild, rchild; //左右孩子索引,终端节点默认为-1,且也不可能为父节点,新生成的非终端节点则指向其子孩子节点,
} LSOrderHufmTree;
生成哈弗曼树数组
这里生成的哈弗曼树实际上是一个一维数组,实际使用可能到这里就行了,后面转化成哈弗曼树并进行编码
生成哈弗曼树有以下步骤:
1.定义LSOrderHufmTree类型的数组hufmTreeList哈弗曼树所有节点,权值数组(也可以包含权值传递字符串对象,这里只会用到权值) ,数量为count, 那么hufmTreeList的数量理应为count*2 - 1 (2n-1),声明为了MAXCount
2.将所有基础节点的权值映射到哈弗曼树的前count个节点中,且父子指针均指向-1(-2等也可以只要不影响取索引)
3.从索引为count开始(临时变量定义为i),遍历下面逻辑到MAXCount前一个 (根据哈弗曼树特性,生成n-1个新节点,便生成了哈弗曼树的所有非终端节点),每次生成一个非终端节点
4.从数组索引第一项到上一次新生成的节点索引(i-1)区间,选出两个最小节点,且没有父节点(没有被选出来过),索引分别为min1,min2,节点为minNode1,minNode2
5.在遍历到即将生成的非终端索引节点处(i)开始赋值,标记父节点为为-1,权值为两个最小节点相加,左右孩子分别指向第一第二最小节点,且两个最小节点的父节点指向自己
6.生成非终端节点的临时变量i自增,重复步骤4,直到循环结束
因此可以看出最后生成的一个节点,即 第2*n - 1个节点为哈弗曼树根节点
void generateOrderHufmTree(LSOrderHufmTree *hufmTreeList, int *list, int count) {
int MAXCount = count*2 - 1; //n个节点生成哈夫曼树,那么生成的节点数为n-1,总结点树为2n-1
//默认节点都是子节点
for (int i = 0; i < count; i++) {
hufmTreeList[i].weight = list[i];
hufmTreeList[i].parent = -1;
hufmTreeList[i].lchild = -1;
hufmTreeList[i].rchild = -1;
}
int MAXINT = INT32_MAX; //假设其为最大数
for (int i = count; i < MAXCount; i++) {
//生成新节点的同时找出最小节点
//找出最小的两个节点
int min1 = 1, min2 = 1;
int minNode1 = MAXINT, minNode2 = MAXINT;
for (int j = 0; j < i; j++) {
if (hufmTreeList[j].parent >= 0) continue;
if (hufmTreeList[j].weight < minNode1) {
minNode2 = minNode1;
min2 = min1;
minNode1 = hufmTreeList[j].weight;
min1 = j;
}else if (hufmTreeList[j].weight < minNode2) {
minNode2 = hufmTreeList[j].weight;
min2 = j;
}
}
//从子节点后面开始生成新的虚拟父节点,作为新的节点参与计算
hufmTreeList[i].parent = -1;
hufmTreeList[i].weight = minNode1 + minNode2;
hufmTreeList[i].lchild = min1;
hufmTreeList[i].rchild = min2;
hufmTreeList[min1].parent = i;
hufmTreeList[min2].parent = i;
}
}
转化为哈弗曼树
转化成哈弗曼树结构之前使用前面生成的哈弗曼树数组,哈弗曼树索引2n-2向即为根节点,取出,开始生成哈弗曼树结构,如图一图二所示
定义哈夫曼树型基础数据结构
typedef struct HufmTree {
int weight; //权值
int index; //自己的索引,以便与编码赋值
struct HufmTree *lchild, *rchild; //左右子节点指针
} LSListHufmTree;
转化逻辑较为简单,从根节点开始,每个节点处都保存着子节点的索引,顺序索引向下展开即可
1.创建node节点,赋值对应索引数据
2.查看其左子节点索引是否存在(索引>=0),若存在,则向下遍历,返回将对应索引子节点带入本函数,再次进入步骤1;否则,左子节点置空
3.查看其右子节点索引是否存在(索引>=0),若存在,则向下遍历,返回将对应索引子节点带入本函数,再次进入步骤1;否则,右子节点置空
4.结束,返回到上一个函数栈,直到第一次遍历的函数栈结束才真正结束
LSListHufmTree *generateHufmTree(LSOrderHufmTree root, LSOrderHufmTree *list) {
LSListHufmTree *node = (LSListHufmTree *)malloc(sizeof(LSListHufmTree));
node->weight = root.weight;
if (root.lchild >= 0)
node->lchild = generateHufmTree(list[root.lchild], list);
else
node->lchild = NULL;
if (root.rchild >= 0)
node->rchild = generateHufmTree(list[root.rchild], list);
else
node->rchild = NULL;
return node;
}
对基础节点进行编码
由于基础节点均为叶子节点,所以从根节点,不停的对所有节点进行带权向下赋值,向下过程不断增加权值,即*2,且累计编码长度
注意:这里由于使用数字表达的,为了查看方便,需要避免第一位为0的情况,默认在最高位多加上一个1,例如下面所示
原来的编码
左侧哈弗曼树:A: 00 B:01 C:10 D:11
右侧哈弗曼树:A:000 B:001 C:01 D:1
为了查看方便改编的编码,最后生成的结果去掉第一位的1即可,仅仅适用与案例,实际操作应以基础编码为基准,作为编码表
左侧哈弗曼树:A: 100 B:101 C:110 D:111
右侧哈弗曼树:A:1000 B:1001 C:101 D:11
实现逻辑:
1.声明节点node为遍历的当前节点,list为结果数组,code为带权编码
2.每经过该方法默认向下遍历一次,权值自增*2,即code *= 2;
3.如果左子节点存在,不赋值,左子节点作为当前节点,进入步骤1,否则,右节点编码赋值为code
4.如果右子节点存在,不赋值,右子节点作为当前节点,进入步骤1,否则,右节点编码赋值为code
void generateHufmEncoding(LSListHufmTree *node, int *list, int code) {
if (!node) return;
code *= 2;
if (node->lchild)
generateHufmEncoding(node->lchild, list, code);
else
list[node->index] = code;
if (node->lchild)
generateHufmEncoding(node->rchild, list, code + 1);
else
list[node->index] = code * 2 + 1;
}