赫夫曼编码是最基本的压缩编码方法,在介绍赫夫曼编码之前,我们先介绍一下赫夫曼树。
1.赫夫曼树
在介绍赫夫曼树之前,我们需要知道下面两个概念。
1.1 树的路径长度
赫夫曼大哥曾经说过,从树中一个结点到另一个结点之间的分支构成两个结点的之间的路径,路径上的分支数目就叫作路径长度。树的路径长度就是从树根到每一结点的路径之和。
如上面这颗二叉树。树根到A的路径为1,到B的路径为2。树的路径长度为:1+1+2+2+3+3+4+4=20。
1.2 树的带权路径长度
如果考虑到带权的路径,结点的带权路径长度为该结点到树根之间的路径长度与路径上的权值作乘积。树的带权路径长度(WPL)为树中所有叶子结点的带权路径长度之和。
如1.1那棵树的带权路径长度为:WPL = 5x1+15x2+40x3+30x4+10x4=315
1.3赫夫曼树的构造
赫夫曼:带权路径长度(WPL)最小的树为赫夫曼树。
那么怎样才能让一棵二叉树的WPL最小呢,直观的方法是让权值大的结点靠近根节点,减少路径长度。
下来我们开怎么构造一棵赫夫曼树。
如有这样几个结点:A5,B15,C40,D30,E10
- 先将权值的叶子结点按照从下到大的顺序排列,即:A5,E10,B15,D30,C40
- 取头两个权值小的结点作为一个新结点N1的左右子结点(左小右大),A就是N1的左结点,E就是N1的有结点。新结点的权值为左右子结点的权值之和:N1的权值为5+10=15。
- 用N1替换A和E,插入到序列中,重新排列:N1,B15,D30,C40
- 重复步骤2,3直到序列中只剩一个根节点。
如上图,以T为根节点的树就是一颗赫夫曼树。
2.赫夫曼编码
赫夫曼编码是为了解决数据远距离传输的优化问题(主要是电报)。
比如我们有一段文字“BADCADFEED”要传输给别人,且只能用二进制0和1来表示那么我们如果用下面的编码方式:
那么编码后的数据为:"001000011010000011101100100011",我们发现一共有30个字符。我们假设六个字母出现的频率分别为:
A 27,B 8,C 15,D 15,E 30,F5,我们不妨先将频率作为权值构造一棵赫夫曼树如下图所示:
在上面这个赫夫曼树中,我们将左权值改为0,右权值改为1,如下所示:
那么我们对这六个字母用其树根到叶子结点经过的0或1来编码可得:
那么“BADCADFEED”的编码为“1001010010101001000111100”,共25个字符,比前面的编码方式少了5个字符。
当接收方拿到这串编码后,可对照上面的编码编码表来进行解码,解码方式唯一。
3.代码实现
这次我用了STL库容器来实现赫夫曼树的创建以及编码。我总感觉代码有些冗余,但是思路很是清晰,能大大帮助大家的理解,我们先看一下程序的运行结果:
程序源码如下:
# include<iostream>
using namespace std;
# include<queue>
# include<vector>
#define ELE_TYPE int
# include<string>
typedef struct Node
{
ELE_TYPE data;
string code;
Node* leftChild;
Node* rightChild;
}*HuffManNode;
//函数对象
template<typename T>
class Greater
{
public:
bool operator()(T a, T b)
{
return a->data>b->data;
}
};
//函数完成之后,heap中只存一个结点,为根结点
void CreateTree(priority_queue<HuffManNode, vector<HuffManNode>, Greater<HuffManNode>>*heap)
{
int eleSize = heap->size();
//存储两个最小的结点和新结点
HuffManNode lChild = nullptr;
HuffManNode rChild = nullptr;
HuffManNode newNode = nullptr;
if (heap->empty())
return;
//如果只有一个结点
if (!heap->empty() && heap->size() < 2)
{
lChild = heap->top();
newNode = new Node;
newNode->leftChild = lChild;
heap->push(newNode);
return;
}
//找出最小的两个结点构造新的结点
for (int i = 0; i < eleSize-1; ++i)
{
lChild = heap->top();
heap->pop();
rChild = heap->top();
heap->pop();
newNode = new Node;
newNode->data = lChild->data + rChild->data;
newNode->leftChild = lChild;
newNode->rightChild = rChild;
//将新建的结点重新插入到堆
heap->push(newNode);
}
}
//将huffMan树打印出来
void Print(HuffManNode tree)
{
cout << tree->data;
if (tree->leftChild)
{
cout << "(";
Print(tree->leftChild);
cout << ",";
}
if (tree->rightChild)
{
Print(tree->rightChild);
cout << ")";
}
}
//创建赫夫曼编码
void CreateCode(HuffManNode tree, vector<HuffManNode>* code)
{
if (tree == nullptr)
return;
if (tree->leftChild)
{
tree->leftChild->code = tree->code + "0";
CreateCode(tree->leftChild,code);
}
if (tree->rightChild)
{
tree->rightChild->code = tree->code + "1";
CreateCode(tree->rightChild,code);
}
if (tree->leftChild == nullptr && tree->rightChild == nullptr)
{
code->push_back(tree);
return;
}
}
//回收空间
void Delete(HuffManNode tree)
{
if (tree == nullptr)
return;
Delete(tree->leftChild);
Delete(tree->rightChild);
delete tree;
}
int main()
{
//小根堆来存储输入的结点
//这里heap存的是结点指针,必须用函数对象或者lamda表达式来确定比较方式
priority_queue<HuffManNode, vector<HuffManNode>, Greater<HuffManNode>> heap;
//用来确保没有重复输入
vector<ELE_TYPE> arr;
cout << "请输入需要构造的结点(用空格隔开,按回车结束):" << endl;
int e = 0;
cin >> e;
arr.push_back(e);
while (cin.get()!='\n')
{
//确保没有重复输入
cin >> e;
if (find(arr.begin(), arr.end(), e) == arr.end())
{
arr.push_back(e);
}
}
//将输入的数据存入小根堆
for (auto ele : arr)
{
//构建结点放入小根堆
HuffManNode newNode = new Node;
newNode->data = ele;
newNode->code = "";
newNode->leftChild = newNode->rightChild = nullptr;
heap.push(newNode);
}
CreateTree(&heap);
cout << "创建的HuffMan树的结构如下:" << endl;
HuffManNode tree = heap.top();
Print(tree);
//创建huffMan编码
vector<HuffManNode> code;
CreateCode(tree, &code);
cout << endl;
//查找相应的编码
while (1)
{
cout << "请输入需要查询的元素:";
cin >> e;
//退出
if (e == -1)
break;
if (find(arr.begin(), arr.end(), e) == arr.end())
{
cout << "没有此元素" << endl;
continue;
}
for (auto ele : code)
{
if (e == ele->data)
{
cout << e << "的编码为" << ele->code << endl;
break;
}
}
}
Delete(tree);
return 0;
}