摘要:
认识Huffman码的发展过程,学习Huffman算法思想,探索Huffman算法在数据压缩问题上的运用。
1、背景介绍
随着科技的发展,计算机成为了当今信息时代的产物。计算机的使用难免会存在信息的传输,由于计算机主要以二进制编码来进行交流,因此将信息编码成二进制位串的形式是必须的。如何将信息编码成合适的二进制位串使得数据的传输效率变得更高,对此人们进行了相当长的历史研究,从莫尔斯码到前缀码的最优前缀码,再到Huffman编码。目前对数据的压缩传输主要使用Huffman算法思想。
2、概念与术语
最优前缀码算法的输入、输出
输入: 一个符号集
和每个符号出现的频率 ;
输出: 每个符号集中元素所对应的前缀码,频率高的符号二进制编码长度相对较短,而频率高的符号二进制编码长度相对较长,并且任何二进制编码的前缀不会与其它编码匹配;
前缀码、最优前缀码的定义与例子
前缀码: 是一种编码方式,使得符号集
中每个符号 都有一个独一无二的二进制编码,这个编码不是其它任何一个编码的前缀,用于解决在计算机或其它设备之间传输或储存数据时候,莫尔斯码中因为“暂停”而产生的不确定性问题。
例如:
符号 |
概率 |
前缀码 |
---|---|---|
A | 0.05 | 01 |
B | 0.2 | 10 |
C | 0.25 | 001 |
D | 0.1 | 110 |
E | 0.4 | 111 |
最优前缀码: 是一种前缀码,通过使用最短的二进制编码来表示一个符号集
。尽可能的将出现频率比较高的符号用较短的编码表示,而出现频率比较低的符号用比较长的编码表示,使得出来到平均编码长度 最小。
例如:
符号 |
概率 |
前缀码 |
最优前缀码 |
---|---|---|---|
A | 0.05 | 01 | 1100 |
B | 0.2 | 10 | 111 |
C | 0.25 | 001 | 10 |
D | 0.1 | 110 | 01 |
E | 0.4 | 111 | 0 |
设:
Huffman算法的思想
Huffman算法: 通过Huffman树的建立来获取Huffman编码。
Huffman编码: 是一种前缀码,也是最优前缀码,可以形象的通过Huffman树查找制定符号得到,向左孩子查找一次编码一个0,向右孩子查找一次编码一个1。
Huffman树: 又叫做最优树,是一种带权路径长度最短的树,符号结点都为叶子结点,且权值越大的结点越靠近树根结点。
Huffman树构建方法: 保持符号作为树的叶子结点,每次选取权值最小的结点作为孩子结点生成双亲结点,双亲结点可为两个孩子权值之和,再选择剩下的结点中包含新生成的双亲结点的两个最小权值结点,并重复上述过程。
3、算法设计
生成Huffman树的伪代码:
CreateHuffmanTree(TreeHead,Symbol,Frequence)
create an priority queue Q by CrePriQueue(Symbol,Frequence)
while Q has more than one element
remove the two nodes t1 and t2 with the lowest frequence from Q
insert t1 and t2 into tree by TreeHead
create a new tree node t with t1 and t2 as its left and right children
frequence[t1] + frequence[t2] as its frequence
insert t into Q
return the head node of the tree : TreeHead
生成Huffman编码的伪代码:
CreateHuffmanCode(HuffManTreeHead,Symbol)
SymbolLen = length of Symbol
create a two-dimensional array HC to store the Huffman codes
create a one-dimensional array Code to store temporary Huffman codes
for i=1 to SymbolLen
c = i
f = fatherNode
while fatherNode is not NULL
if Symbol[i] is fatherNode the left child
append "0" to Code
c = node of left child
else
append "1" to Code
c = node of right child
HC[i] = Code
reset the array Code
return HC
4、算法时间复杂度分析
Huffman算法时间复杂度
Huffman编码总时间复杂度: 主要由构建优先队列,优先队列的插入提取,构建Huffman树,求Huffman编码分别对于给定的符号种类数量
的时间复杂度决定,主要为 ;
构建优先队列: 通常使用堆来实现,也可以使用排序来实现,时间复杂度为;
优先队列取出和插入: 优先队列的取出和插入Huffman树的时间复杂度为,
构建Huffman树: 从优先队列中反复取出最小权值的两个结点组成一个新的结点,对于优先队列中的个结点时间复杂度为 ;
求Huffman编码:通过从最小的叶子结点开始回溯到根节点,求出每个叶子结点符号所对应的Huffman编码,时间复杂度为;
算法拓展 :总的来说算法的时间复杂度为,在细节部分的算法时间复杂度仍可继续探讨;由于斐波拉契堆和二项队列在提取和插入最小值时候时间复杂度为 ,在构建优先队列时候能够优化算法,但是对于大多数数据集其性能差异并不显著。
5、算法的性质及其证明
定理4.27:从 构成的 的编码是一个前缀码。
反证法:
定理4.28:与最优前缀码对应的二叉树是满的。
反证法:
命题4.29:假设
与 是 的树叶,使得 ,进一步假设在
对应于最优前缀码的标记中,树叶
用 标记且树叶 用 标记,那么 。
反证法:
定理4.30: 是 的一片树叶。
反证法:
定理4.31:存在一个与树
对应的最优前缀码,其中两个最低频率的字母被指定为树叶,这两片树叶是 中的兄弟。
证明:
定理4.32: 。
证明:
定理4.33:对于给定字母表的 Huffman 码得到任意前缀码的字母平均位数的最小值。
反证法:
6、算法实现与实例应用
以下通过C++来实现Huffman算法,用HT表来表示Huffman树,用HC表来表示Huffman编码表。
#include<iostream>
#include<algorithm>
#include<string.h>
using namespace std;
#define OK 1 //Algorithm implementation success
#define ERROR 0 //Algorithm implementation failure
#define OVERFLOW 2 //overflow
#define Status int //Program state
typedef struct
{
char symb=' '; //Symbol of node
int weight=0; //The weight of the node
int parent,lchild,rchild; //The parent of the node, the left child and the right child
} HTNode,*HuffmanTree; //Dynamically allocated arrays store Huffman trees
typedef char** HuffmanCode; //Dynamically allocated arrays store Huffman encoding tables
//Select two nodes whose parent domain is 0 and whose weight is the smallest, and return the serial numbers s1 and s2 in HT
void Select(HuffmanTree &HT, int n, int &s1, int &s2)
{
//Find the first node whose parent domain is 0 and has the lowest weight
int min;
for (int i = 1; i <= n; i++) //The first parent field of 0 is found, and the subscript is temporarily stored to min
{
if (HT[i].parent == 0)
{
min = i;
break;
}
}
for (int i = 1; i <= n; i++)
{
if (HT[i].parent == 0)
{
if (HT[i].weight < HT[min].weight)
{
min = i;
}
}
}
s1 = min;
//Find the second node with the lowest weight and parent domain 0
for (int i = 1; i <= n; i++) //The first parent field of 0 is found, and the subscript is temporarily stored to min
{
if (HT[i].parent == 0 && i != s1)
{
min = i;
break;
}
}
for (int i = 1; i <= n; i++)
{
if (HT[i].parent == 0 && i != s1)
{
if (HT[i].weight < HT[min].weight)
{
min = i;
}
}
}
s2 = min;
}
Status CreateHuffmanTree(HuffmanTree &HT,int n)
{
//Build the Huffman tree HT
if(n<=1) return OK;
int m=2*n-1; //The number of nodes used by Huffman tree
HT=new HTNode[m+1]; //Create space
if(HT==NULL) return OVERFLOW; //Space allocation failure
for(int i=1; i<=m; ++i) //Initialization tree
{
HT[i].parent=0;
HT[i].lchild=0;
HT[i].rchild=0;
}
cout<<"Please enter"<<n<<"each symbol and its corresponding weight/frequency (e.g., symbol space weight/frequency) :";
for(int i=1; i<=n; ++i)
{
cin>>HT[i].symb>>HT[i].weight; //Input data
}
// for(int i=1;i<=n;++i) cout<<HT[i].symb<<" "<<HT[i].weight;
//The initialization is over and the Huffman tree is built
int s1,s2;//Choose the smallest subscript each time, which is already an ascending sequence
for(int i=n+1;i<=m;++i)
{
Select(HT,i-1,s1,s2); //Builds the priority queue, returning the smallest two elements
HT[s1].parent=i;
HT[s2].parent=i; //Creating parent node succeeded. Procedure
HT[i].lchild=s1;
HT[i].rchild=s2; //Mark the left and right children of both parents
HT[i].weight=HT[s1].weight+HT[s2].weight; //The weight of the new node
}
return OK;
}
Status CreateHuffmanCode(HuffmanTree HT,HuffmanCode &HC,int n)
{//Get the Huffman encoding from the Huffman tree
HC = new char*[n+1]; //Allocate space to store n symbol codes
if(HC==NULL) return OVERFLOW; //Space allocation failure
char* cd = new char[n]; //Temporary storage space for code
cd[n-1]='\0'; //End of code
for(int i=1;i<=n;++i)
{//Find Huffman encoding character by character
int start = n-1;
int c=i;
int f=HT[i].parent;
while(f!=0)
{//I'm going to start coding backwards from the leaf node
--start; //Backtracking height record
if(HT[f].lchild==c) cd[start]='0'; //c is coded as 0 if it is a left child, otherwise it is 1
else cd[start]='1';
c=f; //Switch to judgment f
f=HT[f].parent; //Change criteria
}
HC[i]=new char[n-start]; //New space
if(HC[i]==NULL) return OVERFLOW; //Space creation failure
strcpy(HC[i],&cd[start]); //Record the encoding into the HC table
}
delete cd;
return OK;
}
void display(HuffmanTree HT,HuffmanCode HC,int n)
{//Generate Huffman encoding table
for(int i=1;i<=n;++i)
{
cout<<HT[i].symb<<":";
for(int j=0;HC[i][j]!='\0';++j)
cout<<HC[i][j];
cout<<endl;
}
}
int main()
{
HuffmanTree HT; //Records the header pointer to the Huffman tree
HuffmanCode HC; //Record Huffman coded table header pointer
int n=0; //The number of symbols encoded
cout<<"Please enter the number of symbols to be coded:";
cin>>n;
if(CreateHuffmanTree(HT,n)==1) //Build the Huffman tree
cout<<"Huffman Tree built successfully!"<<endl<<endl;
else
cout<<"Huffman tree build failed!"<<endl<<endl;
for(int i=1;i<=n*2-1;++i)//View HT table
cout<<HT[i].symb<<" "<<HT[i].weight<<" "<<HT[i].parent<<" "<<HT[i].lchild<<" "<<HT[i].rchild<<endl;
cout<<endl;
if(CreateHuffmanCode(HT,HC,n)==1) //Build the Huffman tree
cout<<"Huffman code generation is successful!"<<endl<<endl;
else
cout<<"Huffman code generation failed!"<<endl<<endl;
cout<<"The Huffman code corresponding to the symbol is:"<<endl;
display(HT,HC,n); //Generate Huffman encoding table
return 0;
}
输入: A 5 B 20 C 25 D 10 E 40
输出:
HT表(Huffman树):
结点下标 | 符号 | 权值/频率 | 双亲 | 左孩子 | 右孩子 |
---|---|---|---|---|---|
1 | A | 5 | 6 | 0 | 0 |
2 | B | 20 | 7 | 0 | 0 |
3 | C | 25 | 8 | 0 | 0 |
4 | D | 10 | 6 | 0 | 0 |
5 | E | 40 | 9 | 0 | 0 |
6 | 15 | 7 | 1 | 4 | |
7 | 35 | 8 | 6 | 2 | |
8 | 60 | 9 | 3 | 7 | |
9 | 100 | 0 | 5 | 8 |
HC表(Huffman编码表):
结点下标 | 符号 | Huffman编码 |
---|---|---|
1 | A | 1100 |
2 | B | 111 |
3 | C | 10 |
4 | D | 1101 |
5 | E | 0 |
----以上为个人思考与见解,有误请指点,有想法也可联系交流!