哈夫曼编码可以有效的压缩数据,通常可以节省20~90%的空间,具体压缩率依赖于数据的特性。我们将待压缩数据看作字符序列,根据每个字符出现的频率(也可以是字符对应的权重),通过哈夫曼贪心算法构造出字符的最优二进制表示。
这里我们只考虑前缀码,即没有任何码字是其他码字的前缀。前缀码可以保证达到最优的数据压缩率,且不会丧失一般性。但这里不做它做出证明。接下来我们介绍一下Huffman算法
哈夫曼设计了一个贪心算法来构造最优前缀码,被称为Huffman code。它的正确性证明也依赖于贪心选择性质和最优子结构。
首先我们先设计一下存储结构,采用顺序表存储,开辟两个数组,一个用于存储Huffman树的结点,另一个用于存储Huffman树各个数据对应的前缀码,代码如下:
typedef struct huffman_Node // 结点
{
char data;
int weight;
int parent, lchild, rchild;
} Node;
typedef struct huffman_Code // 编码
{
char data;
string code;
} Code;
bool flag[N] = {
false};//标志结点是否已经被加入树中
Node huff_tree[N];//Huffman树
Code huff_code[N];//前缀码数组
int n;//数据个数
接着我们根据输入的数据进行Huffman树的建树过程,这里我们采用顺序表结构进行存储,假设C是一个n个字符的集合,每个字符c∈C,c.weight对应它的出现频率(或者说是它的权重),然后先将n个字符存入Huffman树的数组中,其对应下标为0-n-1。接着,采用自底向上的构造出对应的最优编码的二叉树。从C中元素出发,进行|C|-1次合并操作创建出最终的二叉树。算法主要通过找到当前集合中出现频率(权重)最小的两个对象,将其合并,当合并两个对象时,得到的新对象的频率(权重)为原来两个对象的频率(权重)之和。
代码如下:
int find_min(int m)
/**
* @description: 该函数意在找到森林中未被找到过的
* 且权值为最小的那颗树,采用顺序查找来搜索,最终
* 返回它的下标
* @param {*int m}
* @return {*return id}
*/
{
int mi = INT_MAX, id = -1;
for (int i = 0; i < m; ++i)
{
if (!flag[i] && huff_tree[i].weight < mi)
{
mi = huff_tree[i].weight;
id = i;
}
}
flag[id] = true;
return id;
}
void build()
/**
* @description: 该函数为建立huffman树函数
* 每次从森林中选取权值最小的两颗树,将其合并
* 为一颗新树并加入森林中,然后将这两颗树删除
* 显然每做一次操作减少一颗树,当进行了n-1次
* 就可以得到最终形成的huffm树
*/
{
int i, cnt = 0;
pr("请输入字符个数:\n");
input(n);
pr("请输入字符: \n");
for (i = 0; i < n; ++i)
{
cin >> huff_tree[i].data;
huff_tree[i].weight = (int)huff_tree[i].data;
huff_tree[i].lchild = huff_tree[i].rchild = -1;
}
// 左0右1 左小右大
i = n;
while (1)
{
if (cnt == n - 1)
{
int ii = find_min(i);
huff_tree[ii].parent = -1;
break;
}
int i1 = find_min(i);
int i2 = find_min(i);
huff_tree[i].weight = huff_tree[i1].weight + huff_tree[i2].weight;
huff_tree[i].data = ' ';
huff_tree[i1].parent = huff_tree[i2].parent = i;
huff_tree[i].lchild = i1;
huff_tree[i].rchild = i2;
++i, ++cnt;
}
display_Huffman_Tree(i);
}
void build_code()
/**
* @description: 该函数为对huffm树的数据进行编码
* 由于本程序采用顺序表结构进行存储结点,故而对存储
* huffman树结点的数组进行顺序遍历,显然叶子结点的
* 左右孩子均为空,且由于叶子结点为存储数据的结点
* 便从叶子结点往回遍历,按照左0右1的规则进行编码
* 由于这里采用从叶子遍历到根的思路,最后得出来的
* 编码将是逆序的,这时采用reverse函数将其逆序回来
* 然后存入编码数组中
*/
{
int i = 0;
for (; huff_tree[i].lchild == -1 && huff_tree[i].rchild == -1; ++i) // 左0右1
{
if (i == n)
break;
int j = i;
string s = "";
huff_code[i].data = huff_tree[i].data;
while (huff_tree[j].parent != -1) // 叶到根
{
if (huff_tree[huff_tree[j].parent].lchild == j)
s += '0';
else
s += '1';
j = huff_tree[j].parent;
}
reverse(all(s));
huff_code[i].code = s;
}
display_Huffman_Code(i);
}
上面程序采取通过for循环顺序遍历去反复提取两个频率最低的结点下i1,i2,将他们合并为一个新结点i,代替他们,结点i将i1作为左孩子,i2作为右孩子(顺序是任意的,交换左右孩子会生成一个不一样的编码,但代价完全一样).
这里先分析一下Huffman算法的时间复杂度,显然上面主要采用双重for循环进行贪心,故而时间复杂度为O(n^2);当我们假设采用最小二叉堆实现的最小优先队列Q,去进行操作,显然初始化过程花费了O(n),而在合并数据时进行n-1次操作,且每个堆操作需要O(lgn)的时间赋值,故而时间复杂度为O(nlgn);而当我们将最小二叉堆换成van Emde Boas树时,可以将运行时间减少到O(nlglgn)。
完整代码如下
/*
* @Author: csc
* @Date: 2020-11-27 14:03:01
* @LastEditTime: 2020-12-04 14:37:04
* @LastEditors: Please set LastEditors
* @Description: In User Settings Edit
* @FilePath: \code\data structure\huff.cpp
*/
#include <iostream>
#include <algorithm>
#include <string>
#define pr printf
#define all(x) (x).begin(), (x).end()
#define sz(x) x.size()
const int N = 110;
using namespace std;
typedef struct huffman_Node // 结点
{
char data;
int weight;
int parent, lchild, rchild;
} Node;
typedef struct huffman_Code // 编码
{
char data;
string code;
} Code;
class huffman
{
private:
bool flag[N] = {
false};
Node huff_tree[N];
Code huff_code[N];
int n;
public:
void display_Huffman_Tree(int n)
{
cout << "下标\t"
<< "数据\t"
<< "权重\t"
<< "双亲\t"
<< "左孩子\t"
<< "右孩子" << endl;
for (int i = 0; i < n; i++)
cout << i << "\t" << huff_tree[i].data << "\t" << huff_tree[i].weight << "\t" << huff_tree[i].parent << "\t" << huff_tree[i].lchild << "\t" << huff_tree[i].rchild << endl;
}
void display_Huffman_Code(int n)
{
for (int i = 0; i < n; i++)
cout << huff_code[i].data << ":" << huff_code[i].code << endl;
}
int find_min(int m)
/**
* @description: 该函数意在找到森林中未被找到过的
* 且权值为最小的那颗树,采用顺序查找来搜索,最终
* 返回它的下标
* @param {*int m}
* @return {*return id}
*/
{
int mi = INT_MAX, id = -1;
for (int i = 0; i < m; ++i)
{
if (!flag[i] && huff_tree[i].weight < mi)
{
mi = huff_tree[i].weight;
id = i;
}
}
flag[id] = true;
return id;
}
void build()
/**
* @description: 该函数为建立huffman树函数
* 每次从森林中选取权值最小的两颗树,将其合并
* 为一颗新树并加入森林中,然后将这两颗树删除
* 显然每做一次操作减少一颗树,当进行了n-1次
* 就可以得到最终形成的huffm树
*/
{
int i, cnt = 0;
pr("请输入字符个数:\n");
cin >> n;
pr("请输入字符: \n");
for (i = 0; i < n; ++i)
{
cin >> huff_tree[i].data;
huff_tree[i].weight = (int)huff_tree[i].data;
huff_tree[i].lchild = huff_tree[i].rchild = -1;
}
// 左0右1 左小右大
i = n;
while (1)
{
if (cnt == n - 1)
{
int ii = find_min(i);
huff_tree[ii].parent = -1;
break;
}
int i1 = find_min(i);
int i2 = find_min(i);
huff_tree[i].weight = huff_tree[i1].weight + huff_tree[i2].weight;
huff_tree[i].data = ' ';
huff_tree[i1].parent = huff_tree[i2].parent = i;
huff_tree[i].lchild = i1;
huff_tree[i].rchild = i2;
++i, ++cnt;
}
display_Huffman_Tree(i);
}
void build_code()
/**
* @description: 该函数为对huffm树的数据进行编码
* 由于本程序采用顺序表结构进行存储结点,故而对存储
* huffman树结点的数组进行顺序遍历,显然叶子结点的
* 左右孩子均为空,且由于叶子结点为存储数据的结点
* 便从叶子结点往回遍历,按照左0右1的规则进行编码
* 由于这里采用从叶子遍历到根的思路,最后得出来的
* 编码将是逆序的,这时采用reverse函数将其逆序回来
* 然后存入编码数组中
*/
{
int i = 0;
for (; huff_tree[i].lchild == -1 && huff_tree[i].rchild == -1; ++i) // 左0右1
{
if (i == n)
break;
int j = i;
string s = "";
huff_code[i].data = huff_tree[i].data;
while (huff_tree[j].parent != -1) // 叶到根
{
if (huff_tree[huff_tree[j].parent].lchild == j)
s += '0';
else
s += '1';
j = huff_tree[j].parent;
}
reverse(all(s));
huff_code[i].code = s;
}
display_Huffman_Code(i);
}
void build_Code()
{
cout << "请输入编码字符串:\n";
string CODE;
cin >> CODE;
int length_code = sz(CODE);
pr("编码结果:\n");
for(int i = 0; i < length_code;++i)
{
for(int j = 0; j < n; ++j)
{
if(huff_code[j].data == CODE[i])
{
cout << huff_code[j].code;
break;
}
}
}
pr("\n");
}
void decode()
/**
* @description: 本函数为对01字符串进行解码的函数
* 首先进行预处理,找到根节点的存储下标,然后根据
* 输入的01字符串进行解码,从根节点开始,若为0则往
* 它的左子树走,若为1则往它的右子树走,直至走到
* 叶子结点,然后将数据输出。重新从根节点出发进行解码
*/
{
int root = 0;
while (huff_tree[root].parent != -1)
++root;
string s;
pr("请输入解码字符串:\n");
cin >> s;
pr("解码结果:\n");
int i, j = root, len = sz(s);
for (i = 0; i < len; ++i)
{
if (s[i] == '0')
j = huff_tree[j].lchild;
else
j = huff_tree[j].rchild;
if (huff_tree[j].lchild == -1 && huff_tree[j].rchild == -1)
{
cout << huff_tree[j].data;
j = root;
}
}
pr("\n");
}
};
int main()
{
int _ = 1;
//sf(_);
while (_--)
{
huffman t;
t.build();
t.build_code();
t.build_Code();
t.decode();
}
return 0;
}
这里对上面代码的编码过程和解码过程进行解释。在完成Huffman树的建立后,我们可以根据集合C中的数据连成的数据字符串进行编码,通过顺序遍历数据字符串的每个字符,在前缀码数组中找到其对应的编码,最后输出结果,该部分也可采用从根结点出发,一个字符依次查找对应的编码。而解码过程也是如此,可以根据输入的01字符串,从根节点出发,根据左0右1的原则,进行解码,输出数据。