借鉴《趣学算法》–陈小玉
应用: 数据压缩
核心思想: 权值越大的叶子离根越近。
实现方法: 构建哈夫曼树:每次从数的集合中取出没有双亲且权值最小的两棵树作为左右子树(贪心的思想),构建一棵新树,新树的根节点的权值为其左右孩子结点权值之和,将新数插入到数的集合中,通过n-1次这样的合并,构建成的树即为哈夫曼树。(因为n个点,所以要进行n-1次合并);求哈夫曼编码:约定左分支上的编码为0,右分支上的编码为1,从叶子结点到根节点逆向求出每个字符的哈夫曼编码,从根节点到叶子结点路径上的字符组成的字符串为该叶子结点的哈夫曼编码。
具体步骤:
(1):数据结构要求:
(2):初始化:
构造n结点为n个字符的单结点树(即只有一个树根)集合T = {t1, t2, t3, …, tn},每棵树只有一个带权的根结点,权值为该字符的使用频率。
(3)如果集合T中只剩下一颗树,则哈夫曼编码树构造成功,调到步骤(6)。否则,从集合T中取出没有双亲且权值最小的两棵树ti和tj,将他们合并成一颗新树zk,新树的做孩子为ti,右孩子为tj,zk的权值为ti和tj的权值和。
(4)从集合T中删去ti,tj,加入zk。
(5)重复(3) ~ (5)过程。
(6)约定左分支上的编码为0,右分支上的编码为1,从叶子结点到根节点逆向求出每个字符的哈夫曼编码,从根节点到叶子结点路径上的字符组成的字符串为该叶子结点的哈夫曼编码。
具体图解过程: 推荐看看陈小玉的趣学算法里哈夫曼的图解,很详细。
复杂度分析:
时间复杂度:O(n^2)
每一次合并时候找最大值和次小值的时间复杂度花费是O(n^2),编码和输出编码的时间复杂度花费是O( n ^ 2),对于整个算法的时间复杂度主要花费在这两个地方,所以时间复杂度为O(n^2)
空间复杂度:O(n * MAXBIT)
所需要的存储空间为结点结构体数组与编码结构体数组,哈夫曼数组HuffNode[]中的结点个数为n - 1个,每个包含bit[MAXBIT]和start两个域,所以该算法空间复杂度为O(n * MAXBIT)
代码:
#include
#include
#include
using namespace std;
#define MAXBIT 100
#define MAXVALUE 10000
#define MAXLEAF 30
#define MAXNODE MAXLEAF * 2 - 1
typedef struct{
double w; //权重
int pa; //父节点
int lch; //左孩子节点
int rch; //右孩子节点
char val; //字符
}HNodeType; //结点结构体
typedef struct {
int bit[MAXBIT];
int start;
}HCodeType; //编码结构体
HNodeType HuffNode[MAXNODE]; //定义一个结点结构体
HCodeType HuffCode[MAXLEAF]; //定义一个编码结构体
//构建哈夫曼树
void HuffmanTree(HNodeType HuffNode[], int n){
int x1, x2; //构建哈夫曼树不同过程中两个最小权值结点的序号
double m1, m2; //构建哈夫曼树不同过程中两个最小权值结点的权值
int i, j; //循环变量
//初始化哈夫曼数组HuffNode[]中的结点
for(i = 0; i < 2 * n - 1; i ++) //这里多开n - 1个结点:存放新合并在结点
{
HuffNode[i].w = 0; //初始权重为0
//初始父亲结点 、左右儿子结点都为-1 表示不存在
HuffNode[i].pa = -1;
HuffNode[i].lch = -1;
HuffNode[i].rch = -1;
}
//输入n个叶子结点的权重
for(i = 0; i < n; i ++){
cout<<"please input value and weight of leaf node "<<i + 1<<'\n';
cin>>HuffNode[i].val>>HuffNode[i].w;
}
//构建哈夫曼树
for(i = 0; i < n - 1; i ++)//n个树两两合并,需要n - 1 次
{
//找无父节点中需要合并数中最小权重的两个数, 然后将他们合并成一颗树
m1 = m2 = MAXVALUE; //两个最小权重数的值默认最大, m1:最小值,m2:次小值
x1 = x2 = -1; //两个最小权重数的值的序号为 - 1, x1:最小值序号 x2:次小值编号
//找最小值和次小值
for(j = 0; j < n + i; j ++){
if(HuffNode[j].w < m1 && HuffNode[j].pa == -1){
m2 = m1;
x2 = x1;
m1 = HuffNode[j].w;
x1 = j;
}
else if(HuffNode[j].w < m2 && HuffNode[j].pa == -1){
m2 = HuffNode[j].w;
x2 = j;
}
}
//更新合成该树的两个子节点以及父结点信息
HuffNode[x1].pa = n + i; //子结点的父结点指向新构成的新结点
HuffNode[x2].pa = n + i;
HuffNode[n + i].w = m1 + m2; //新构成结点的权值是两个子结点的权值和
HuffNode[n + i].lch = x1; //新构成的左右孩子结点赋值(左右顺序没有关系)
HuffNode[n + i].rch = x2;
cout<<"x1.weight and x2.weight in round "<<i + 1<< '\t'<<HuffNode[x1].w<<'\t'<<HuffNode[x2].w<<'\n';
}
}
//构建哈夫曼编码
void HuffmanCode(HCodeType HuffCode[], int n){
HCodeType cd; //定义一个临时变量来存放求解编码的信息
int c, p, i, j;
for(i = 0; i < n; i ++) //遍历叶子结点,通过叶子结点到根结点逆向求出每个字符的哈夫曼编码
{
cd.start = n - 1; //从最后向前更新编码值(因为我们构建编码的时候是从叶子到根的顺序,正好与编码顺序相反)
c = i; //c记录当前结点的编号
p = HuffNode[c].pa; //p记录当前结点的父亲结点
while(p != -1){
//约定左分支上编码为0,右分支上编码为1
if(HuffNode[p].lch == c)cd.bit[cd.start] = 0;
else cd.bit[cd.start] = 1;
cd.start --;//cd向前移动一位
c = p; //c更新成c的父结点编号(即模拟一个从叶子结点向根结点走的过程)
p = HuffNode[c].pa; //p也更新成c(此时的c是上次c的父结点)的父亲结点
}
//将构成的哈夫曼编码存入该叶子结点对应的领接表中
for(j = cd.start + 1; j < n; j ++){
HuffCode[i].bit[j] = cd.bit[j];
HuffCode[i].start = cd.start;
}
}
}
int main()
{
int n;
cout<<"please input n: "<<'\n';
cin>>n;
HuffmanTree(HuffNode, n);
HuffmanCode(HuffCode, n);
for(int i = 0; i < n; i ++){
cout<<HuffNode[i].val<<":Huffman code is: ";
//这里start指向相对于编码的首尾要前一位,所以要 + 1
for(int j = HuffCode[i].start + 1; j < n; j ++)cout<<HuffCode[i].bit[j];
cout<<'\n';
}
return 0;
}