1. 哈夫曼树(Huffman Tree)
哈夫曼树/赫夫曼树,又称最优树,是一种带权路径最短的二叉树,在信息检索中应用广泛。
特点:
- 树的带权路径只考虑叶子节点,针对相同叶子结点个数与权值,可以构造出结构不同的二叉树。
- 权值较大的结点离根较近,而权值较小的结点离根最远,以此保证树的带权路径长度最小。
路径和路径长度
- 路径:在一棵树中,从一个结点往下可以达到的孩子或孙子结点之间的通路,称为路径。
- 路径长度:路径经过的分支数目称为路径长度,若规定根结点的层数为1,则从根结点到第L层结点的路径长度为L-1。
结点的权值及带权路径长度
- 权值:若将树中结点赋给一个有着某种含义的数值,则这个数值称为该结点的权。
- 结点的带权路径长度:从根结点到该结点之间的路径长度与该结点的权的乘积。
树的带权路径长度
- 树的带权路径长度规定为所有叶子结点的带权路径长度之和,记为WPL:
W P L = ∑ i = 1 n W i ∗ l i WPL=\displaystyle \sum^{n}_{i=1}{W_i * l_i} WPL=i=1∑nWi∗li- n 为叶子结点的个数, W i W_i Wi 为第 i 个叶子结点的权值, l i l_i li 为第 i 个叶子结点的路径长度。
如下图:
3 棵树的带权路径长度分别如下:
- a 树:
WPL = (7 * 2) + (5 * 2) + (2 * 2) + (4 * 2) = 36
- b 树:
wpl = (7 * 3) + (5 * 3) + (2 * 1) + (4 * 2) = 46
- c 树:
WPL = (7 * 1) + (5 * 2) + (2 * 3) + (4 * 3) = 35
- c 的形式就是最优二叉树,即哈夫曼树。
2. 构建哈夫曼树
步骤:
- 1 将结点按照权值从小到大排序。
- 2 取出权值最小的两个结点,将其的权值相加构成一个新的结点。
- 3 删除相加的两个结点,并将新的结点添加到序列中,再次排序。
- 4 重复以上 (1) (2) (3) 过程,直至只剩下一个结点,这个结点为根节点。
定义树节点:
class HuffmanTreeNode implements Comparable<HuffmanTreeNode> {
public int weight; // 结点权值
public HuffmanTreeNode left; // 左子树
public HuffmanTreeNode right; // 右子树
public HuffmanTreeNode(int weight) {
this.weight = weight;
}
// 先序遍历
public void preOrder() {
System.out.printf("%d\t", this.weight); // 输出当前结点
if (this.left != null) this.left.preOrder(); // 左递归
if (this.right != null) this.right.preOrder(); // 右递归
}
@Override
public String toString() {
return "HuffmanTreeNode{" +
"value=" + weight +
'}';
}
/**
* Comparable 接口中的方法,在 Collections 调用 sort 时调用此方法。
*
* return this.value - o.value,升序排序
* return o.value - this.value,降序排序
*
* @param o 传入比较的对象
* @return 两个对象的大小,取值为 负数、0和正数
*/
@Override
public int compareTo(HuffmanTreeNode o) {
// 从小到大排序
return this.weight - o.weight;
}
}
创建哈夫曼树:
public class HuffmanTreeDemo {
public static void main(String[] args) {
int[] arr = {15, 4, 13, 7, 5, 2};
//HuffmanTreeNode root = createHuffmanTree(arr);
//root.preOrder();
// 1. 创建存储树结点的集合 nodes
List<HuffmanTreeNode> nodes = new ArrayList<>();
// 2. 将数组中的元素封装为树结点,并存入 nodes 集合
for (int num : arr) {
nodes.add(new HuffmanTreeNode(num));
}
// 3. 将 nodes 中的结点生成哈夫曼树
while (nodes.size() > 1) {
// 3.1 根据权值,从小到达进行排序
Collections.sort(nodes);
// 3.2 取出权值最小的结点作为左右孩子进行组装
HuffmanTreeNode left = nodes.get(0);
HuffmanTreeNode right = nodes.get(1);
HuffmanTreeNode temp = new HuffmanTreeNode(left.weight + right.weight);
temp.left = left;
temp.right = right;
// 3.3 将新组装的结点加入原集合中,删除已用的两个最小的结点
nodes.add(temp);
nodes.remove(left);
nodes.remove(right);
}
// 4. 当循环结束后,就生成了哈夫曼树,此时 nodes 中只剩下一个结点,即根结点
HuffmanTreeNode root = nodes.get(0);
// 5. 先序遍历验证结果
root.preOrder();
}
}
- 与动图结果一致。
3. 哈夫曼编码
- 对于二叉树而言,规定向左分支标记为0,向右的分支标记为1,从根节点到达叶子节点所经过分支构成的代码序列就是哈夫曼编码。
如上图所示中,A、B、C、D 的编码分别如下:
- A:0
- B:10
- C:110
- D:111
哈夫曼编码是一种前缀编码,任意两个叶子节点的编码不会重复且不会产生歧义。
示例:
对字符串进行编码和解码操作。
要求:
- 统计字符串中出现的每一个字符的出现次数。
- 将每个字符的出现次数作为权值,并将该字符与字符出现次数封装成树节点,生成哈夫曼树。
- 遍历哈夫曼树,生成哈夫曼编码表和哈夫曼解码表。
- 根据哈夫曼编码表对字符串进行编码,根据哈夫曼解码表对哈夫曼编码进行解码恢复原字符串。
定义树节点:
public class HuffmanEncodingNode implements Comparable<HuffmanEncodingNode> {
public Character data; // 存放具体的字符
public int weight; // 字符对应的权值
public HuffmanEncodingNode left;
public HuffmanEncodingNode right;
public HuffmanEncodingNode(Character data, int weight) {
this.data = data;
this.weight = weight;
}
@Override
public String toString() {
return "HuffmanEncodingNode{" +
"data=" + data +
", weight=" + weight +
'}';
}
@Override
public int compareTo(HuffmanEncodingNode o) {
return this.weight - o.weight;
//return o.weight - this.weight;
}
}
定义哈夫曼编码与解码功能的类:
public class HuffmanEncoding {
public Map<Character, String> huffmanCodeTable = new HashMap<>(); // 哈夫曼编码表
public StringBuilder huffmanCode = new StringBuilder(); // 拼接每个叶子结点的编码
/**
* 获取哈夫曼编码结果
*
* @param s 待编码的字符串
* @return 编码字符串
*/
public String getHuffmanCode(String s) {
// 1. 封装结点,以字符出现的次数作为权值,字符串作为数据
List<HuffmanEncodingNode> nodes = getNodes(s.toCharArray());
// 2. 将结点集合生成哈夫曼树并获取根结点
HuffmanEncodingNode root = createHuffmanTree(nodes);
// 3. 遍历哈夫曼树,生成哈夫曼编码表
createHuffmanCodeTable(root, "", huffmanCode);
// 4. 根据哈夫曼编码表,对原字符串进行编码,并返回结果。
return huffmanCoding(s.toCharArray());
}
/**
* 根据哈夫曼编码进行解码
*
* @param code 哈夫曼编码
* @return 解码后的字符串
*/
public String decode(String code) {
// 生成一个解码表,将哈夫曼编码表 huffmanCodeTable 的 key 与 value 置换
Map<String, Character> huffmanDecodeTable = new HashMap<>();
for (Map.Entry<Character, String> entry : huffmanCodeTable.entrySet()) {
huffmanDecodeTable.put(entry.getValue(), entry.getKey());
}
// 根据哈夫曼解码表,对哈夫曼编码进行解码
int l = 0;
int r = 0;
// 拼接解码后的字符
StringBuilder res = new StringBuilder();
while (r < code.length()) {
// substring,左闭右开,取 (l, r + 1)
Character c = huffmanDecodeTable.get(code.substring(l, r + 1));
if (c == null) { // 没有匹配到编码,右指针后移一位继续匹配
r += 1;
} else { // 匹配到编码,拼接当前字符
res.append(c);
r += 1; // 右指针后移以为,继续匹配
l = r; // 左指针也指向后指针,继续向后截取编码
}
}
return res.toString();
}
/**
* 统计字符串中出现的字符及该字符出现的次数
* <p>
* 将每个字符及其出现次数封装为树结点,data 存储字符,weight 存储出现次数。
*
* @param bytes 字符串转化的字节数组
* @return 封装为结点后的集合
*/
public List<HuffmanEncodingNode> getNodes(char[] bytes) {
// 1. 创建 ArrayList 存储哈夫曼树的结点
List<HuffmanEncodingNode> nodes = new ArrayList<>();
// 2. 创建存储字符及其出现的 Map 集合
Map<Character, Integer> counts = new HashMap<>();
// 3. 开始统计字符出现次数
for (Character b : bytes) {
counts.merge(b, 1, Integer::sum);
}
// 4. 将 Map 集合中的数据封装成结点,存入 List 中
for (Map.Entry<Character, Integer> entry : counts.entrySet()) {
nodes.add(new HuffmanEncodingNode(entry.getKey(), entry.getValue()));
}
// 5. 返回封装结果
return nodes;
}
/**
* 生成哈夫曼树
*
* @param nodes 待生成哈夫曼树的结点集合
* @return 哈夫曼树的根节点
*/
public HuffmanEncodingNode createHuffmanTree(List<HuffmanEncodingNode> nodes) {
while (nodes.size() > 1) {
// 1. 先升序排序集合
Collections.sort(nodes);
// 2. 取出最小的两个结点,作为左右孩子结点,组成新的二叉树结构
HuffmanEncodingNode left = nodes.get(0);
HuffmanEncodingNode right = nodes.get(1);
// 3. 组成新的结点,新节点不存数据。
HuffmanEncodingNode temp = new HuffmanEncodingNode(null, left.weight + right.weight);
temp.left = left;
temp.right = right;
// 4. 将新结点添加到结点数组中,并删除已用的两个结点。
nodes.add(temp);
nodes.remove(left);
nodes.remove(right);
}
// 5. 循环结束后,就生成了哈夫曼树,此时结点集合 nodes 中只剩下一个结点是根节点,返回即可。
return nodes.get(0);
}
/**
* 递归生成哈夫曼编码表
*
* @param node 当前待编码结点
* @param code 上个结点到当前结点的编码值,左孩子编码 0,右孩子编码 1
* @param huffmanCode 从根结点到当前结点的编码值
*/
private void createHuffmanCodeTable(HuffmanEncodingNode node, String code, StringBuilder huffmanCode) {
// 先存储上个结点到当前结点的编码值
StringBuilder temp = new StringBuilder(huffmanCode);
temp.append(code);
// 如果当前结点不为为 null 时,处理当前结点
if (node != null) {
// 非叶子结点,data == null,继续向后递归
if (node.data == null) {
// 左递归时,编码 0,并传入已编码好的路径
createHuffmanCodeTable(node.left, "0", temp);
// 右递归时,编码 1,并传入已编码好的路径
createHuffmanCodeTable(node.right, "1", temp);
} else {// 如果遇到叶子结点,就将当前结点的 data 与编码存入哈夫曼编码表 huffmanCodeTable
huffmanCodeTable.put(node.data, temp.toString());
}
}
}
/**
* 根据哈夫曼编码表对原字符串进行编码
*
* @param s 待编码字符串的字符数组
* @return 哈夫曼编码
*/
private String huffmanCoding(char[] s) {
StringBuilder code = new StringBuilder();
for (Character b : s) {
code.append(huffmanCodeTable.get(b));
}
return code.toString();
}
}
测试编码和解码操作:
public class TestDemo {
public static void main(String[] args) {
HuffmanEncoding demo = new HuffmanEncoding();
String s = "编码解码!";
System.out.println("原字符串:" + s);
// 哈夫曼编码
String huffmanCode = demo.getHuffmanCode(s);
System.out.println("编码值:" + huffmanCode);
// 打印当前编码表
System.out.println("当前编码表:" + demo.huffmanCodeTable);
// 解码
String decode = demo.decode(huffmanCode);
System.out.println("解码后的字符串:" + decode);
}
}