一,Huffman树的简介
1,定义
给定n个带权值的节点,将这n个节点作为叶子节点构建一棵二叉树,若该树的带权路径最小,则称该树为最优二叉树,也叫Huffman树。
2,生成方法
<1>,在节点中取权值最小的两个节点为叶节点,生成一棵二叉树,该二叉树的父节点权值为两个叶节点的权值之和。
<2>,将取出的两个节点删除,加入新生成的父节点,重新回到步骤1,直到只剩下1个节点不能再生成树为止。
举个例子,现在我们有一下几个节点,它们的权值分别如下所示:
带权路径长度WPL(weighted path length)的计算公式:
公式中,n为叶子节点树,Wk为第k个叶子结点的权值,Lk为该结点的路径长度。
生成的Huffman树为:
这里我们规定了权值小的节点为左子节点,实际上最后生成的Huffman树结构不一定只有一种,但他们的带权路径长是相同的。
3,Huffman树的特点
<1>一开始给出需要处理的节点都是Huffman树的叶节点。
<2>Huffman树的带权路径长度是最小的。
现在我们以这些节点为叶节点随便构建一棵二叉树:
该树的带权路径长度为:
同样利用这些节点生成Huffman树的带权路径长为:
这里我们可以看到:
这是因为在生成Huffman树的时候,我们让权值大的节点的路径尽可能最短。
4,编码规则
生成Huffman树之后,我们可以对每个叶子节点编码,我们规定,左边路径编码为0,右编路径编码为1,则上述Huffman树编码为:
节点e可以用01表示,节点b可以用011表示。
5,应用
Huffman树可以用来压缩数据,以上面节点为例,一共有5种不同的节点,采用不同的编码规则传输。
<1>如果我们用等长编码分别表示这五个节点:
000 ——> a
001 ——> b
010 ——> d
011 ——> f
100 ——> e
那么我们要传送下面这串字符串的话传输的编码为:
一共传输48位,解码时3位一读就可以还原。
<2>不等长编码
在传输电位时为了使传输的位数尽可能小,可以令出现频次多的字符编码长度短,出现频次少的字符编码长度长,如:
0 ——> d
1 ——> a
00 ——> e
10 ——> b
111 ——> f
则传输的编码为:
传输长度为23位。
但当我们解码时以前5位11111为例,我们不知道到底时5个a还是1个f和2个a,这是因为有编码字符是另一个编码字符的前缀,导致译码不唯一,因此这种编码方式不可取。
<3>Huffman编码
为了使传输的位数尽可能少并且使任何一个编码字符都不是另一个编码字符的前缀,设计出了Huffman编码。
当我们采用Huffman编码时
传输的编码为:
一共传输33位,码表如下:
10 ——> a
011 ——> b
11 ——> d
010 ——> f
00 ——> e
所有字母都处在叶子节点上,每个叶子节点都不会经过其他叶子节点,这样不会出现一个字母的编码是另一个字母编码左起子串的情况。
二,Huffman树的构建
1,先创建节点类,该类中包含了每一个节点所需要的属性,以及不同情况创建节点时使用的构造方法。
package com.yzd0425.Huffman;
//节点类
public class HNode {
public String code;//节点的Huffman编码
public char data;//节点数据
public int count;//节点频次;
public HNode left;//节点的左子节点
public HNode right;//节点的右子节点
//构造方法 构建叶节点时使用的方法
public HNode(char data,int count) {
this.data=data;
this.count=count;
}
//构造方法 构建父节点时使用的方法
public HNode(int count,HNode left,HNode right) {
this.count=count;
this.left=left;
this.right=right;
}
}
2,创建字符数据类,我们使用链表LinkedList来存储字符相关信息,这样方便读取。LinkedList中存储的是统计了频次的字符信息,每一个元素代表了每个字符的信息:字符数据+字符频次。
public LinkedList<CharData> charList;//用来存放去重字符的队列
package com.yzd0425.Huffman;
//每个独一无二字符的信息:字符c+出现次数num
public class CharData {
public char c;
public int num = 1;//初始化为1 只要创建了则最少出现一次
//构造方法
public CharData(char c) {
this.c=c;
}
}
3,统计字符串中每个字符出现的频次。依次从传入的字符串str中取出每个字符,如果和字符链表charList中某个字符相同,该字符的频次num+1,没有的话创建一个新的CharData加入到charList中。
//统计出现的字符及其频率
public LinkedList<CharData> getCharNum(String str,LinkedList<CharData> charList) {
for(int i=0;i<str.length();i++) {
char c = str.charAt(i);//得到字符串中指定位置的字符
flag = false;//默认不重复
//取出的字符和存放去重的字符队列中的每一个元素比较 如果都不相同 则时一个新字符 添加进字符队列中
for(int j=0;j<charList.size();j++) {
CharData cd = charList.get(j);
if(c==cd.c) {
cd.num++;
flag = true;
break;
}
}
if(!flag) {//遍历完CharList都没找到 则创建一个新的CharData
charList.add(new CharData(c)); //该函数中已对全局参数charList进行了具体赋值 之后的charList可以使用形参
}
}
return charList;
}
4,创建节点。将统计完的charList中的元素创建为一个个节点,同样也使用一个链表nodeList来存储创建好的节点,这样在创建Huffman树的时候就可以从nodeList中取出节点创建了。
public LinkedList<HNode> nodeList;//用来存放节点的队列(已去重)
//将出现的不重复字符创建为单个节点 因为生成Huffman树是用节点生成
public LinkedList<HNode> createNode(LinkedList<CharData> charList,LinkedList<HNode> nodeList) {
for(int i=0;i<charList.size();i++) {
char data = charList.get(i).c;
int count = charList.get(i).num;
HNode node = new HNode(data,count);
nodeList.add(node);//该函数中已对全局参数nodeList进行了具体赋值 之后的nodeList可以使用形参
}
return nodeList;
}
5,对创建好的节点按频次升序排序。
//对去重的nodeList中的节点数据升序排序
public LinkedList<HNode> order(LinkedList<HNode> nodeList) {
for(int i=0;i<nodeList.size()-1;i++) {
for(int j=i+1;j<nodeList.size();j++) {
if(nodeList.get(i).count>nodeList.get(j).count) {
HNode temNode = nodeList.get(i);
nodeList.set(i, nodeList.get(j));
nodeList.set(j, temNode);
}
}
}
return nodeList;
}
6,取出升序排序后的nodeList中的前两个节点,将这两个节点作为左、右两个子节点创建一棵二叉树,父节点的频次为左、右节点频次之和,将取出的两个节点从nodeList中删除,父节点加入nodeList中,重新升序排序,不断重复这一步骤,直到nodeList中只剩下一个节点无法再生成树为止。
我们在生成二叉树的时候也进行编码操作,设置当前生成的左节点的编码为0,右节点的编码为1,同时从当前左、右节点一直递归到叶节点不断更新其余子树节点的编码值(该更新操作每加入两个节点都要重新执行)。
//设置节点的Huffman编码 从当前传入的节点一致递归编码至叶节点
//根节点并没有编码 每次子树编码更新是在传入的节点的编码上+0/1 并不是在子树原来的编码上+0/1 这样能保证父节点编码是子节点编码的前子串
public void setCode(HNode node) {
if(node.left!=null) {
node.left.code=node.code+"0";
setCode(node.left);
}
if(node.right!=null) {
node.right.code=node.code+"1";
setCode(node.right);
}
}
//生成Huffman树
public void createTree(LinkedList<HNode> nodeList) {
while(nodeList.size()>=2) {//当队列中节点数目>=2时,就仍能构成一棵树
//1,取出并删除节点队列中的前两个节点(权值最小)
HNode left = nodeList.poll();
HNode right = nodeList.poll();
//2,设置当前子树的Huffman编码
left.code="0";
right.code="1";
setCode(left);//从当前子树一直递归到叶节点设置编码 不断更新 并不是将Huffman树完全构建后再从根节点开始设置编码
setCode(right);
//3,生成父节点并加入节点队列中
int count = left.count+right.count;
HNode parent = new HNode(count,left,right);//此时已构成父子连接关系
nodeList.add(parent);
//4,重新将nodeList中的节点升序排序 这样保证每次处理的1、2、3步骤处理nodeList都是排序好的
order(nodeList);//不断递归
}
}
7,调用以上函数生成一棵Huffman树,使用中序遍历方法仅打印出叶节点的值验证。
//生成Huffman树 函数在该函数中调用
public void createHuffman(String str) {
//this.str=str;
charList = new LinkedList<CharData>();
nodeList = new LinkedList<HNode>();
//1,统计需要处理的字符串中字符出现的个数
charList = getCharNum(str,charList);
//2,创建节点
nodeList = createNode(charList,nodeList);
//3,对节点权值升序排序
nodeList = order(nodeList);//保证第一次处理的nodeList是有序的
//4,生成Huffman树
createTree(nodeList);//内部已不断重新排序
//5,最后一个生成的父节点赋给根节点 方便找到此树 队列中该节点不能和其他节点和起来再生成树 但有一个相同的节点已经被生成作为树的根节点
root = nodeList.get(0);
}
//遍历节点 中序遍历 递归调用 只输出打印叶节点的数据: 字符+频次+Huffman编码
public void printNode(HNode node) {
if(node.left==null&&node.right==null) {
System.out.println("该节点数据为:"+node.data+" "+"频次为:"+node.count+" "+"Huffman编码为:"+node.code);
}
if(node.left!=null) {
printNode(node.left);
}
if(node.right!=null) {
printNode(node.right);
}
}
//主函数 程序入口
public static void main(String[] args) {
HuffmanTree huff = new HuffmanTree();
String data = "aaabbdddddfeeea";
huff.createHuffman(data);//创建树
huff.printNode(root);//打印已去重的节点
}
输出结果如下:
和我们在上文中生成的Huffman树是一致的。
三,Huffman编码
利用上述生成的Huffman树,我们对字符串"aaabbdddddfeeea"进行编码。
1,从字符串str的第一个字符开始,每个字符c都从根节点开始搜索,搜索Huffman树的所有叶子节点。
2,当找到某个叶子节点的值data和字符c一致时,便使用该节点的code作为该字符的Huffman码。
3,每个字符的Huffman码都添加到之前已完成搜索的HcodeStr之后,这样就能得到整个字符串str的Huffman编码了。
public static String HcodeStr="";//字符串编码后的Huffman编码
/***********************以下是编码的实现*************************/
//得到字符串Huffman编码 每次调用都在上一个Huffman编码的基础上累加
public void search(HNode node,char c) {
//找到叶节点
if(node.left==null&&node.right==null) {
if(node.data==c) {
HcodeStr += node.code;
//System.out.println("找到了");
}
}
if(node.left!=null) {
search(node.left,c);
}
if(node.right!=null) {
search(node.right,c);
}
}
//得到Huffman编码 每个字符搜索一次 每次Huffman编码累加 得到字符串完整的Huffman编码
public String getHcode(String str) {
for(int i=0;i<str.length();i++) {
char c = str.charAt(i);
search(root,c);
}
return HcodeStr;
}
//将得到的Huffman编码打印出来
public void printHcode(String Hcodestr) {
System.out.println("该字符串的Huffman编码为:"+Hcodestr);
}
HcodeStr = huff.getHcode(data);//编码
huff.printHcode(HcodeStr);
输出如下:
四,Huffman解码
我们对上述得到的Huffman码HcodeStr"101010011011111111111101000000010"进行解码。
1,因为每个字符的Huffman编码长度不一,每个字符对应的Huffman编码是整个HcodeStr的子串,所以我们定义每次查找的子串起始位start和终止位end,使用函数HcodeStr.substring()得到得到codeString的 start : end-1 位子编码串。
2,用result保存解码后的字符串,设置一个标识符target判断是否找到匹配字符,每次查找前默认为没找到false。
3,从HcodeStr的第一位开始查找,得到一个子编码串str,从Huffman树的根节点开始遍历所有叶子节点,这里有两种情况:
(a)如果有叶节点字符的Huffman编码与str一致,则认为找到了,将target置为true,该节点的字符data存入result中,设置下一次查找的起始位start为该次查找的end位,同时end位向后移一位end++。
(b)如果没有叶节点的Huffman编码与str一致,该次查找未找到,target保持默认值false,将查找的子编码串添加一位end++,起始位star仍然不变,继续查找,直到满足情况(a)。
4,重复(a)(b)两步,不断将每次查找到的data加在result后,直到遍历完codeString的所有字符。
public static String result="";//Huffman解码后的字符串
public boolean target;//判断某字符串是否解码成功的标志 成功为true 不成功为false
/***********************以下是解码的实现*************************/
//匹配Huffman编码 找到编码对应的字符 递归调用 遍历所有叶子节点
public void matchCode(HNode node,String code) {
if(node.left==null&&node.right==null) {//在叶节点中寻找
if(code.equals(node.code)) {//编码对应的字符找到
target = true;
result+=node.data;
return;//一找到即返回退出函数 不在找其他叶节点
}
}
if(node.left!=null) {
matchCode(node.left,code);
}
if(node.right!=null) {
matchCode(node.right,code);
}
}
//解码函数
public String code2string(String HcodeStr) {
int start = 0;//从Huffman编码的第一个数字开始查找
int end = 1;
while(end<=HcodeStr.length()) {//查找到Huffman编码的最后一位
target = false;//每次查找前默认为没找到对应字符
String str = HcodeStr.substring(start,end);//得到codeString的 start : end-1 位子字符串
matchCode(root,str);//每次都从根节点开始查询 可以遍历所有叶子节点 查找该编码是否有对应的字符 遍历完结果可能有也可能没有
if(target) {
start = end;//如果找到了就从找到Huffman编码的下一位开始重新查找 如果没找到start保持原位
}
end++;//无论找没找到 每重新执行一次查找时 都将查找编码的最后一位向后移一位
}
return result;
}
//打印Huffman解码后的字符串
public void printString(String result) {
System.out.println("Huffman解码后的字符串为:"+result);
}
result = huff.code2string(HcodeStr);//解码
huff.printString(result);
输出如下:
Huffman树构建、编码、解码完整代码:
百度网盘提取码:polp