一、概念
哈夫曼树(Huffman tree),又名最优树,指给定n个权值作为n的叶子结点,构造一棵二叉树,若带权路径长度达到最小,称这样的二叉树为最优二叉树,也称为哈夫曼树(Huffman tree)。
二、想法
2.1、节点结构
哈夫曼树的节点中应该存在数据,权重,双亲以及左右孩子,
所以节点构造如下:1
2
3
4
5
6
7
8
9template <class T> 
struct HuffmanNode
{
	T data;					//数据 
	double weight;			//符号出现的频率
	int parent;				//双亲结点的坐标
	int lchild;				//左孩子的坐标
	int rchild;				//右孩子的坐标
};
2.2、哈夫曼树结构及树的构造方法
我们先说,如何去构造一个哈夫曼树,应该是根据权值的不同,每次找到两个小值,把他们两个拼凑起来,其中,权值应该是通过统计得到的,后面会提到方法,这边暂且假设已经得到。1
2
3
4
5
6
7
8template<class T>
class HuffmanTree  
{
    ……………………
private:
	vector<HuffmanNode<T> > huffmantree;	//huffmantree所有节点的存储空间
	int n;			//叶子结点数
};
哈夫曼树,接受一个存有所有叶子节点的向量,根据这些向量,来构造一棵树,我们知道,n个叶子节点,最后会生成2n-1个节点,所以需要先resize树向量。然后把huffmantree里面初值赋完,接下来,我们要找两个权值最小和次小的节点,并用这两个节点信息生成新的节点,填入。1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25template<class T>
HuffmanTree<T>::HuffmanTree(vector<HuffmanNode<T> > &leafs)
{
	n = leafs.size();   //叶子节点个数赋值给n;
	huffmantree.resize(2*n-1);  //为为分支结点预留向量空间
	for (int i=0;i<n;i++)
	{
		huffmantree[i].data=leafs[i].data;
		huffmantree[i].weight=leafs[i].weight;
		huffmantree[i].parent=huffmantree[i].lchild=huffmantree[i].rchild=-1;
	}
	
	int least, less;  //最小数 次小数的下标
	for( i=n; i<2*n-1; i++)
	{
		SelectSmall(least, less, i); //找到最小值 次小值的结点下标
		//由之前的两个最小值生成新结点,
		huffmantree[least].parent = huffmantree[less].parent = i;//原2个节点的父节点置值
		huffmantree[i].data = i;//并没什么作用。。后面输出也会被过滤
		huffmantree[i].parent = -1;
		huffmantree[i].lchild = least;
		huffmantree[i].rchild = less;
		huffmantree[i].weight = huffmantree[least].weight + huffmantree[less].weight;
	}
}
2.3、找最小值以及次小值
下面介绍一下,如何找到最小值和次小值:
首先,只有没有被用过的节点才有比较的资格,也就是父节点为-1的才能参与比较,每轮比较,我们从头开始循环.循环判断过程如下
- 父节点是否为-1
 
- 当当前值比最小值小
 
- 那么先把原先的最小值抛给最小值,并保存次小值序号,
 - 更新最小值及序号。
 
- 当前值大于等于最小值,且小于次小值
 
- 更新次小值及其序号
 
1  | template<class T>  | 
2.4、频率统计
上面的构造函数,是根据所给的含有频率信息的HuffmanNode数组构造数,下面,我来介绍一下,对于ASCII码的字符(非汉字)如何统计。
思路如下,因为ASCII码值,不超过256个,所以,可以建立一个大小为256的int数组,初值置为0,遍历字符串,遇到一个,对应值加1,可以达到简单的统计效果,键值就是0~256,内容就是出现的次数。1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23int frequency[256];//用于记录每个ASCII出现的次数
memset(frequency,0,sizeof(frequency));
for (int i = 0; i < rst.length(); i++)
	frequency[rst[i]]++;	//每出现一次,次数加1
for ( i = 0; i < 256; i++)
{
	if (frequency[i] != 0)
	{
		weight.push_back(frequency[i]*1.0/rst.length()*100);		//当频率不为0的时候,压入权重向量//这个做了个规约处理
		s.push_back(i);											//同时把字符对应的ASCII值存到S向量
	}
}
for ( i = 0; i < s.size(); i++)									//把组织好的值压入向量中,供构造树。
{
	HuffmanNode<char> tmp={ s[i], weight[i], -1, -1, -1 };
	huffnode.push_back(tmp);
}
HuffmanNode<char> *hn=new HuffmanNode<char> [s.size()];
for (i=0;i<s.size();i++)
{
	hn[i]=huffnode[i];
}
这个统计字频的思路很简单,但是对于中文是完全无效的,算是缺陷。这边在频率统计完了之后,顺便构造了Node数组,便于后面构造树。
2.5、生成编码字符串
首先,我们要对这棵哈夫曼树的叶子节点编码,得到每个叶子节点的码值,然后在读取源码,生成对应的01编码。
编码的原理,就是从当前节点向前回溯,如是其父节点的左孩子插入1,相反插0;知道根节点停止。1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17template<class T>
vector<int> HuffmanTree<T>::GetCode(int i)
{
	vector<int> code;
	int parent;
	parent = huffmantree[i].parent;   //先获得父节点的下标找到父节点
	while(parent != -1)  //parent == -1 时,表明已经到了根节点了
	{
		if(i == huffmantree[parent].lchild)
			code.insert(code.begin(), 0);
		else
			code.insert(code.begin(), 1);
		i = parent;  //把父节点换成当前的子节点
		parent = huffmantree[i].parent;  //沿父指针上溯
	}
	return code;
}
编码函数:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17//编码,并把结果存在codes向量里面,并用hash表存储,便于读取
int hash[256];  
memset(hash,-1,sizeof(hash));
vector<string> codes;
vector<int> code;
cout<<endl<<"字符编码如下:"<<endl;
for( i=0; i<HT.GetN(); i++)
{
	char c=HT.GetData(i);		cout<<c<<":   ";
	hash[c]=i;
	code = HT.GetCode(i);
	string tmp;
	for(int j=0; j<code.size(); j++)
		tmp+=(char)(code[j]+48);
	codes.push_back(tmp);
	cout<<tmp<<endl;
}
下面是生成编码字符串1
2
3
4
5
6
7
8
9
10
11string res;
for ( i=0;i<rst.length();i++)
{
	char c=rst.at(i);
	res+=codes[hash[c]];
}
ofstream out;
out.open("code.txt");
out<<res;
out.close();
cout<<endl<<"编译码已经写入code.txt!"<<endl;
2.6、解码
根据01编码进行解码。1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18template<class T>	 
void HuffmanTree<T>::DeCode(string source)
{
	int root = huffmantree.size()-1;   //获得根节点下标
	int p = root;   //p为当前结点的下标
	for(int i=0; i<source.size(); i++)
	{
		if(source.at(i) == '0')
			p = huffmantree[p].lchild;
		else
			p = huffmantree[p].rchild;
		if( (huffmantree[p].lchild == -1) && (huffmantree[p].rchild == -1) )  //如果到了叶子节点 
		{
			cout<<huffmantree[p].data;
			p = root;   //当前结点再次是根结点
		}
	}
}
1  | //下面是译码  | 
2.7、打印
为了方便查看哈夫曼树的结构,设计了这个方法:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22template<class T>
void HuffmanTree<T>::Print()
{
	cout<<"编号"<<"\t"<<"符号"<<"\t"<<"频率"<<"\t"<<"父结点"<<"\t"<<"左孩子"<<"\t"<<"右孩子"<<endl;
	for(int i=0; i<2*n-1; i++)
	{
		cout<<i<<"\t";
		if (i<n)
		{	
			cout<<huffmantree[i].data<<"\t";
		}
		else
		{
			cout<<"\t";
		}
		cout<<setprecision(4)<<huffmantree[i].weight<<"%"<<"\t";
		cout<<huffmantree[i].parent<<"\t";
		cout<<huffmantree[i].lchild<<"\t";
		cout<<huffmantree[i].rchild<<"\t";
		cout<<endl;
	}
}
2.8、保存哈夫曼树
为了讲编码翻译成源码,需要有哈夫曼树的存在,本来可以用对象的序列化存为二进制文件更加安全的,这边还是用文件的写保存。1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25template<class T>
void HuffmanTree<T>::Save(char *fname)
{
	ofstream out;
	out.open(fname);
	out<<n<<endl;
	for(int i=0; i<2*n-1; i++)
	{
		if (i<n)
		{	
			out<<huffmantree[i].data<<"\t";
		}
		else
		{
			out<<0<<"\t";
		}
		out<<setprecision(4)<<huffmantree[i].weight<<"\t";
		out<<huffmantree[i].parent<<"\t";
		out<<huffmantree[i].lchild<<"\t";
		out<<huffmantree[i].rchild<<"\t";
		out<<endl;
	}
	out.close();
	cout<<endl<<"该树已经写入"<<fname<<endl;
}
2.9、还原哈夫曼树
也就是按照文件读入来构造哈夫曼树,实质上是构造函数,可能对于后于需要的其他功能提供方便。1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22template<class T>
HuffmanTree<T>::HuffmanTree(char *fname)
{
	ifstream in;
	in.open(fname);
	in>>n;   //叶子节点个数赋值给n;
	huffmantree.resize(2*n-1);  //为为分支结点预留向量空间
	for(int i=0; i<2*n-1; i++)
	{
		char c;
		in>>c;
		if (c!='0')
		{
			huffmantree[i].data = c;
		}
		in>>huffmantree[i].weight;
		in>>huffmantree[i].parent;
		in>>huffmantree[i].lchild;
		in>>huffmantree[i].rchild;
	}
}
三、测试函数
1  | int main()  | 
测试一:提供源码文件,统计频率,输出编码,并译码。
1  | HuffmanTree<char> ht("MyTree.txt");  | 
测试二:提供哈夫曼树,提供编码,译码(有误?)
这边空格被误识别,原因未知。