哈夫曼树
哈夫曼编码简介
- 哈夫曼树是哈夫曼编码的基础,它在我们的生活中有着广泛的应用,其中最典型的就是文件压缩。
- 为什么利用哈夫曼编码可以起到压缩文件的效果呢?
这就涉及到哈夫曼编码的原理了,我们首先来看一些基本概念。
- 咱们来看个实例,直观感受一下等长编码与不等长编码的区别。
我们可以看出
- 不等长编码,就是文件压缩的核心之一。
- 在存储编码的时候可以采用字符编码与bit编码(二进制文件)两种方式,其中bit编码更节省空间。
- 在编码过程中,为了避免产生二义性,所有字符对应的编码都不可以是其他字符的前缀。
哈夫曼树
简介
哈夫曼树是一种用来进行哈夫曼编码的方法。
我们知道,对于同一组叶结点,使用不同结构的二叉树,得到的带权路径长度是不同的。如下图:
哈夫曼树的构造
- 问题:若一组叶结点的权值已知,如何构造哈夫曼树?(即构造带权路径最短的二叉树)
思路: 让权值越大的叶结点越靠近根节点,权值越小就越原离根节点。
方法:(从下到上构造)
- 选择有效权值最小的两个,构成最小二叉树,标记这两个权值已使用。
- 将这两个权值相加,其和并入权值表,返回步骤1
当权值表中的所有值都使用过,结束循环。
利用哈夫曼树进行编码
哈夫曼树的实现
哈夫曼树的存储和建立
- 哈夫曼树的特点:结点的度只有2和0,所以有n个叶结点的哈夫曼树总结点数一定是 2*n-1.
- 顺序存储结构(设置一个huffTree[2*n-1]数组)
struct HNode
{
int weight;//权值
int lchild,rchild,parent;
};
哈夫曼编码表的存储和建立
struct HCode
{
char data;
string code;
};
哈夫曼编码
哈夫曼解码
代码
程序框架
- Initial():对输入的字符串进行词频统计,并据此初始化数据成员。
- DouMin():求出可使用的树节点中,权值最小的两个。
- Reserve():实现编码的逆置(因为创建编码表时是从叶结点向上遍历的)
- Huffman():创建Huffman树并打印表格,表格表示Huffman树的建立过程。
- CreateCodeTable():创建并打印编码表。
- Encode():编码字符串,并打印结果。
- Decode():是Encode()函数的逆过程,对其结果进行解码。
- Analyze():自动分析Huffman编码的效果
- ~Huffman():析构函数,释放堆空间。
声明
//Huffman编码类
class Huffman
{
private:
TNode *node;//树结点
CNode *codeTable;//存储编码表
int length;//字符串长度
int N; //叶结点数量=不同字符种类
string str;//存储编码后的字符串
int en_length;//存储编码后字符串长度
char name[128];//将string转化为字符数组并存储
void Initial();//对输入的字符串进行频次统计,初始化相应的参数
void DouMin(int &x, int &y,int a,int b);//取权值最小的两个结点
void Reverse(string &s);//逆置编码
public:
Huffman();//创建哈夫曼树并打印
void CreateCodeTable();//创建并打印编码表
void Encode();//编码,并打印结果
void Decode();//解码,并打印结果
void Analyze();//分析Huffman编码的效果
~Huffman();//析构,释放堆空间
};
构造哈夫曼树
基本思想:
- 利用不同符号的ASCII码作为索引来进行词频的统计。根据统计结果对数据成员进行堆空间的申请和初始化赋值。
//对输入的字符串进行频次统计,初始化相应的参数
void Huffman::Initial()
{//时间复杂度:O(n)
length = 0;
int nNum[256] = { 0 };
int ch = cin.get();
//初始化length和name[]
while ((ch != '\r') && (ch != '\n'))
{
nNum[ch]++;//利用ACSII码统计频次
name[length] = (char)ch;//存储字符串中每个字符
length++;
ch = cin.get();//读取下一个字符
}
//初始化N--字符种类
N = 0;
for (int i = 0; i < 256; i++)
{
if (nNum[i] != 0)N++;
}
//初始化树结点和编码表结点
node = new TNode[2 * N - 1];//构建Huffman树结点
codeTable = new CNode[N];//创建编码表结点
int i = 0;
for (int j = 0; j < 256; j++)
{
if (nNum[j] != 0)
{
node[i].weight = nNum[j];
node[i].lchild = -1;//初始化结点,标记为未使用
node[i].rchild = -1;
node[i].parent = -1;
codeTable[i].data = (char)j;
codeTable[i].code = "";
i++;
}
}
}
//取权值最小的两个结点
void Huffman::DouMin(int &x,int &y,int a,int b)
{//时间复杂度:O(n)
struct tempNode
{
int weight;//权重
int position;//位置
};
tempNode *temp=new tempNode[b];
int j = 0;
for (int i = a; i < b; i++)
{//选出所有可使用的结点并用temp[]存储
if (node[i].parent == -1)
{
temp[j].weight = node[i].weight;
temp[j].position = i;
j++;
}
}
int m1 = 0 , m2 = 0;
//求第一个最小值
for (int i = 0; i < j; i++)
if (temp[i].weight < temp[m1].weight)
m1 = i;
//求第二个最小值
for (int i = 0; i < j; i++)
{
if (m1 == m2)
m2++;
if (temp[i].weight <= temp[m2].weight)//注意这里要取等号
m2 = i;
}
x = temp[m1].position;
y = temp[m2].position;
delete[]temp;
}
//创建哈夫曼树并打印
Huffman::Huffman()
{//时间复杂度:O(n)
Initial();//初始化哈夫曼树相关数据成员
if (N <= 1)//异常处理
cout<<"Error!Cannot create a Huffman tree!";
int x=0, y=0;
for (int i = N; i < 2 * N - 1; i++)
{//创建Huffman树(静态)
DouMin(x, y,0,i);//选出权值最小的两个结点,且x<y
node[x].parent = i;
node[y].parent = i;
node[i].weight = node[x].weight + node[y].weight;
node[i].lchild = x;
node[i].rchild = y;
node[i].parent = -1;
}
cout << "Huffman tree: " << endl;
//打印Huffman树的静态表格
for (int i = 0; i < 2 * N - 1; i++)
{
cout << "-----------------------" << endl;
cout << setiosflags(ios::left) << setw(3) << i << "│" << " " << setw(3) << node[i].weight
<< "│" << " " << setw(3) << node[i].lchild << "│" << " " << setw(3) << node[i].rchild
<< "│" << " " << setw(3) << node[i].parent << endl;
}
cout << "-----------------------" << endl;
}
构造哈夫曼编码表
基本思想:
从叶结点开始依次向上得到编码,再通过函数将其逆置,从而得到正确的编码。最后打印编码表。
//逆置编码
void Huffman::Reverse(string &s)
{//时间复杂度:O(n)
int length = s.size();
char temp;
for (int i = 0; i < length / 2; i++)
{
temp = s[i];
s[i] = s[length - i - 1];
s[length - i - 1] = temp;
}
}
//创建并打印编码表
void Huffman::CreateCodeTable()
{//时间复杂度:O(n^2)
cout << "The code table is: " << endl;
for (int i = 0; i < N; i++)
{
int child = i;//从叶结点开始编码,再逆置编码
int parent = node[i].parent;
int temp = 0;
while (node[child].parent != -1)//判断是否为根结点
{
if (child == node[parent].lchild)//左孩子,编码取0
codeTable[i].code += '0';
else if(child == node[parent].rchild)//右孩子,编码取1
codeTable[i].code += '1';
temp = child;
child = parent;
parent = node[temp].parent;
}
Reverse(codeTable[i].code);//逆置
//打印编码表
cout << "------------" << endl;
cout << setiosflags(ios::left) << ' ' << setw(4) << codeTable[i].data
<< "│" << ' ' << setw(4) << codeTable[i].code << endl;
}
cout << "------------" << endl;
}
编码
基本思想:
在初始化函数中已经将输入的字符串存储在name[ ]数组中。遍历name[ ]数组,根据编码表中字符与字符编码的对应关系,生成Huffman编码。
//编码,并打印结果
void Huffman::Encode()
{//时间复杂度:O(n)
string en_str;//编码结果
for (int i = 0; i < length; i++)
{
for(int j = 0; j < N; j++)
{
if (name[i] == codeTable[j].data)
{
en_str += codeTable[j].code;
}
}
}
str = en_str;
cout << en_str << endl;
en_length = en_str.length();
}
解码
基本思想:
Encode()函数的逆过程。遍历字符串str,根据编码表中字符编码与字符的对应关系,求出原始的字符串。
void Huffman::Decode()
{//时间复杂度:O(n)
string de_str;
int x = str.length();
for (int i = 0; i < x; i++)
{
int parent = 2 * N - 2;//根节点在静态哈夫曼树表中的索引
int j = 0;
while (node[parent].lchild != -1)//不是叶结点
{
if (str[i + j] == '0')
parent = node[parent].lchild;
else
parent = node[parent].rchild;
j++;
}
i += j - 1;
de_str += codeTable[parent].data;
}
cout << de_str << endl;
}
析构
//析构,释放堆空间
Huffman::~Huffman()
{//时间复杂度:O(1)
delete[]node;
delete[]codeTable;
}
完整代码
你也可以直接在我的Github下载
头文件
#pragma
#include<iostream>
#include<string>
#include<iomanip>
using namespace std;
//树结点
struct TNode
{
int weight;//权值
int lchild, rchild, parent;
};
//编码表结点
struct CNode
{
char data;//被编码内容
string code;//编码
};
//Huffman编码类
class Huffman
{
private:
TNode *node;//树结点
CNode *codeTable;//存储编码表
int length;//字符串长度
int N; //叶结点数量=不同字符种类
string str;//存储编码后的字符串
int en_length;//存储编码后字符串长度
char name[128];//将string转化为字符数组并存储
void Initial();//对输入的字符串进行频次统计,初始化相应的参数
void DouMin(int &x, int &y,int a,int b);//取权值最小的两个结点
void Reverse(string &s);//逆置编码
public:
Huffman();//创建哈夫曼树并打印
void CreateCodeTable();//创建并打印编码表
void Encode();//编码,并打印结果
void Decode();//解码,并打印结果
void Analyze();//分析Huffman编码的效果
~Huffman();//析构,释放堆空间
};
//对输入的字符串进行频次统计,初始化相应的参数
void Huffman::Initial()
{//时间复杂度:O(n)
length = 0;
int nNum[256] = { 0 };
int ch = cin.get();
//初始化length和name[]
while ((ch != '\r') && (ch != '\n'))
{
nNum[ch]++;//利用ACSII码统计频次
name[length] = (char)ch;//存储字符串中每个字符
length++;
ch = cin.get();//读取下一个字符
}
//初始化N--字符种类
N = 0;
for (int i = 0; i < 256; i++)
{
if (nNum[i] != 0)N++;
}
//初始化树结点和编码表结点
node = new TNode[2 * N - 1];//构建Huffman树结点
codeTable = new CNode[N];//创建编码表结点
int i = 0;
for (int j = 0; j < 256; j++)
{
if (nNum[j] != 0)
{
node[i].weight = nNum[j];
node[i].lchild = -1;//初始化结点,标记为未使用
node[i].rchild = -1;
node[i].parent = -1;
codeTable[i].data = (char)j;
codeTable[i].code = "";
i++;
}
}
}
//取权值最小的两个结点
void Huffman::DouMin(int &x,int &y,int a,int b)
{//时间复杂度:O(n)
struct tempNode
{
int weight;//权重
int position;//位置
};
tempNode *temp=new tempNode[b];
int j = 0;
for (int i = a; i < b; i++)
{//选出所有可使用的结点并用temp[]存储
if (node[i].parent == -1)
{
temp[j].weight = node[i].weight;
temp[j].position = i;
j++;
}
}
int m1 = 0 , m2 = 0;
//求第一个最小值
for (int i = 0; i < j; i++)
if (temp[i].weight < temp[m1].weight)
m1 = i;
//求第二个最小值
for (int i = 0; i < j; i++)
{
if (m1 == m2)
m2++;
if (temp[i].weight <= temp[m2].weight)//注意这里要取等号
m2 = i;
}
x = temp[m1].position;
y = temp[m2].position;
delete[]temp;
}
//创建哈夫曼树并打印
Huffman::Huffman()
{//时间复杂度:O(n)
Initial();//初始化哈夫曼树相关数据成员
if (N <= 1)//异常处理
cout<<"Error!Cannot create a Huffman tree!";
int x=0, y=0;
for (int i = N; i < 2 * N - 1; i++)
{//创建Huffman树(静态)
DouMin(x, y,0,i);//选出权值最小的两个结点,且x<y
node[x].parent = i;
node[y].parent = i;
node[i].weight = node[x].weight + node[y].weight;
node[i].lchild = x;
node[i].rchild = y;
node[i].parent = -1;
}
cout << "Huffman tree: " << endl;
//打印Huffman树的静态表格
for (int i = 0; i < 2 * N - 1; i++)
{
cout << "-----------------------" << endl;
cout << setiosflags(ios::left) << setw(3) << i << "│" << " " << setw(3) << node[i].weight
<< "│" << " " << setw(3) << node[i].lchild << "│" << " " << setw(3) << node[i].rchild
<< "│" << " " << setw(3) << node[i].parent << endl;
}
cout << "-----------------------" << endl;
}
//逆置编码
void Huffman::Reverse(string &s)
{//时间复杂度:O(n)
int length = s.size();
char temp;
for (int i = 0; i < length / 2; i++)
{
temp = s[i];
s[i] = s[length - i - 1];
s[length - i - 1] = temp;
}
}
//创建并打印编码表
void Huffman::CreateCodeTable()
{//时间复杂度:O(n^2)
cout << "The code table is: " << endl;
for (int i = 0; i < N; i++)
{
int child = i;//从叶结点开始编码,再逆置编码
int parent = node[i].parent;
int temp = 0;
while (node[child].parent != -1)//判断是否为根结点
{
if (child == node[parent].lchild)//左孩子,编码取0
codeTable[i].code += '0';
else if(child == node[parent].rchild)//右孩子,编码取1
codeTable[i].code += '1';
temp = child;
child = parent;
parent = node[temp].parent;
}
Reverse(codeTable[i].code);//逆置
//打印编码表
cout << "------------" << endl;
cout << setiosflags(ios::left) << ' ' << setw(4) << codeTable[i].data
<< "│" << ' ' << setw(4) << codeTable[i].code << endl;
}
cout << "------------" << endl;
}
//编码,并打印结果
void Huffman::Encode()
{//时间复杂度:O(n)
string en_str;//编码结果
for (int i = 0; i < length; i++)
{
for(int j = 0; j < N; j++)
{
if (name[i] == codeTable[j].data)
{
en_str += codeTable[j].code;
}
}
}
str = en_str;
cout << en_str << endl;
en_length = en_str.length();
}
//解码,并打印结果
void Huffman::Decode()
{//时间复杂度:O(n)
string de_str;
int x = str.length();
for (int i = 0; i < x; i++)
{
int parent = 2 * N - 2;//根节点在静态哈夫曼树表中的索引
int j = 0;
while (node[parent].lchild != -1)//不是叶结点
{
if (str[i + j] == '0')
parent = node[parent].lchild;
else
parent = node[parent].rchild;
j++;
}
i += j - 1;
de_str += codeTable[parent].data;
}
cout << de_str << endl;
}
//分析Huffman编码的效果
void Huffman::Analyze()
{
cout << "编码前字符串所占内存(单位:bit):" << length*8<<endl;
cout << "编码后字符串所占内存(不采用二进制编码,单位:bit):" << en_length*8<<endl;
cout << "编码后字符串所占内存(假设采用二进制编码,单位:bit):" << en_length<<endl;
cout << "结论:只有采用二进制编码才有压缩效果。" << endl;
}
//析构,释放堆空间
Huffman::~Huffman()
{//时间复杂度:O(1)
delete[]node;
delete[]codeTable;
}
main函数
#include"Huffman.h"
using namespace std;
int main()
{
cout << "请输入待编码字符:";
Huffman huff;
huff.CreateCodeTable();
cout << "编码结果:" ;
huff.Encode();
cout << "解码结果:";
huff.Decode();
cout << endl;
huff.Analyze();
cout << endl;
}
不足与反思
- 没有真正实现文件的压缩,只是从形式上体现了Huffman的原理和方法。迭代方向:使用二进制进行编码。
- 交互性不强,无法实现编码的传送。迭代方向:利用文件读写,将编码表与编码都写入文件,再将此文件发送给有解码程序的用户,即可实现交互。
关于上述两个问题,我会尽量抽时间来解决。如果你有好的想法,欢迎分享~
希望这篇文章可以帮助你理解Huffman编码,如果有任何疑问或者建议,欢迎评论或私聊我。
一起学习,一起进步~