一、哈夫曼树的相关的几个名词
1、路径:在一棵树中,一个结点到另一个结点之间的通路,称为路径。如下图根结点到a结点之间的通路就是一条路径。
2、路径长度:在一条路径中,每经过一个结点,路径长度都要加1。如下图根结点到b结点之间所经过的通路数为2,也就是路径长度为2。
3、结点的权:每一个结点都有一个唯一的数值,称为结点的权。如下图结点a的权值为7,b的权值为5。
4、结点的带权路径长度:根结点到结点间的路径长度*结点的权值。如图1结点b的带权路径长度=2*5=10。
5、树的带权路径长度(WPL):树中所有叶子结点的带权路径长度之和。如图1的WPL=1*7+2*5+3*2+3*4=35。
二、什么是哈夫曼树?
在权为w1,w2,...,wn的n各叶子结点的所有二叉树中,树的带权路径长度WPL最小的二叉树称为最优二叉树,也叫赫夫曼树或哈夫曼树。
//哈夫曼树的结点结构
typedef struct {
char ch;///字符结点
int weight;///字符权值
int parent;///父母结点
int lchild;///左孩子在数组位置的下标
int rchild;///右孩子在数组位置的下标
}hnode;
三、如何构建哈夫曼树?
在构建哈夫曼树时,只需遵循一个原则:权值越大的结点离根结点越近。(权值最大就作为根结点)
1、构建哈夫曼树的过程:
(1)在n个结点权值中选出最小和次最小的结点权值,两结点组成一个新的二叉树,其根结点的权值为左右孩子结点的权值和。
(2)在原有的n个结点权值中删除最小和次最小·,将新权值加入到n-2个权值的行列中。
(3)重复(1)、(2),直到所有结点构成一棵二叉树为止,这棵树就是哈夫曼树。
2、构建哈夫曼树:
哈夫曼树表:
结点i | 字符 | 权值 | 左孩子 | 右孩子 | 双亲结点 |
0 | a | 7 | -1 | -1 | 6 |
1 | b | 5 | -1 | -1 | 5 |
2 | c | 2 | -1 | -1 | 4 |
3 | d | 4 | -1 | -1 | 4 |
4 | 6 | 2 | 3 | 5 | |
5 | 11 | 1 | 4 | 6 | |
6 | 18 | 0 | 5 | -1 |
伪代码实现:
void Great_hfmtree(HfmTree ht[], int n){///创建哈夫曼树,n个结点
int m1, m2, x1, x2;
int i, j;
for (i = 0; i<2 * n - 1; i++){///初始化
ht[i].weight = 0;
ht[i].parent = -1;
ht[i].lchild = -1;
ht[i].rchild = -1;
}
cout<<"<>输入字符结点:";
for (int i = 0; i<n; i++)
cin >> ht[i].ch;
cout<<"<>输入各字符结点对应的权值:";
for (i = 0; i<n; i++)
cin >> ht[i].weight;
for (i = 0; i<n - 1; i++){
x1 = x2 = MAXV;
m1 = m2 = 0;
for (j = 0; j<n + i; j++){///n+i为原来结点+新增结点
if (ht[j].parent == -1 && hte[j].weight<x1){///找权值次最小结点
x2 = x1;///记录x1,保证权值最小一定小于于权值次小
x1 = ht[j].weight;
m1 = j;///记录权值次最小结点位置
}
else if (ht[j].parent == -1 && ht[j].weight<x2){///找权值最小结点
x2 = ht[j].weight;
m2 = j;///记录权值最小结点位置
}
}
ht[m1].parent = n + i;
ht[m2].parent = n + i;
ht[n + i].weight = ht[m1].weight + ht[m2].weight;///权值最小结点和权值次最小结点之和
ht[n + i].lchild = m1;
ht[n + i].rchild = m2;
}
}
四、哈夫曼编码
哈夫曼编码:一棵哈夫曼树中,规定哈夫曼树的左分支代表0,右分支代表1,则从根结点到每个叶结点所经过的路径分支组成的0和1序列便为该结点对应字符的编码。如下图
代码实现方法①:
void Reverse(char c[]) //将字符串倒置
{
int k = 0;
char temp;
while (c[k + 1] != '\0')//获取当前字符编码的长度
{
k++;
}
for (int i = 0; i <= k / 2; i++)//交换位置
{
temp = c[i];
c[i] = c[k - i];
c[k - i] = temp;
}
}
void HfmCode(HfmTree ht[], HCode hc[], int n)//从叶子结点到根结点遍历编码
{
hc = new HCode[n];
for (int i = 0; i<n; i++)
{
hc[i].data = ht[i].ch;
int ic = i;
int ip = ht[i].parent;
int k = 0;
while (ip != -1)
{
if (ic == ht[ip].lchild) //左孩子标'0'
hc[i].code[k] = '0';
else
hc[i].code[k] = '1'; //右孩子标'1'
k++;
ic = ip;
ip = ht[ic].parent;
}
hc[i].code[k] = '\0';
Reverse(hc[i].code);
}
for(int i=0;i<n;i++) cout<<"结点"<<hc[i].data<<"的字符编码:"<<hc[i].code<<endl;
}
代码实现方法②:
typedef char **huffmancode;///定义字符存储编码指针
void Haffmancode(HfmTree ht[], int n, huffmancode &hcode){
char *cd = new char[n];///申请内存空间,等同char*cd=(char*)malloc(n*sizeof(char));
hcode = new char *[n + 1];
cd[n - 1] = '\0';
int i, j, c, p;
for (i = 0; i<n; i++){
int start = n;
c = i;
p = ht[c].parent;
while (p != -1){
--start;
if (ht[p].lchild == c) cd[start] = '0';
else cd[start] = '1';
c = p;
p = ht[p].parent;
}
hcode[i] = new char[n - start];///分配内存空间或者建字符数组
strcpy(hcode[i], &cd[start]);///赋予字符串地址,因为编码数组范围在[n-start,n-1].
}
for(int i=0;i<n;i++) cout<<"结点"<<hfmtree[i].ch<<"的字符编码:"<<hcode[i]<<endl;
delete cd;///释放内存空间
}
五、哈夫曼树完整代码的实现
#include<bits/stdc++.h>
using namespace std;
#define MAXV 10000
#define N 10000
typedef struct
{
char ch;
int weight;
int lchild, rchild, parent;
}HfmTree;
typedef struct
{
char data;
char code[100];
}HCode;
void Great_hfmtree(HfmTree ht[], int n){///创建哈夫曼树,n个结点
int m1, m2, x1, x2;
int i, j;
for (i = 0; i<2 * n - 1; i++){///初始化
ht[i].weight = 0;
ht[i].parent = -1;
ht[i].lchild = -1;
ht[i].rchild = -1;
}
char x,y;
cout<<"<>输入各字符结点:";
for (int i = 0; i<n; i++)
cin >> ht[i].ch;
cout<<"<>输入各字符对应的权值:";
for (i = 0; i<n; i++)
cin >> ht[i].weight;
for (i = 0; i<n - 1; i++){
x1 = x2 = MAXV;
m1 = m2 = 0;
for (j = 0; j<n + i; j++){///n+i为
if (ht[j].parent == -1 && ht[j].weight<x1){///找权值最小结点
x2 = x1;///记录x1,保证权值最小一定大于权值次小
x1 = ht[j].weight;
m1 = j;///记录权值最小结点位置
}
else if (ht[j].parent == -1 && ht[j].weight<x2){///找权值次最小结点
x2 = ht[j].weight;
m2 = j;///记录权值次最小结点位置
}
}
ht[m1].parent = n + i;
ht[m2].parent = n + i;
ht[n + i].weight =ht[m1].weight + ht[m2].weight;///权值最小结点和权值次最小结点之和
ht[n + i].lchild = m1;
ht[n + i].rchild = m2;
}
}
void Reverse(char c[]) //将字符串倒置
{
int k = 0;
char temp;
while (c[k + 1] != '\0')
{
k++;
}
for (int i = 0; i <= k / 2; i++)
{
temp = c[i];
c[i] = c[k - i];
c[k - i] = temp;
}
}
void HfmCode(HfmTree ht[], HCode hc[], int n) //输出哈弗曼编码
{
hc = new HCode[n];
for (int i = 0; i<n; i++)
{
hc[i].data = ht[i].ch;
int ic = i;
int ip = ht[i].parent;
int k = 0;
while (ip != -1)
{
if (ic == ht[ip].lchild) //左孩子标'0'
hc[i].code[k] = '0';
else
hc[i].code[k] = '1'; //右孩子标'1'
k++;
ic = ip;
ip = ht[ic].parent;
}
hc[i].code[k] = '\0';
Reverse(hc[i].code);
}
for(int i=0;i<n;i++) cout<<"结点"<<hc[i].data<<"的字符编码:"<<hc[i].code<<endl;
}
void TransCode(HfmTree ht[], HCode hc[], int n, char *s) //哈夫曼译码
{
cout << "解码数据为:";
int i = 2 * (n - 1); //根结点
while (*s != '\0')
{
if (*s == '0')
i = ht[i].lchild;
else
i = ht[i].rchild;
if (ht[i].lchild == -1)
{
cout << ht[i].ch;
i = 2 * n - 2;
}
s++;
}
cout << endl;
}
void menu(){
cout << "\t\t◆◆◆◆◆◆◆◆◆◆◆◆◆◆◆◆◆◆◆◆◆◆◆◆" << endl;
cout << "\t\t* ********哈夫曼编码/译码器******** *" << endl;
cout << "\t\t* 1.创建哈夫曼树; *" << endl;
cout << "\t\t* 2.进行哈夫曼编码; *" << endl;
cout << "\t\t* 3.进行哈夫曼译码; *" << endl;
cout << "\t\t* 4.退出; *" << endl;
cout << "\t\t◆◆◆◆◆◆◆◆◆◆◆◆◆◆◆◆◆◆◆◆◆◆◆◆" << endl;
cout << "\t\t <注意>'+'代表' ','-'代表'!' " << endl;
cout << endl;
}
int main(){
HfmTree ht[N]={};
HCode hc[N];
char s[N];
int n;
char x,y;
menu();
system("color b0");
while (true){
///menu();
int op;
cout << "<>请选择你要进行的功能<1.创建,2.编码,3.译码,4.退出>:";
cin >> op;
switch (op){
case 1:{
cout<<"<>输入结点个数:";
cin>>n;
Great_hfmtree(ht,n);
printf("创建哈夫曼树成功!\n\n");
printf("结点i\t字符\t权值\t左孩子\t右孩子\t双亲结点\n");
for (int i = 0; i<2 * n - 1; i++)
cout << i << "\t" << ht[i].ch << "\t" << ht[i].weight << "\t" << ht[i].lchild << "\t" << ht[i].rchild << "\t" << ht[i].parent << endl;
puts("");
break;
}
case 2: {
HfmCode(ht,hc,n);
puts("");
break;
}
case 3: {
cout << "输入要进行译码的字符串:";
cin >> s;
TransCode(ht, hc, n, s);
cout<<"译码成功!";
//cout << a << endl;
cout << endl;
break;
}
case 4:{
cout << "退出成功!" << endl;
exit(0);
break;
}
default:
break;
}
}
}
/*
颜色属性由两个十六进制数字指定 -- 第一个为背景,第二个则为
前景。每个数字可以为以下任何值之一:
0 = 黑色 8 = 灰色
1 = 蓝色 9 = 淡蓝色
2 = 绿色 A = 淡绿色
3 = 浅绿色 B = 淡浅绿色
4 = 红色 C = 淡红色
5 = 紫色 D = 淡紫色
6 = 黄色 E = 淡黄色
7 = 白色 F = 亮白色
如果没有给定任何参数,该命令会将颜色还原到 CMD.EXE 启动时
的颜色。这个值来自当前控制台窗口、/T 命令行开关或
DefaultColor 注册表值。
如果用相同的前景和背景颜色来执行 COLOR 命令,COLOR 命令
会将 ERRORLEVEL 设置为 1。
例如: "COLOR fc" 在亮白色上产生亮红色
*/
运行结果如下: