1.哈夫曼树的定义
树中的 节点被赋予一个表示某种意义的数值,称为该节点的权。给定N个权值作为N个叶子结点,构造一棵二叉树,若该树的带权路径长度达到最小,称这样的二叉树为最优二叉树,也称为哈夫曼树(Huffman Tree)。哈夫曼树是带权路径长度最短的树,权值较大的结点离根较近。
2. 哈夫曼树的构造
1.将n个结点分别作为n棵只含一个节点的二叉树,构建成森林F
2.构造一个新结点,从F中选取两棵根结点权值最小的树作为左右子树,并且新节点为左右子树之和
3.从F中删除那两棵树,加入新结点进入F
4.重复2.3直到F中只留下一棵树
如下图为一棵哈夫曼树(数字表示各个数字的权重):
根据哈夫曼树可以得到哈夫曼编码(左子树为0,右子树为1):
a:000
b:001
c:01
d:10
e:11
哈夫曼编码是前缀编码,特点是任意一个字母的编码都不会是另外一个字母的编码的前缀。
3.哈夫曼树的特点:
1.每个初始结点最终都成为叶结点,且权值越小的结点到根结点路径越大。
2.构建过程中共新建n-1个结点,因此哈夫曼树结点总数为2*n-1.
3.哈夫曼树不存在度为1的结点。
4.代码实现
public class HfmTree {
//根结点
HfmNode root;
//叶子结点的权重表
Map<Character, Integer> weights;
//叶子结点
List<HfmNode> leafs;
//计算出的每个叶子结点的编码表
Map<Character, String> strCode;
public HfmTree(Map<Character, Integer> weights) {
this.weights = weights;
leafs = new ArrayList<>();
strCode = new HashMap<>();
createTree();
code();
}
/**
* 创建哈夫曼树
*/
private void createTree() {
// 拿出所有的点
Character[] keys = weights.keySet().toArray(new Character[0]);
// jdk底层的优先队列
PriorityQueue<HfmNode> queue = new PriorityQueue<>();
for (Character c : keys) {
HfmNode node = new HfmNode();
node.chars = c.toString();
// 权重
node.fre = weights.get(c);
//记录下所有初始节点,他们都会是叶子结点
leafs.add(node);
// 把我们的优先队列初始化进去
queue.add(node);
}
int len = queue.size();
// 每次找最小的两个点合并
for (int i = 0; i < len - 1; i++) {
// 每次取优先队列的前面两个 就一定是两个最小的
HfmNode n1 = queue.poll();
HfmNode n2 = queue.poll();
HfmNode newNode = new HfmNode();
// 我们把值赋值一下,也可以不复制
newNode.chars = n1.chars + n2.chars;
// 把权重相加
newNode.fre = n1.fre + n2.fre;
// 维护出树的结构
newNode.left = n1;
newNode.right = n2;
n1.parent = newNode;
n2.parent = newNode;
queue.add(newNode);
}
// 最后这个点就是我们的根节点
root = queue.poll();
}
/**
* 构造哈夫曼编码表
*/
private void code() {
for (int i = 0; i < leafs.size(); i++) {
HfmNode node = leafs.get(i);
// 叶子节点肯定只有一个字符
Character c = node.chars.charAt(0);
String code = "";
//parent==null说明到了根结点
while (node.parent != null) {
//当前结点是左结点为0,右节点为1
code = node == node.parent.left ? "0" + code : "1" + code;
node = node.parent;
}
strCode.put(c, code);
System.out.println(c + ":" + code);
}
}
/**
* 编码
*/
public String encode(String str) {
char[] chars = str.toCharArray();
String encode = "";
for (int i = 0; i < chars.length; i++) {
encode = encode + strCode.get(chars[i]);
}
return encode;
}
/**
* 解码
*/
public String decode(String str) {
char[] chars = str.toCharArray();
HfmNode curr = root;
String decode = "";
for (int i = 0; i < chars.length; i++) {
curr = chars[i] == '0' ? curr.left : curr.right;
if (curr.left == null && curr.right == null) {
decode = decode + curr.chars;
curr = root;
}
}
return decode;
}
/**
* 树的结点
*/
class HfmNode implements Comparable<HfmNode> {
//节点里面的字符
String chars;
//权重
int fre;
//用来找上层的
HfmNode parent;
HfmNode left;
HfmNode right;
@Override
public int compareTo(HfmNode o) {
return this.fre - o.fre;
}
}
/**
* 测试
*/
public static void main(String[] args) {
// a:3 b:4 c:8 d:12 e:13
Map<Character, Integer> weights = new HashMap<Character, Integer>();
weights.put('a', 3);
weights.put('b', 4);
weights.put('c', 8);
weights.put('d', 12);
weights.put('e', 13);
HfmTree huffmenTree = new HfmTree(weights);
String str = "abcde";
String str1 = huffmenTree.encode(str);
String str2 = huffmenTree.decode(str1);
System.out.println("编码前"+str);
System.out.println("编码后的:" + str1);
System.out.println("解码后" + str2);
}
}
测试结果如下:
a:000
b:001
c:01
d:10
e:11
编码前abcde
编码后的:000001011011
解码后abcde
结果和上图相符合。