声明:以下是学的尚硅谷网课并结合网上资料所记的笔记。可能会有一些错误,发现了会修改。
赫夫曼树
概念:给定n个权值作为n个叶子节点,构造一颗二叉树,若该树的带权路径长度达到最小,称这样的树为最优二叉树,也叫赫夫曼树(哈夫曼树)。它是带权路径长度最短的树,权值较大的结点离根较近。
名词解释:
- 路径:一棵树中,从一个结点往下可以达到的子结点,或孙子结点的通路,称为路径。
- 路径长度:通路中分支的数目称为路径长度,根节点到第L层结点的路径长度为 L-1 。
- 权:树中结点的值。
- 结点带权路径长度:从根结点到该结点之间路径长度与该结点的权的乘积。
- 树的带权路径长度:树的带权路径长度规定为所有叶子结点的带权路径长度之和,记为WPL,权值越大的结点离根结点越近的二叉树是最优二叉树,即WPL最小的就是赫夫曼树。
赫夫曼树构建思路:
- 从小到大进行排序,每个数据都是一个结点,每个节点可以看成是一颗简单的二叉树。
- 取出根结点权值最小的两颗二叉树。
- 组成一颗新的二叉树,该新的二叉树的根结点权值是前面两颗二叉树根结点权值之和。
- 再将这颗新的二叉树,以根结点的权值大小再次进行排序,重复1,2,3,4步,直到数列中所有的数据都被处理,就得到一颗赫夫曼树。
例如:对于数组{13,7,8,3,29,6,1},变换过程如下:
赫夫曼编码
特点:
- 是一种编码方式,属于一种程序算法,是无损压缩。
- 是赫夫曼树在电讯通信中的经典应用。
- 广泛应用于数据文件压缩,压缩率通常20%~90%之间。
- 是可变字长编码的一种。
步骤:
- 先统计待压缩字符串的每个字符出现的次数。
- 将每个字符出现的次数表示为权值,构建赫夫曼树(字符当做每个结点的属性)。
- 根据赫夫曼树,给各个字符规定编码,向左的路径为0,向右的路径为1。
- 将字符串编码成赫夫曼编码。
注: 可看出赫夫曼编码为一种前缀编码方式,因为字符集中任一字符的编码都不是其他字符编码的前缀。出现频率高的字符,也就是权值大的编码比较短,出现频率低的也就是权值小的编码比较长,体现了赫夫曼树的思想,用于数据压缩。赫夫曼树根据排序方式不同,也可能不太一样(当有相同权值的字符时),这样其赫夫曼编码也不同,但WPL一样,即带权路径长度最小。
代码如下:
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Set;
public class HuffmanCode {
public static void main(String[] args) {
String content = "i like like like java do you like a java";
byte[] contentBytes = content.getBytes();
//System.out.println(contentBytes.length); // 40
List<Node> nodes = getNodes(contentBytes);
System.out.println("nodes = " + nodes);
// 测试,创建的二叉树
System.out.println("生成的赫夫曼树");
Node huffmanTreeRoot = createHuffmanTree(nodes);
HuffmanCode.preOrder(huffmanTreeRoot);
// 测试,是否生成了对应的赫夫曼编码
HuffmanCode.getCodes(huffmanTreeRoot);
System.out.println("生成的赫夫曼编码表: " + huffmanCodes);
}
/**
* @param contentBytes接受字节数组
* @return 返回一个List集合,形式[Node[data=97,weight=5],Node[data=32,weight=9]...]
*/
private static List<Node> getNodes(byte[] contentBytes) {
// 创建一个ArrayList
ArrayList<Node> nodes = new ArrayList<Node>();
// 遍历bytes,统计每一个byte出现的次数->map[key,value]
Map<Byte, Integer> counts = new HashMap<Byte, Integer>();
for (byte b : contentBytes) {
Integer count = counts.get(b);
if (count == null) {
counts.put(b, 1);
} else {
counts.put(b, count + 1);
}
}
// 把每一个键值对转成一个Node对象,并加入到nodes集合
// 遍历map
Set<Byte> keySet = counts.keySet();
Iterator<Byte> it = keySet.iterator();
while(it.hasNext()) {
byte key = it.next();
Integer value = counts.get(key);
nodes.add(new Node(key,value));
}
return nodes;
//另一种遍历方法
//for (Map.Entry<Byte, Integer> entry : counts.entrySet()) {
// nodes.add(new Node(entry.getKey(), entry.getValue()));
//}
//return nodes;
}
// 通过List 创建对应的赫夫曼树
private static Node createHuffmanTree(List<Node> nodes) {
while (nodes.size() > 1) {
// 排序,从小到大
Collections.sort(nodes);
// 取出第一个和第二个节点
Node leftNode = nodes.get(0);
Node rightNode = nodes.get(1);
// 创建一个新的二叉树,此时的根节点没有data(字符),只有权值
Node parent = new Node(null, leftNode.weight + rightNode.weight);
parent.left = leftNode;
parent.right = rightNode;
// 将处理的两个节点从nodes中删除
nodes.remove(leftNode);
nodes.remove(rightNode);
// 将新的节点加入到nodes
nodes.add(parent);
}
// nodes最后只剩下一个节点,即赫夫曼树的根节点
return nodes.get(0);
}
// 前序遍历的方法
private static void preOrder(Node root) {
if (root != null) {
root.preOrder();
} else {
System.out.println("root节点为空!");
}
}
/*
* 生成赫夫曼对应的赫夫曼编码 思路:
* 1.将赫夫曼编码表存放到Map<Byte,String>,形式如:32(表示空格)->01, 97(表示a字符)->100等
* {32=01,97=100, 100=11000, 117=11001, 101=1110, 118=11011, 105=101, 121=11010, 106=0010, 107=1111, 108=000, 111=0011}
* 2.在生成赫夫曼编码表时,需要去拼接路径,定义一个StringBuilder存储某个叶子节点的路径
*/
static Map<Byte, String> huffmanCodes = new HashMap<Byte