数据结构.哈夫曼树(HuffmanTree)

Huffman有好几种叫法

  • 霍夫曼
  • 哈夫曼
  • 赫夫曼
  • 难道还有别的吗…

不过我比较喜欢“哈”开头,因为笔画比较少……不过还是原汁原味的‘Huffman’更有感觉。

HuffmanTree

  • Huffman树是“带权路径长度最短的树”,说白了就是权值越大的越靠近根节点越容易被取出来,权值越小的越深入地下越费力才能取出来。
  • 所谓权值,我所知道的应用场所就是频率,越有可能被使用到的数据(频率越大)对应的权值也就越大,也就越靠近根节点(所在的层数越小)使得越容易被取出。如此排列使得获取数据所需要花费的力气的数学期望可以达到一个最小的值。
  • 应用之一是哈夫曼编码

Huffman Code 哈夫曼编码

  • 非等长编码,即出现频率越大的内容给予的编码长度越短(对应HuffmanTree中的实现就是其所在的层数越小),使得平均编码长度(数学期望)达到最小,减小对内容编码所占的空间
  • HuffmanCode使用的编码方式是“前缀编码”,即每一个编码都不是另一个编码的前缀,使得译码的时候一个一个读取可以一一对应到相应的内容,例如下面
内容 编码
a 0
b 10
c 110
d 111
  • 对于一串编码”010110111”顺序读取下来就可以译码成”abcd”而没有二义性
  • 这里就要用到HuffmanTree来进行编码表的创建,以26个小写字母为例,每个字母对应一串只有’0’和’1’的编码

1、统计字母的频率

  • 实际只需统计频数即可,因为需要的是小写字母出现频率(权重)之间的大小关系,在这一点上频率和频数的作用一样
  • 读取文件source.txt(#define SOURCEFILE source.txt),内容是一串小写字母
int Weight[267]={0};//[1]到[26]分别对应'a''z',[0]未使用
ifstream input;
input.open(SOURCEFILE);
char inChar;
while(!input.eof())
{
    input>>inChar;/*默认输入的是小写字母,实际应当检查一下输入是否正确*/
    Weighr[inChar - 'a' + 1] += 1;
}
input.close();
  • 可能会有的小写字母未出现,那权值也就是0

2、建立HuffmanTree

  • 这里并没有使用真正的链表,而是使用了数组,以索引来当地址,我称之为“伪链表”不知是否合适。对应的逻辑仍然是一棵二叉树
  • HuffmanTree建立的思想是(以26个字母与和其对应的权值为例)
    • 1)每个字母对应一个叶子节点,包括权值int weight和链域(数组索引)int parent, LeftChild, RightChild
    • 2)选取权值最小的两个节点分别作为左右孩子来合并成一个新节点,新节点的权值为二者之和,这样就剩下了25个节点
    • 3)再从这25个节点中选权值最小的两个合并
    • 4)直到只剩下一个节点,那也就是根节点了,HuffmanTree也就建立好了
  • HuffmanTree共2*26-1=51个节点,数组形式以[1,26]存放叶子节点,[27,51]存2度节点,合并的节点从27开始依次使用,[51]即为根节点
//哈夫曼树节点
struct HuffmanTreeNode
{
    int weight;
    int parent, LeftChild, RightChild;
};
//使用到的相关数据
HuffmanTreeNode *huffmanTree;//哈夫曼树的数组形式
int length;//值为26,表示参与编码的内容(小写字母)有26个
/*建立哈夫曼树*/

int treeLength = 2 * length - 1;//哈夫曼数组的长度
huffmanTree = new HuffmanTreeNode[treeLegnth+1];//[0]未使用

int index = 1;//初始化工作
for(;index<=length;++index)
{//[1,length]为叶子节点对应着'a'到'z',赋值权值
    huffmanTree[index] = {Weight[index],0,0,0};
}
for(;index<treeLength;++index)
{//[length+1,treeLength]为度为2的节点
    huffmanTree[index] = {0,0,0,0};
}

int s1 = 0, s2 = 0;//select index,保存选择的权值最小且parent为0的节点的索引
for(index = length+1;index<=treeLength;++index)
{
    Select(index - 1,s1,s2);//从huffmanTree[1,index-1]中选取相应的节点,s1和s2引用传递

    huffmanTree[s1].parent = index;
    huffmanTree[s2].parent = index;
    huffmanTree[index].LeftChild = s1;
    huffmanTree[index].RightChild = s2;
    huffmanTree[index].weight = huffmanTree[s1].weight + huffmanTree[s2].weight;
    /*合并节点,可见s1和s2的节点parent都不是0了,而刚刚拥有weight值的index节点的parent仍未初始值0,在下一次搜索最小权值点时s1和s2将不会被纳入搜索范围,而index也就是s1和s2合并的节点会被纳入搜索范围*/
}

3、建立huffman编码表

  • 就是这个
char **huffmanCode;//编码表,第i行为letters[i]对应的'0''1'编码
/*根据huffmanTree建立huffman编码表*/

huffmanCode = new char*[length+1];//[0]未使用
char *cd = new char[length+1];//currentCode
int cdi = 0;//currentCodeIndex

for(int i=1;i<=treeLength;++i)
{
    huffmanTree[i].weight = 0;//权值已无用,用来当标志域
    //0表示下一步寻找左孩
    //1表示下一步寻找右孩
    //2表示返回上一个节点
}

int index = treeLength;//从根节点开始
while(index!=0)
/* 最终退出是因为遍历了整个树后继续向上返回index=huffmanTree[treeLength].parent变成了0 */
{
    if(huffmanTree[index].weight == 0)
    {//向左
        huffmanTree[index].weight = 1;//下次向右
        if(huddmanTree[index].LeftChild !=0)
        {//有左孩
            index = huffmanTree[index].LeftChild;
            cd[cdi++]='0';//此位编码为0
        }
        else
        {//没有左孩,意味着到达叶子,也就是该得到一个字母对应的编码了
            huffmanCode[index] = new char[cdi+1];
            cd[cdi] = '\0';
            for(int i=0;huddmanCode[index][i]=cd[i];++i);
            //有一个默认的对应关系,即1到26对应字母'a'到'z'
        }
    }
    else if(huffmanTree[index].weight == 1)
    {//向右
        huffmanTree[index].weight = 2;//下一步返回
        if(huffmanTree[index].RightChild!=0)
        {//有右孩
            index = huffmanTree[index].RightChild;
            cd[cdi++] = '1';
        }
    }
    else //if(huffmanTree[index].weight == 2)
    {//返回
        huffmanTree[index].weighr = 0;//还原
        index= huffmanTree[index].parent;
        cdi--;
    }
}

4、愉快地Huffman编码

  • 手握编码表
char **huffmanCode;//编码表,第i行为letters[i]对应的'0''1'编码
  • 对于原本的小写字母序列就可以进行编码了
ifstream input;
input.open(SOURCEFILE);//输入
ofstream output;
output.open(OUTPUTFILE);//输出

char inChar;
while(!input.eof())
{
    input>>inChar;
    output<<huffmanCode[inChar -'a' + 1];
    cout<<huffmanCode[inChar - 'a' + 1];
}
input.close();
output.close();

5、其他

  • 这种方法是静态编码。对于每个不同的源文件都需要先扫描一遍计算出每个字母对应的频数(权值)然后才能建立huffmanTree然后再进行编码。(不过身为一种压缩技术,压缩的时间会比较费时,不过解压起来很快,在已有的huffmanTree上从根节点开始根据读取的’0’或’1’来移向左孩或右孩最终达到一片叶子也就解码了一个字母)
  • 叫我感觉我会说扫描两边显得很笨很麻烦,扫描两遍算什么啊
  • 嗯,只扫描一边叫做动态Huffman编码,暂时只了解思想含义,并没有实践
  • 本来想着画图来帮助理解,然而用PS太麻烦。后来又想起来可以用Visio啊!哎,只顾着嫌弃自己笨而懒得画图了。

你可能感兴趣的:(数据结构,Huffman,霍夫曼,哈夫曼,赫夫曼)