哈夫曼树的创建
Step1:创建一个数组,存放所有的结点。结点的个数是leafNum2-1
Step2:将叶子结点存放到数组中,对叶子结点进行初始化 parent,left,right设为未构建 权重按照weightArray数组赋值
Step3:对数组中其它的结点也进行初始化 parent,left,right设为未构建 权重按照weightArray=0
Step4:依次求子树结点存入数组 循环本轮要填的结点的位置k从leafNum开始,到leafNum2-1结束
求本轮最小和次小权值的下标
minIndex1和minIndex2两个结点构建子树
minIndex1和minIndex2的parent设为i
第i个结点的权重为两个结点之和
第i个结点的left为minIndex1
第i个结点的right为minIndex2
参考函数头
void createHuffmanTree(HTNode * HTArray,int * weightArray,int leafNum)
【注意】 当前结点比最小权重小,标记它为最小权重。 当前结点比最小权重大,比次小权重小,标记为次小权重。 不要写成大于等于,或者小于等于,逻辑上可以,但是后续哈夫曼编码的某些测试数据可能不能通过。
【输入输出】
第一行输入叶子节点的个数。
第二行输入各个叶子节点的权值。
输出哈夫曼树的数组,直接调用打印函数printHufTree实现。
【测试数据】
输入: 4 2 4 5 3
输出:
0 weight= 2 parent= 4 leftChild=-1 rightChild=-1
1 weight= 4 parent= 5 leftChild=-1 rightChild=-1
2 weight= 5 parent= 5 leftChild=-1 rightChild=-1
3 weight= 3 parent= 4 leftChild=-1 rightChild=-1
4 weight= 5 parent= 6 leftChild= 0 rightChild= 3
5 weight= 9 parent= 6 leftChild= 1 rightChild= 2
6 weight=14 parent=-1 leftChild= 4 rightChild= 5
【题解代码】
#include<iostream>
#include<string.h>
#include <iomanip>
#define UNCONSTRUCTED -1 //表示结点没有构建树
#define MAX 20
using namespace std;
/**哈夫曼树结点结构*/
struct HTNode
{
int weight;//结点权重
int parent, left, right;//父结点、左孩子、右孩子在数组中的位置下标
};
/**选择最小和次小结点*/
void selectMin(HTNode * HTArray,int k,int & minIndex1,int & minIndex2);
/**创建哈夫曼树*/
void createHuffmanTree(HTNode * HTArray,int * weightArray,int leafNum);
/**打印哈夫曼树*/
void printHufTree(HTNode * treeArray,int leafNum);
/**创建哈夫曼树
* @param HTArray为地址传递的存储哈夫曼树的数组
* @param weightArray为存储结点权重值的数组
* @param leafNum为结点个数
算法
创建一个数组,存放所有的结点。结点的个数是leafNum*2-1
将叶子结点存放到数组中,对叶子结点进行初始化
parent,left,right设为未构建
权重按照weightArray数组赋值
对数组中其它的结点也进行初始化
parent,left,right设为未构建
权重按照weightArray=0
依次求子树结点存入数组
循环本轮要填的结点的位置k位置从leafNum开始,到leafNum*2-1结束
求最小和次小权值的下标
minIndex1和minIndex2两个结点构建子树
minIndex1和minIndex2的parent设为i
第i个结点的权重为两个结点之和
第i个结点的left为minIndex1
第i个结点的right为minIndex2
*/
void createHuffmanTree(HTNode * HTArray,int * weightArray,int leafNum)
{
//哈夫曼数组的长度
int length=leafNum*2-1;
//叶子结点初始化
for(int i=0;i<leafNum;i++)
{
HTArray[i].parent=UNCONSTRUCTED;
HTArray[i].left=UNCONSTRUCTED;
HTArray[i].right=UNCONSTRUCTED;
HTArray[i].weight=weightArray[i];
}
//非叶子结点初始化
for(int i=leafNum;i<length;i++)
{
HTArray[i].parent=UNCONSTRUCTED;
HTArray[i].left=UNCONSTRUCTED;
HTArray[i].right=UNCONSTRUCTED;
HTArray[i].weight=0;//权值为0
}
//构建哈夫曼树
//k为当前要写的下标,从第一个非叶子结点开始
for(int k=leafNum;k<length;k++)
{
int minIndex1,minIndex2;//两个最小值的下标
selectMin(HTArray,k,minIndex1,minIndex2);//求最小和次小权值的下标
//cout<<"\n本轮写的结点下标k="<<k<<" minIndex1="<<minIndex1<<" minIndex2="<<minIndex2<<endl;
//minIndex1和minIndex2两个结点构建子树
HTArray[k].weight=HTArray[minIndex1].weight+HTArray[minIndex2].weight;
HTArray[k].left=minIndex1;
HTArray[k].right=minIndex2;
HTArray[minIndex1].parent=k;
HTArray[minIndex2].parent=k;
//cout<<"最小下标"<<minIndex1<<"次小下标"<<minIndex2<<endl;
//printHufTree(HTArray,leafNum);//打印中间结果
}
return ;
}
/**
* 从给定的哈希表数组中选择两个最小权重的元素的索引。
*
* @param HTArray 指向哈希表数组的指针。
* @param k 要检查的哈希表数组的大小。
* @param minIndex1 用于存储第一个最小权重元素的索引的引用。
* @param minIndex2 用于存储第二个最小权重元素的索引的引用。
*/
void selectMin(HTNode * HTArray, int k, int & minIndex1, int & minIndex2)
{
int i = 0; // 初始化循环变量i为0
minIndex1 = -1; // 初始化第一个最小权重元素的索引为-1,表示未找到
minIndex2 = -1; // 初始化第二个最小权重元素的索引为-1,表示未找到
while (i < k) // 当i小于k时继续循环
{
if (HTArray[i].parent == UNCONSTRUCTED) // 如果当前元素未被构造
{
if (minIndex1 == -1 || HTArray[i].weight < HTArray[minIndex1].weight) // 如果当前元素权重小于已找到的第一个最小权重元素
{
minIndex2 = minIndex1; // 将第二个最小权重元素的索引更新为第一个最小权重元素的索引
minIndex1 = i; // 更新第一个最小权重元素的索引为当前元素的索引
}
else if (minIndex2 == -1 || HTArray[i].weight < HTArray[minIndex2].weight) // 如果当前元素权重小于已找到的第二个最小权重元素
{
minIndex2 = i; // 更新第二个最小权重元素的索引为当前元素的索引
}
}
i++; // 增加循环变量i的值
}
}
/**
打印哈夫曼树
* @param HTNode * treeArray 哈夫曼树的数组
* @param int leafNum 叶子个数
*/
void printHufTree(HTNode * treeArray,int leafNum)
{
//cout<<"\n哈夫曼树:\n";
for(int i=0;i<leafNum*2-1;i++)
{
cout<<setw(2)<< i
<<" weight="<<setw(2)<<treeArray[i].weight
<<" parent="<<setw(2)<<treeArray[i].parent
<<" leftChild="<<setw(2)<<treeArray[i].left
<<" rightChild="<<setw(2)<<treeArray[i].right
<<endl;
}
cout<<"----------------------\n";
return;
}
int main()
{
//叶子结点个数
int leafNum;
int i;
int w[MAX]={0};
//权值数组和字符数组
cin>>leafNum;
for(i=0;i<leafNum;i++)
{
cin>>w[i];
}
//哈夫曼树数组
HTNode * HTArray=new HTNode[leafNum*2-1];
//创建哈夫曼树
createHuffmanTree(HTArray,w,leafNum);
//打印哈夫曼树
printHufTree(HTArray,leafNum);
return 0;
}
哈夫曼编码
【哈夫曼编码】 是在哈夫曼树的基础上构建的,这种编码方式最大的优点就是用最少的字符包含最多的信息内容。
根据发送信息的内容,通过统计文本中相同字符的个数作为每个字符的权值,建立哈夫曼树。
对于树中的每一个子树,统一规定其左孩子标记为 0 ,右孩子标记为 1 。
用到哪个字符时,从哈夫曼树的根结点开始,依次写出经过结点的标记,最终得到的就是该结点的哈夫曼编码。
结点权值越高,在哈夫曼树中的体现就是越接近树根,编码的长度越短。
【哈夫曼编码方案】
从叶子到根逆向求每个字符的哈夫曼编码 算法
Step1:创建一个暂存字符串,来保存哈夫曼编码
Step2:循环,依次访问每一个叶子结点
如果结点还有双亲就进行循环 找到结点的双亲
如果它是双亲的左儿子,字符串标0
如果它是双亲的右儿子,字符串标1
访问下标进行移动,指向结点的双亲
当前叶子结点追溯到了根结点,将暂存的字符串导入结果字符串数组
【输入输出】
第一行输入结点的个数n,n小于100。
第二行输入n个结点的权值
第三行输入n个结点的内容,为n个字符
输出n行,为n个结点的哈夫曼编码。
【测试数据】
输入
7
9 11 5 7 8 2 3
A B C D E F G
输出
A:00
B:10
C:010
D:110
E:111
F:0110
G:0111
【题解代码】
#include<iostream>
#include<string.h>
#include <iomanip>
#define UNCONSTRUCTED -1 //表示结点没有构建树
#define MAX 20
using namespace std;
/**哈夫曼树结点结构*/
struct HTNode
{
int weight;//结点权重
int parent, left, right;//父结点、左孩子、右孩子在数组中的位置下标
};
/**选择最小和次小结点*/
void selectMin(HTNode* HTArray, int k, int& minIndex1, int& minIndex2);
/**创建哈夫曼树*/
void createHuffmanTree(HTNode* HTArray, int* weightArray, int leafNum);
/**打印哈夫曼树*/
//void printHufTree(HTNode* treeArray, int leafNum);
/**创建哈夫曼树
* @param HTArray为地址传递的存储哈夫曼树的数组
* @param weightArray为存储结点权重值的数组
* @param leafNum为结点个数
算法
创建一个数组,存放所有的结点。结点的个数是leafNum*2-1
将叶子结点存放到数组中,对叶子结点进行初始化
parent,left,right设为未构建
权重按照weightArray数组赋值
对数组中其它的结点也进行初始化
parent,left,right设为未构建
权重按照weightArray=0
依次求子树结点存入数组
循环本轮要填的结点的位置k位置从leafNum开始,到leafNum*2-1结束
求最小和次小权值的下标
minIndex1和minIndex2两个结点构建子树
minIndex1和minIndex2的parent设为i
第i个结点的权重为两个结点之和
第i个结点的left为minIndex1
第i个结点的right为minIndex2
*/
void createHuffmanTree(HTNode* HTArray, int* weightArray, int leafNum)
{
//哈夫曼数组的长度
int length = leafNum * 2 - 1;
//叶子结点初始化
for (int i = 0; i < leafNum; i++)
{
HTArray[i].parent = UNCONSTRUCTED;
HTArray[i].left = UNCONSTRUCTED;
HTArray[i].right = UNCONSTRUCTED;
HTArray[i].weight = weightArray[i];
}
//非叶子结点初始化
for (int i = leafNum; i < length; i++)
{
HTArray[i].parent = UNCONSTRUCTED;
HTArray[i].left = UNCONSTRUCTED;
HTArray[i].right = UNCONSTRUCTED;
HTArray[i].weight = 0;//权值为0
}
//构建哈夫曼树
//k为当前要写的下标,从第一个非叶子结点开始
for (int k = leafNum; k < length; k++)
{
int minIndex1, minIndex2;//两个最小值的下标
selectMin(HTArray, k, minIndex1, minIndex2);//求最小和次小权值的下标
//cout<<"\n本轮写的结点下标k="<<k<<" minIndex1="<<minIndex1<<" minIndex2="<<minIndex2<<endl;
//minIndex1和minIndex2两个结点构建子树
HTArray[k].weight = HTArray[minIndex1].weight + HTArray[minIndex2].weight;
HTArray[k].left = minIndex1;
HTArray[k].right = minIndex2;
HTArray[minIndex1].parent = k;
HTArray[minIndex2].parent = k;
//cout<<"最小下标"<<minIndex1<<"次小下标"<<minIndex2<<endl;
//printHufTree(HTArray,leafNum);//打印中间结果
}
return;
}
/**选择最小和次小权重
*@param HTArray 存放哈夫曼树结点的数组
*@param k 为当前要写的结点的下标
*@param minIndex1,minIndex2最小和次小结点的下标
查找权重值最小的两个结点的思想是:从树组起始位置开始,首先找到两个无父结点的结点(说明还未使用其构建成树),然后和后续无父结点的结点依次做比较,有两种情况需要考虑:
如果比两个结点中较小的那个还小,就保留这个结点,删除原来较大的结点;
如果介于两个结点权重值之间,替换原来较大的结点;
*/
void selectMin(HTNode* HTArray, int k, int& minIndex1, int& minIndex2)
{
int i = 0;
minIndex1 = -1;
minIndex2 = -1;
while (i < k)
{
if (HTArray[i].parent == UNCONSTRUCTED)
{
if (minIndex1 == -1 || HTArray[i].weight < HTArray[minIndex1].weight)
{
minIndex2 = minIndex1;
minIndex1 = i;
}
else if (minIndex2 == -1 || HTArray[i].weight < HTArray[minIndex2].weight)
{
minIndex2 = i;
}
}
i++;
}
}
/**生成哈夫曼编码的函数
HTArray 存放哈夫曼树的数组
code 存放编码的字符串
leafNum 叶子节点个数
从叶子结点一直找到根结点,逆向记录途中经过的标记。
是左儿子标记0,右儿子标记1。
*/
void huffmanCoding(HTNode* HTArray, char**& code, int leafNum)
{
char *temp; //声明temp数组
temp = new char[leafNum];//定义临时工作空间,存储临时产生的编码串
temp[leafNum - 1] = '\0'; //因为是逆向,所以得从末尾开始,先把最后一位置为空
int start, pos; //start为temp数组正在处理位置de下标,pos记录正在处理的哈夫曼树的当前位置
int parent; //记录父节点
for(int i=0;i<leafNum;i++)//依次遍历哈夫曼数组
{
start = leafNum - 1;
pos = i;
parent = HTArray[i].parent; //找到父节点
while (parent != -1)
{
if (HTArray[parent].left == pos) //当前结点是左孩子的话,就存0
{
temp[--start] = '0'; //start前移一位,存入编码
}
else //右孩子,存1
{
temp[--start] = '1';
}
pos = parent; //pos移动到父节点,以进行下一轮查找
parent = HTArray[parent].parent;//更新父节点
}
code[i] = new char[leafNum - start]; //建立哈夫曼编码实际需要的内存空间
//leafNum-start是正在处理结点的编码长度
strcpy(code[i], &temp[start]); //将临时存储的编码拷贝到code中
}
delete []temp; //释放工作空间
}
//打印哈弗曼编码
void printCode(char* *code, char* data, int leafNum)
{
// cout<<"哈夫曼编码是\n";
for (int i = 0; i < leafNum; i++)
{
cout << data[i]<<":";
cout << code[i] << endl;
}
}
int main()
{
//叶子结点个数
int leafNum;
int i;
int w[MAX] = { 0 };
//权值数组和字符数组
cin >> leafNum;
for (i = 0; i < leafNum; i++)
{
cin >> w[i];
}
//输入字符数组
char ch[MAX] = { 0 };
char** code = new char* [leafNum]; //二级指针,指向指针的指针。为code分配内存空间,存储编码
for (i = 0; i < leafNum; i++)
{
cin >> ch[i];
}
//哈夫曼树数组
HTNode* HTArray = new HTNode[leafNum * 2 - 1];
//创建哈夫曼树
createHuffmanTree(HTArray, w, leafNum);
//哈夫曼编码
huffmanCoding(HTArray,code, leafNum);
//打印哈夫曼编码
printCode(code, ch,leafNum);
//删除内存
for(int j=0;j<leafNum;j++)
{
delete code[j];
}
delete []code;
delete []HTArray;
return 0;
}
计算WPL
【算法】
WPL为所有叶节点的带权路径长度之和,同时也是所有非叶子结点的权值之和。
【题解代码】
正常思路是遍历整个哈夫曼树,对于树中的每个叶子结点,计算其权重与从根结点到该叶子结点的路径长度的乘积。然后将所有叶子结点的这个乘积相加,得到整棵树的WPL。
但是通过观察可以发现WPL也可以通过用叶子结点的权值乘以其哈弗曼编码的长度求得。下面展示的是这种方法的代码。
哈夫曼树的创建和其编码的函数代码和上面一样,以下是计算WPL的函数。
/*求哈夫曼树的权值*/
//首先找到叶子结点,然后用叶子结点的数字*其哈夫曼编码的长度,再求和
int seekWPL(HTNode* HTArray, int leafNum, char** code)
{
int i = 0;
int sum = 0;
while (i < leafNum)
{
if (HTArray[i].left == -1&&HTArray[i].right==-1)//找到叶子结点
{
int length = strlen(code[i]);
sum += HTArray[i].weight * length;
}
i++;
}
return sum;
}
这里的code
是一个二级指针,因为它是用来存储哈夫曼编码的字符串数组的地址。通过使用二级指针,seekWPL
函数可以直接修改 code
数组中的字符串。
字符串的哈夫曼编码
【题目要求】
输入一行字符串,例如:
Hooray! It's snowing! It's time to make a snowman.James runs out. He makes a big pile of snow.
统计上面的英文字母出现的次数作为权值,不区分大小写,对字母进行哈夫曼编码,输出各个字母的哈夫曼编码。字符个数不超过1000个,不少于2个,英文字符的种类不唯一。
【测试数据】
输入
Hooray! It's snowing! It's time to make a snowman.James runs out. He makes a big pile of snow.
输出
A:000
B:100110
E:1101
F:100111
G:00110
H:00111
I:1110
J:101000
K:10000
L:101001
M:1011
N:1111
O:010
P:101010
R:10001
S:011
T:1100
U:10010
W:0010
Y:101011
注意:输出结果中不包含统计个数为0的字母,应该想办法去除,在生成哈夫曼编码时就将他们去掉。
【题解代码】
#include<iostream>
#include<string.h>
#include <iomanip>
#include<string>
#define UNCONSTRUCTED -1 //表示结点没有构建树
#define MAX 20
using namespace std;
/**哈夫曼树结点结构*/
struct HTNode
{
int weight;//结点权重
int parent, left, right;//父结点、左孩子、右孩子在数组中的位置下标
};
/**选择最小和次小结点*/
void selectMin(HTNode* HTArray, int k, int& minIndex1, int& minIndex2);
/**创建哈夫曼树*/
void createHuffmanTree(HTNode* HTArray, int* weightArray, int leafNum);
/**创建哈夫曼树*/
void createHuffmanTree(HTNode* HTArray, int* weightArray, int leafNum)
{
//哈夫曼数组的长度
int length = leafNum * 2 - 1;
//叶子结点初始化
for (int i = 0; i < leafNum; i++)
{
HTArray[i].parent = UNCONSTRUCTED;
HTArray[i].left = UNCONSTRUCTED;
HTArray[i].right = UNCONSTRUCTED;
HTArray[i].weight = weightArray[i];
}
//非叶子结点初始化
for (int i = leafNum; i < length; i++)
{
HTArray[i].parent = UNCONSTRUCTED;
HTArray[i].left = UNCONSTRUCTED;
HTArray[i].right = UNCONSTRUCTED;
HTArray[i].weight = 0;//权值为0
}
//构建哈夫曼树
//k为当前要写的下标,从第一个非叶子结点开始
for (int k = leafNum; k < length; k++)
{
int minIndex1, minIndex2;//两个最小值的下标
selectMin(HTArray, k, minIndex1, minIndex2);//求最小和次小权值的下标
//cout<<"\n本轮写的结点下标k="<<k<<" minIndex1="<<minIndex1<<" minIndex2="<<minIndex2<<endl;
//minIndex1和minIndex2两个结点构建子树
HTArray[k].weight = HTArray[minIndex1].weight + HTArray[minIndex2].weight;
HTArray[k].left = minIndex1;
HTArray[k].right = minIndex2;
HTArray[minIndex1].parent = k;
HTArray[minIndex2].parent = k;
//cout<<"最小下标"<<minIndex1<<"次小下标"<<minIndex2<<endl;
//printHufTree(HTArray,leafNum);//打印中间结果
}
return;
}
/**选择最小和次小权重*/
void selectMin(HTNode* HTArray, int k, int& minIndex1, int& minIndex2)
{
int i = 0;
minIndex1 = -1;
minIndex2 = -1;
while (i < k)
{
if (HTArray[i].parent == UNCONSTRUCTED)//找无父结点的结点(未构建树的结点)
{
if (minIndex1 == -1 || HTArray[i].weight < HTArray[minIndex1].weight)//比最小的还小
{
minIndex2 = minIndex1;
minIndex1 = i;
}
else if (minIndex2 == -1 || HTArray[i].weight < HTArray[minIndex2].weight)//在两者之间
{
minIndex2 = i;
}
}
i++;
}
}
/**生成哈夫曼编码的函数
HTArray 存放哈夫曼树的数组
code 存放编码的字符串
leafNum 叶子节点个数
从叶子结点一直找到根结点,逆向记录途中经过的标记。
是左儿子标记0,右儿子标记1。
*/
void huffmanCoding(HTNode* HTArray, char**& code, int leafNum)
{
char* temp; //声明temp数组
temp = new char[leafNum];//定义临时工作空间,存储临时产生的编码串
temp[leafNum - 1] = '\0'; //因为是逆向,所以得从末尾开始,先把最后一位置为空
int start, pos; //start为temp数组正在处理位置de下标,pos记录正在处理的哈夫曼树的当前位置
int parent; //记录父节点
for (int i = 0; i < leafNum; i++)//依次遍历哈夫曼数组
{
start = leafNum - 1;
pos = i;
parent = HTArray[i].parent; //找到父节点
while (parent != -1)
{
if (HTArray[parent].left == pos) //当前结点是左孩子的话,就存0
{
temp[--start] = '0'; //start前移一位,存入编码
}
else //右孩子,存1
{
temp[--start] = '1';
}
pos = parent; //pos移动到父节点,以进行下一轮查找
parent = HTArray[parent].parent;//更新父节点
}
code[i] = new char[leafNum - start]; //建立哈夫曼编码实际需要的内存空间
//leafNum-start是正在处理结点的编码长度
strcpy(code[i], &temp[start]); //将临时存储的编码拷贝到code中
}
delete[]temp; //释放工作空间
}
//打印编码
void printCode(HTNode* HTArray, char** code, char* data, int leafNum)
{
// cout<<"哈夫曼编码是\n";
for (int i = 0; i < leafNum; i++)
{
cout << data[i] << ":";
cout << code[i] << endl;
}
}
int main()
{
string s;
int i;
int word[27] = { 0 };//记录从a到z的每个字母的个数
getline(cin, s); //获取带空格的字符串
//计算字母个数
for (i = 0; i < s.length(); i++)
{
if (isalpha(s[i])) //判断当前字符是否是字母
{
s[i] = tolower(s[i]); //如果是的话,将大写转换成小写
word[s[i] - 'a']++; //记录个数
}
}
int count = 0;// 计算有多少个字母个数不为0
for (i = 0; i < 26; i++)
{
if (word[i] != 0)
count++;
}
int weightArray[27] = { 0 };
char data[27] = { 0 }; //记录实际个数不为0的字母
int j = 0;
for (i = 0; i < 26; i++)
{
if (word[i] != 0)
{
weightArray[j] = word[i];
data[j] = 'A' + i;
j++;
}
}
char** code = new char* [count]; //为code分配内存空间,存储编码
//哈夫曼树数组
HTNode* HTArray = new HTNode[count * 2 - 1];
//创建哈夫曼树
createHuffmanTree(HTArray, weightArray, count);
//生成哈夫曼编码
huffmanCoding(HTArray, code, count);
//打印编码
printCode(HTArray, code, data, count);
//删除内存
for (int j = 0; j < count; j++)
{
delete code[j];
}
delete[]code;
delete[]HTArray;
return 0;
}