目录
一、哈夫曼树的初始化
/**哈夫曼树结点结构*/
struct HTNode
{
int weight;//结点权重
int parent, left, right;//父结点、左孩子、右孩子在数组中的位置下标
};
void createHuffmanTree(HTNode * HTArray,int * weightArray,int leafNum)
{
//哈夫曼数组的长度
int length=leafNum*2-1;
//叶子结点初始化
for(int i=0;i<leafNum;i++)
{
HTArray[i].parent=-1;
HTArray[i].left=-1;
HTArray[i].right=-1;
HTArray[i].weight=weightArray[i];
}
//非叶子结点初始化
for(int i=leafNum;i<length;i++)
{
HTArray[i].parent=-1;
HTArray[i].left=-1;
HTArray[i].right=-1;
HTArray[i].weight=0;//权值为0
}
//构建哈夫曼树
//k为当前要写的下标,从第一个非叶子结点开始
for(int k=leafNum;k<length;k++)
{
int minIndex1,minIndex2;//两个最小值的下标
selectMin(HTArray,k,minIndex1,minIndex2);//求最小和次小权值的下标
//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 ;
}
这段代码就完成了一个哈夫曼数最基本的创建,采用类似于表格的方式存储各类节点的信息。对于每个叶子节点,初始时权重是已知的,其余都赋值为-1表示没有,而其他节点所有元素都是未知的。
接下来,我们展示如何每次选择两个权重最小的节点。
void selectMin(HTNode * HTArray,int k,int & minIndex1,int & minIndex2)
{
//minIndex1表示权重最小节点索引,minIndex2表示次小节点
//flag表示当前已经被赋值的最小索引个数
int flag = 0;
for(int i = 0; i < k; i ++)
{
if(HTArray[i].parent == -1)
{
//没有一个索引被赋值,则赋值给第一个索引
if(!flag)
{
minIndex1 = i;
flag ++;
}
else if(flag == 1) //只有一个被赋值,则判断大小决定赋值
{
if(HTArray[i].weight < HTArray[minIndex1].weight)
{
minIndex2 = minIndex1;
minIndex1 = i;
}
else minIndex2 = i;
flag ++;
}
else
{
if(HTArray[i].weight < HTArray[minIndex1].weight)
{
minIndex2 = minIndex1;
minIndex1 = i;
}
else if(HTArray[i].weight < HTArray[minIndex2].weight) minIndex2 = i;
}
}
}
}
二、哈夫曼编码
在成功实现哈夫曼树的创建之后,我们就可以对每个叶子节点进行哈夫曼编码了。
void huffmanCoding(HTNode* HTArray, char *code[], int leafNum)
{
char* temp = new char[leafNum];
temp[leafNum - 1] = '\0';
for(int i = 0; i < leafNum; i ++)
{
int start = leafNum - 1;
int pos = i;
int parent = HTArray[i].parent;
while(parent != -1)
{
if(HTArray[parent].left == pos)
temp[--start] = '0';
else temp[--start] = '1';
pos = parent;
parent = HTArray[parent].parent;
}
code[i] = new char[leafNum -start];
strcpy(code[i], &temp[start]);
}
delete temp;
}
由于我们特殊的存储格式,很难实现从父节点一路遍历到孩子节点,所以我们采用逆向获得编码的方式,从孩子节点向上遍历。
三、WPL计算
根据WPL(带权路径长度)的计算公式,可以很容易求出对应哈夫曼树的WPL。
int getWPL(HTNode* HTArray, int leafNum)
{
int sum = 0;
for(int i = leafNum; i < 2 * leafNum - 1; i ++)
{
sum += HTArray[i].weight;
}
return sum;
}
四、字符串的哈夫曼编码
由于输入的是字符串,所以我们需要提取一下字符串里的数据
void getData(string s, int w[],char data[], int &leafNum)
{
for(auto i : s)
{
w[i] ++; //统计位置为i的字符个数
}
for(int i = 0; i < 128; i ++)
{
if(w[i])
data[leafNum ++] = i; //按顺序把出现过的字符放入data数组
}
int k = 0;
for(int i = 0; i < 128; i ++)
{
if(w[i])
w[k ++] = w[i];
//把w数组中为零的部分删掉,不为零的下标前移,让data和w数组中相同下标对应的元素分别为该字符,和它的权重
}
}
其中s为读取的字符串,w为对应权值数组,data为字符串中的内容,leafNum为叶子节点个数(字符个数)。查ASCII码表得知常见字符最大的对应十进制数为127。
接下来利用函数打印就行
void printCode(char* code[], char* data, int leafNum)
{
for(int i = 0; i < leafNum; i ++)
{
cout << data[i] << ':' << code[i] << endl;
}
}
五、利用哈夫曼编码实现字符串压缩
对于一个较长的字符串,我们可以利用求出哈夫曼编码对其压缩。
string getCode(char* code[], string s)
{
string a;
for(auto i : s)
{
a += code[i];
}
int t = 0;
int j = 1;
string b;
for(auto i : a)
{
if(t == 8)
{
t = 0;
char c = char(j);
b += c;
j = 1;
}
j <<= 1;
if(i == '1')
j ++;
t ++;
}
if(a.size() % 8 != 0)
j <<= (8 - a.size() % 8);
b += char(j);
return b;
}
我们知道,ASCII码表中的字符对应的都是八位二进制数,所以,我们可以对得到的编码每八位进行一次转换,得到一个对应的字符。这样就可以把长串的字符串变成短串。对于最后如有剩余,我们采用补零的策略。
对于压缩后的短串,我们同样需要解压缩。
string Sentence(string Code, HTNode* HTArray, int leafNum, int size)
{
int pos = 2 * leafNum - 2;
string a;
bool flag = false;
for(auto i : Code)
{
string b;
for(int j = 0; j < 8; j ++)
{
int x = int(i) >> j & 1;
b = char(x + '0') + b;
}
for(auto i : b)
{
if(i == '0')
{
if(HTArray[pos].left != -1)
{
pos = HTArray[pos].left;
}
else
{
a += HTArray[pos].data;
pos = 2 * leafNum - 2;
pos = HTArray[pos].left;
}
}
else
{
if(HTArray[pos].right != -1)
{
pos = HTArray[pos].right;
}
else
{
a += HTArray[pos].data;
pos = 2 * leafNum - 2;
pos = HTArray[pos].right;
}
}
if(a.size() == size)
{
flag = true;
break;
}
}
}
if(!flag) a += HTArray[pos].data;
return a;
}
对于Code(压缩串)中的每一个字符,我们都需要把它分成八位的二进制串再进行运算。先将字符转成十进制整数,然后从低位向高位依次取出二进制对应数,由于是逆向的,所以要将每一次取出来的数放在前面。由于最后可能被补零了,所以我们要记录原长度,防止多读。