哈夫曼树
哈夫曼树又称最优二叉树,是一种带权路径长度最短的二叉树。所谓树的带权路径长度,就是树中所有的叶结点的权值乘上其到根结点的路径长度(若根结点为0层,叶结点到根结点的路径长度为叶结点的层数)。
带权路径总和最小的就是哈夫曼树。即 WPL=叶子节点权*路径+…+叶子节点权*路径
中间的树就是哈夫曼树。
. 创建哈夫曼树
思路分析:
- 将数列从小到大排序,此时每个数据就是一个节点
- 取出前两个节点,作为子节点,计算出父节点的权值(就是两个节点的权值和)
- 下一步就是将计算出的新父节点的权值放入数列中,重新排序,返回第二步
- 往复,最终会得到一个哈夫曼树
public class HuffmanTree {
public static void main(String[] args) {
int arr[] ={3,6,15,20};
Node huffmanTree = createHuffmanTree(arr);
if (huffmanTree!=null){
huffmanTree.preOrder();
}
}
public static Node createHuffmanTree(int[] arr){
// 创建一个集合,存入创建的节点
List<Node> nodeList = new ArrayList<>();
for (int item : arr) {
nodeList.add(new Node(item));
}
// 因为每次都会remove一些节点,最终会在list中剩下一个节点,这个节点就是根节点
while (nodeList.size() > 1){
// 从小到达排序list
Collections.sort(nodeList);
// 取出前两个最小的,第一个作为左节点,第二个作为右节点
Node leftNode = nodeList.get(0);
Node rightNode = nodeList.get(1);
// 将权重+路径和赋值给父节点,将父节点的左右节点挂上
Node parentNode = new Node(leftNode.getValue()+rightNode.getValue());
parentNode.setLeft(leftNode);
parentNode.setRight(rightNode);
// 移除最小的两个节点,将父节点放入list集合中,进行下一轮
nodeList.remove(leftNode);
nodeList.remove(rightNode);
nodeList.add(parentNode);
}
// 返回最终的根节点
return nodeList.get(0);
}
}
class Node implements Comparable<Node>{
private int value;
private Node left;
private Node right;
public Node(int value) {
this.value = value;
}
public int getValue() {
return value;
}
public void setValue(int value) {
this.value = value;
}
public Node getLeft() {
return left;
}
public void setLeft(Node left) {
this.left = left;
}
public Node getRight() {
return right;
}
public void setRight(Node right) {
this.right = right;
}
@Override
public String toString() {
return "Node{" +
"value=" + value +
'}';
}
// 从小到大排序
@Override
public int compareTo(Node node) {
return this.value - node.value;
}
// 前序遍历
public void preOrder(){
System.out.println(this.toString());
if (this.left!=null){
this.left.preOrder();
}
if (this.right!=null){
this.right.preOrder();
}
}
}
结果:
Node{value=44}
Node{value=20}
Node{value=24}
Node{value=9}
Node{value=3}
Node{value=6}
Node{value=15}
哈夫曼编码
哈夫曼编码的优点:
- 能够压缩20%~90%的数据
- 无损压缩
- 此编码满足前缀编码,不会造成匹配多义性
需要注意的是:
- 在数据排序时不稳定而结果会造成最终的编码结果不同,但是编码后的长度是一样的。
- 数据重复不多,压缩效果不明显。
. 思路分析
这里有一串数据:“i love love love java you”
-
先统计数据中每个字符出现的次数,即:
i:1 l:3 o:4 v:4 e:3 j:1 a:2 y:1 u:1 :5
i有1个,l有3个,o有4个,v有4个,e有3个,j有1个,a有2个,y有1个,u有1个,空格有5个
-
然后个数就是权重,根据次构建一个哈夫曼树
-
通过创建成功的哈夫曼树,到达每个节点的路径就是该字母的编码,从根节点出发,往左走就是0,往右走就是1
-
将遍历哈夫曼树,得出每个叶子节点也就是每个字符的哈夫曼编码,存储到map集合中形成编码表,然后字符串根据编码表找到对应的编码字符串,每8位一组存储到byte数组中,形成编码后的结果。
-
解码,将byte数组,每一个数转换成8位的二进制字符串,扫描字符串依次与哈夫曼编码表的反转map比较,找到对应的字节输出。
. 代码实现
public class HuffmanCode {
public static void main(String[] args) {
String content = "i love love love java you";
System.out.println("字符串:"+content);
byte[] encode = encode(content);
System.out.println("压缩后得到的哈夫曼编码字节数组:"+Arrays.toString(encode));
byte[] decode = decode(encode);
System.out.println("解码后:"+new String(decode));
}
// 获得一个哈夫曼树的根节点
public static CodeNode getHuffmanTree(byte[] contentBytes){
// 将字符串转换成字节,统计每个字节出现的次数存储到map中
Map<Byte, Integer> contentMap = new HashMap<>();
for (byte contentByte : contentBytes) {
if (contentMap.get(contentByte) == null){
contentMap.put(contentByte,1);
}else {
contentMap.put(contentByte,contentMap.get(contentByte)+1);
}
}
// 遍历map,每个entry创建一个codeNode节点,存储到list中便于创建哈夫曼树
List<CodeNode> codeNodeList = new ArrayList<>();
for(Map.Entry<Byte,Integer> mapEntry: contentMap.entrySet()){
codeNodeList.add(new CodeNode(mapEntry.getKey(),mapEntry.getValue()));
}
// 创建哈夫曼树
while (codeNodeList.size() > 1){
Collections.sort(codeNodeList);
CodeNode leftNode = codeNodeList.get(0);
CodeNode rightNode = codeNodeList.get(1);
CodeNode parentNode = new CodeNode(null,leftNode.getWeight()+rightNode.getWeight());
parentNode.setLeft(leftNode);
parentNode.setRight(rightNode);
codeNodeList.remove(leftNode);
codeNodeList.remove(rightNode);
codeNodeList.add(parentNode);
}
return codeNodeList.get(0);
}
// 根据哈夫曼树,左路为0,右路为1,获得编码表
static Map<Byte, String> codeMap = new HashMap<>();
public static Map<Byte,String> getCodeMap(CodeNode node, String code, StringBuilder stringBuilder){
StringBuilder strBuilder = new StringBuilder(stringBuilder);
strBuilder.append(code);
if (node != null){
if (node.getData() == null){
// 向左
getCodeMap(node.getLeft(),"0",strBuilder);
// 向右
getCodeMap(node.getRight(),"1",strBuilder);
}else {
codeMap.put(node.getData(),strBuilder.toString());
}
}
return codeMap;
}
// codeSize记录编码字符串的长度,用于解决编码后最后一个数存在01110这种类似情况的编码
// 如果存在这种01110编码,在转为byte时存入14这个数字
// 而在解码的时候,因为14是数组的最后一个,在转为二进制字符串如果直接追加1110,在对照编码表的时候
// 会出错,因此需要判断在追加1110后,解析后的编码长度是不是加密时的编码长度一样,
// 如果不一样就需要在1110前面追加0,直到与加密时的长度一致,才可以解析成功
static int codeSize = 0;
// 获取字符串的最终编码
public static byte[] encode(String content){
byte[] contentBytes = content.getBytes();
// 创建哈夫曼树
CodeNode huffmanTree = getHuffmanTree(contentBytes);
// 获得哈夫曼树的编码表
Map<Byte, String> codeMap = getCodeMap(huffmanTree, "", new StringBuilder());
// 获得字符串的编码字符串
StringBuilder encodeBuilder = new StringBuilder();
for (byte contentByte : contentBytes) {
encodeBuilder.append(codeMap.get(contentByte));
}
// 将编码字符串8位一组存入byte数组中
String encodeString = encodeBuilder.toString();
codeSize = encodeString.length(); // 存储编码有多少位
// System.out.println(encodeString);
// 输出:101010001011111111001000101111111100...
// 获取前8位,10101000字符串转换成字节
// Integer.parseInt("10101000",2):将10101000以2进制为基准解析成十进制的int类型,
// 这里的10101000为补码,反码为10101000-1=10100111,正码为11011000,最高位为符号位得出-88
int size = (encodeString.length()+7)/8;
byte[] codeBytes = new byte[size];
int count = 0;
for (int i = 0; i < codeBytes.length; i++) {
if (count+8 < encodeString.length()){
codeBytes[i] = (byte) Integer.parseInt(encodeString.substring(count,count+8),2);
count+=8;
}else {
codeBytes[i] = (byte) Integer.parseInt(encodeString.substring(count),2);
}
}
return codeBytes;
}
// 反转编码表,用于解析使用
public static Map<String,Byte> getReCodeMap(){
Map<String, Byte> reCodeMap = new HashMap<>();
for (Map.Entry<Byte, String> entry : codeMap.entrySet()) {
reCodeMap.put(entry.getValue(),entry.getKey());
}
return reCodeMap;
}
public static byte[] decode(byte[] encode){
StringBuilder decodeBuilder = new StringBuilder();
Map<String, Byte> reCodeMap = getReCodeMap();
// 获取哈夫曼编码字符串
for (int i = 0; i < encode.length; i++) {
if (encode[i] < 0){
String str = Integer.toBinaryString(encode[i]);
decodeBuilder.append(str.substring(str.length()-8));
}else if (i != encode.length-1){
// 如果是正数并且不是数组最后一位数,需要给正数补位,用256与运算补位即可
int temp = encode[i] | 256;
// 256的二进制是1 0000 0000与正数与运算
// 比如256与77二进制100 1101与运算 得到1 0100 1101,截取8位即可
String str = Integer.toBinaryString(temp);
decodeBuilder.append(str.substring(str.length()-8));
}else {
// 最后一位不需要转换成8位数,直接添加到末尾即可
String lastCode = Integer.toBinaryString(encode[i]);
// 判断最后一个数转成二进制代码后,解析二进制编码总长度是否与加密时候一致
if (codeSize != (i*8+lastCode.length())){
// 不一致,就在最后一个数前面加差数个的0字符串,然后重置最后一个编码
int gap = codeSize- (i*8+lastCode.length());
String str = "";
for (int j = 0; j < gap; j++) {
str+="0";
}
lastCode = str + lastCode;
}
decodeBuilder.append(lastCode);
}
}
// System.out.println(decodeBuilder.toString());
// 扫描字符串进行解析
String decodeStr = decodeBuilder.toString();
List<Byte> decodeList = new ArrayList<>();
for (int i = 0; i < decodeStr.length(); ) {
int count = 1;
while (true){
String sub = decodeStr.substring(i, i + count);
Byte aByte = reCodeMap.get(sub);
if (aByte!=null){
decodeList.add(aByte);
break;
}else {
count++;
}
}
i = i+count;
}
byte[] decode = new byte[decodeList.size()];
int index = 0;
for (Byte aByte : decodeList) {
decode[index++] = aByte;
}
return decode;
}
}
// 创建节点
class CodeNode implements Comparable<CodeNode>{
private Byte data; // 数据
private int weight; // 数据在content中出现的次数,权重
private CodeNode left;
private CodeNode right;
public CodeNode(Byte data, int weight) {
this.data = data;
this.weight = weight;
}
public Byte getData() {
return data;
}
public void setData(Byte data) {
this.data = data;
}
public int getWeight() {
return weight;
}
public void setWeight(int weight) {
this.weight = weight;
}
public CodeNode getLeft() {
return left;
}
public void setLeft(CodeNode left) {
this.left = left;
}
public CodeNode getRight() {
return right;
}
public void setRight(CodeNode right) {
this.right = right;
}
@Override
public String toString() {
return "CodeNode{" +
"data=" + data +
", weight=" + weight +
'}';
}
@Override
public int compareTo(CodeNode o) {
return this.weight - o.weight;
}
public void preOrder(){
System.out.println(this.toString());
if (this.left!=null){
this.left.preOrder();
}
if (this.right!=null){
this.right.preOrder();
}
}
}
结果:
字符串:i love love love java you
压缩后得到的哈夫曼编码字节数组:[-7, 53, 100, -43, -109, 86, 47, 94, 19, 30]
解码后:i love love love java you