哈夫曼树
给定N个权值作为N个叶子结点
,构造一棵二叉树,若该树的带权路径长度达到最小,称这样的二叉树为最优二叉树,也称为哈夫曼树(Huffman Tree)。哈夫曼树是带权路径长度最短的树,权值较大的结点离根较近。
定义
哈夫曼树又称最优二叉树,是一种带权路径长度最短
的二叉树。所谓树的带权路径长度,就是树中所有的叶结点的权值乘上其到根结点的路径长度(若根结点为0层,叶结点到根结点的路径长度为叶结点的层数}。树的路径长度是从树根到每一结点的路径长度之和,记为WPL = (W1*L1+W2*L2+W3*L3+…+Wn*Ln),N个权值Wi(i=1,2,…n)构成一棵有N个叶结点的二叉树,相应的叶结点的路径长度为Li(i=1,2,…n)。可以证明哈夫曼树的WPL是最小的。
如下图:
实现
了解了哈夫曼树的定义,那么该如何去用实现并创建一颗哈夫曼树呢?
根据哈夫曼树的定义及特点可知,构建哈夫曼树总共分为三步:
- 从给出的节点中找出两个较小的组成一颗树(分别作为左右孩子,一般规定的是权值较小的为左孩子),根节点的权值为两者之和。
- 删除(或标记)已经构造过的节点(避免重复构造),并将构造好的根节点放入其中。
- 重复1,2步,直至只剩下一个根节点为止
可结合下图进行推理验算。
逻辑上已经清楚如何创建,如何用代码进行实现呢?
代码实现
首先定义一个哈夫曼树的结构体,其中包含四个int域和一个char域,分别为节点自身权值、双亲及左右孩子的下标和数据域。
typedef struct{
char data; //数据域
int weight; //结点的权值
int parent, lChild, rChild; //双亲与孩子的下标
}htNode,*HuffmanTree;
初始化哈夫曼树
int initHuffmanTree(HuffmanTree& HT){
HT = (htNode*)malloc(sizeof(htNode) * (2 * 10));//给HT分配2 * 10个htNOde大小的htNode类型的数组
for (int i = 1; i <= 2 * 10 - 1; i++){
HT[i].parent = HT[i].lChild = HT[i].rChild = -1;//双亲和孩子的值都置为-1
}
printf("请输入数据:\n");
for (int i = 1; i <= 10; i++){
//scanf("%c ",&HT[i].data);
char a = getchar();
if(a == '\n') //遇到回车就结束
break;
else
HT[i].data = a; //给每个结点赋予数据
}
printf("请输入权值:\n");
for (int i = 1; i <= 10; i++){
scanf("%d",&HT[i].weight);//给每个结点赋予权值
}
char c = getchar();//这个来接收上面的回车
return 1;
}
接着是创建哈夫曼树
void createHuffmanTree(HuffmanTree& HT, int n){
if (n <= 1) //如果结点数小于等于1,不创建
return;
int min1, min2; //定义两个数,来存储每次选取最小两个结点的权值
int rnode, lnode;
for (int i = n + 1; i <= 2 * n -1; i++){
int min1 = INT_MAX; int lnode = -1;
int min2 = INT_MAX; int rnode = -1;
for (int j = 1; j <= i - 1; j++){
if (HT[j].weight < min1 && HT[j].parent == -1){
min2 = min1;
rnode = lnode;//碰到比min1小的,那min1的值就给第二小的min2,下标也给它
min1 = HT[j].weight; lnode = j; //然后最小的给min1,下标同理
}else if (HT[j].weight < min2 && HT[j].parent == -1){
min2 = HT[j].weight;
rnode = j;
}
}
HT[lnode].parent = HT[rnode].parent = i;
HT[i].lChild = lnode;
HT[i].rChild = rnode;
HT[i].weight = HT[lnode].weight + HT[rnode].weight;
}
}
这样就可以完整的创建出一颗哈夫曼树了。当然,哈夫曼树不止只有用来存储数据这么简单,其对应的哈弗曼编码才是最实用的王牌
哈夫曼编码
哈夫曼编码(Huffman Coding),又称霍夫曼编码,是一种编码方式,哈夫曼编码是可变字长编码(VLC)的一种。Huffman于1952年提出一种编码方法,该方法完全依据字符出现概率来构造异字头的平均长度最短的码字,有时称之为最佳编码,一般就叫做Huffman编码。
如最开始的第一张图可知,令哈夫曼树左子树分支编码为0,右子树分支编码为1,根据叶子节点的最短路径为该节点的编码,如:A的编码为10,I的编码为001…
这样实现的编码是最短且最优的。
下面将通过代码实现哈夫曼编码
void createHuffmanCode(HuffmanTree HT, HuffmanCode & HC, int n){
HC = (HuffmanCode)malloc(sizeof(HuffmanCode) * n + 1);//申请n + 1个HuffmanCode大小HuffmanCode类型的临时空间
//因为下标是从一开始,和哈夫曼树的结构对应
char* array = (char*)malloc(sizeof(char) * n);//申请n个char大小char类型的临时空间,这个临时数组记录每次遍历出来的编码
int start = 0,child = 0,parent = 0; //start为array数组记录下标,child初始为叶子结点下标,而后就是孩子结点的下标,parent记录双亲结点的下标
array[n - 1] = '\0';
for (int i = 1; i <= n; i++){//只要叶子结点的编码
start = n - 1;
child = i;
parent = HT[child].parent;
while (parent != -1){//根节点没有双亲
start--;
if (HT[parent].lChild == child) //左孩子就是0,右孩子就为1
array[start] = '0';
else
array[start] = '1';
child = parent; parent = HT[child].parent; //向根结点接近
}
HC[i] = (char*)malloc(sizeof(char) * (n - start)); //给数组里的数组申请n - start个char大小的char*类型的临时空间
strcpy(HC[i], &array[start]); //array里记录的编码给HC的第i个数组
}
free(array); //释放临时空间
}
加入main方法进行测试:
int main(){
HuffmanTree HT ;
initHuffmanTree(HT);
HuffmanCode HC;
createHuffmanTree(HT,10);
createHuffmanCode(HT,HC,10);
for (int i = 1; i <= 10; i++){ //遍历输出编码
printf("%c:\t",HT[i].data);
printf("%s\n", HC[i]);
}
return 0;
}
测试结果
然后加入读取文件的功能,就能够对文件中出现的字母进行编码
读取文件功能
加入读取文件的功能,然后统计各个字符出现的次数为权值。
代码实现
//文本文件读文件
void read(string path) { // path为传入文件绝对路径
//创建流对象
ifstream ifs;
//打开文件并且判断是否打开成功
ifs.open(path,ios::in) ;
if (!ifs.is_open()) {
cout << "文件打开失败" << endl;
return;
}
//读数据
char buf[1024];
while (ifs >> buf) {
cout << "读取文件中...";
}
//5、关闭文件
ifs.close();
}
定义一个记录统计字符次数的结构体(使用师兄师姐的代码)
typedef struct Count{ // 统计字符和对应的次数
char ch;
int count = 0;
}Count;
typedef struct NumCount{ // 统计次数的外部封装
Count count [MaxSize] ;
int Length = 0;
}NumCount;
函数本体
void WordCount(char *data,NumCount *paraCnt)
{
int flag;// 标识是否已经记录
int len = strlen(data);
for(int i = 0;i < len;++i)
{
flag = 0;
for(int j = 0;j < paraCnt->length;++j)
{
if(paraCnt->count[j].ch == data[i]) // 若已有记录,直接++
{
++paraCnt->count[j].cnt;
flag = 1;
break;
}
}
if(!flag) // 没有记录,则新增
{
paraCnt->count[paraCnt->length].ch = data[i];
++paraCnt->count[paraCnt->length].cnt;
++paraCnt->length;
}
}
return ;
}
此时可以进行简单的文件编码功能,下面是测试结果:
附上文件内容