先了解几个概念:
- 树节点的带权路径长度为从该节点到树根之间的路径长度与节点上权的乘积。
- 树的带权路径长度为树中所有叶子节点的带权路径长度之和。
- 带权路径长度最小的二叉树称做最优二叉树或赫夫曼树。
构造赫夫曼树的通常方法:
1. 根据给定的 n 个权值 {
w1
,
w2
, …,
wn
}构成n棵二叉树的集合 F={
T1
,
T2
, …,
Tn
},其中每棵二叉树
Ti
中只有一个带权为
wi
的根节点,其左右节点均为空。
2. 在F中选取两棵根节点的权值最小的树作为左右子树构造一棵新的二叉树,且置新的二叉树的根节点的权值为其左右子树上根节点的权值之和。
3. 在F中删除这两棵树,同时将新得到的二叉树加入F中。
4. 重复(2)和(3),直到F只含一棵树为止。这棵树便是赫夫曼树。
霍夫曼编码:
霍夫曼编码(Huffman Coding)是一种编码方法,霍夫曼编码是可变字长编码(VLC)的一种。霍夫曼编码使用变长编码表对源符号(如文件中的一个字母)进行编码,其中变长编码表是通过一种评估来源符号出现机率的方法得到的,出现机率高的字母使用较短的编码,反之出现机率低的则使用较长的编码,这便使编码之后的字符串的平均长度、期望值降低,从而达到无损压缩数据的目的。
例如使用赫夫曼编码对文本进行编码:
每种字符在电文中出现的次数为
wi
, 编码长度为
li
,电文中只有n种字符。那么编码总长为
∑ni=0wili
。如果将
wi
,
li
分别认为是树的叶子节点的权重和路径长度,
∑ni=0wili
为二叉树上带权路径长度。设计电文总厂最短的二进制前缀编码即为n种字符出现的频率作权,设计一棵赫夫曼树的问题。
下面给出实现代码:
package huffman;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.Map.Entry;
/**
* @ClassName: Huffman
* @author xhf
* @date 2017年11月18日
* @Description: 霍夫曼编码问题
*/
public class Huffman {
/**
* @Fields rootNode : 赫夫曼树的根节点
*/
private Node rootNode;
/**
* @Fields nodeList : 存放统计后的节点
*/
private List<Node> nodeList = new LinkedList<>();
/**
* @Title: createHuffmanTree
* @param @return
* @return Node
* @throws
* @Description: 根据给定的字符出现的次数和字符信息构造霍夫曼树
*/
public void createHuffmanTree(String data) {
/**
* 统计字符串中字符出现的次数比重
*/
this.statistics(data);
List<Node> tempNodeList = new ArrayList<>();
tempNodeList.addAll(nodeList);
/**
* 根据霍夫曼树构造规则构造霍夫曼树
*/
while (tempNodeList.size() > 1) {
List<Node> minList = this.getMinList(tempNodeList);
Node minOne = minList.get(0);
Node minTwo = minList.get(1);
tempNodeList.remove(minOne);
tempNodeList.remove(minTwo);
Node newNode = new Node();
minOne.setCode((byte)0);
minTwo.setCode((byte)1);
newNode.setLeftChild(minOne);
minOne.setParent(newNode);
newNode.setRightChild(minTwo);
minTwo.setParent(newNode);
newNode.setWeight(minOne.getWeight() + minTwo.getWeight());
tempNodeList.add(newNode);
}
this.rootNode = tempNodeList.size() == 1 ? tempNodeList.get(0) : null;
}
/**
* @Title: statistics
* @param @param data
* @return void
* @throws
* @Description: 统计字符串中各字符出出现的次数
*/
public void statistics(String data) {
char[] datachars = data.toCharArray();
HashMap<Character, Integer> dataMap = new HashMap<>();
int sum = 0;
for (char c : datachars) {
if (dataMap.get(c) == null) {
dataMap.put(c, 1);
} else {
dataMap.put(c, dataMap.get(c)+1);
}
sum += 1;
}
for (Entry<Character, Integer> e : dataMap.entrySet()) {
Node node = new Node();
node.setData(e.getKey());
node.setWeight((double)e.getValue() / sum);
nodeList.add(node);
}
}
/**
* @Title: getMinList
* @param @param nodeList
* @param @return
* @return List<Node>
* @throws
* @Description: 获取当前集合中的最小的两个节点
*/
public List<Node> getMinList(List<Node> nodeList) {
List<Node> minList = new ArrayList<>();
Node minOne = null;
Node minTwo = null;
for (Node node : nodeList) {
if (minOne == null) {
minOne = node;
continue;
}
if (minTwo == null) {
if (node.getWeight() >= minOne.getWeight()) {
minTwo = node;
} else {
minTwo = minOne;
minOne = node;
}
continue;
}
{
if (node.getWeight() < minOne.getWeight()) {
minTwo = minOne;
minOne = node;
} else if (node.getWeight() < minTwo.getWeight()) {
minTwo = node;
}
continue;
}
}
minList.add(minOne);
minList.add(minTwo);
return minList;
}
/**
* @Title: decode
* @param @return
* @return String
* @throws
* @Description: 根据构造的霍夫曼树得到霍夫曼编码
*/
public String encode(String data) {
this.createHuffmanTree(data);
char[] chars = data.toCharArray();
StringBuffer sb = new StringBuffer();
for (char c : chars) {
for (Node n : nodeList) {
if (n.getData() == c) {
sb.append(this.getNodeCode(n));
}
}
}
return sb.toString();
}
/**
* @Title: decode
* @param @param code
* @param @return
* @return String
* @throws
* @Description: 根据霍夫曼树将赫夫曼编码转为初始文本
*/
public String decode(String code) {
if (rootNode == null)
return "";
char[] chars = code.toCharArray();
StringBuffer sb = new StringBuffer();
Node finder = rootNode;
for (char c : chars) {
if (c == '0') {
finder = finder.getLeftChild();
} else {
finder = finder.getRightChild();
}
if (finder.getLeftChild() == null) {
sb.append(finder.getData());
finder = rootNode;
}
}
return sb.toString();
}
/**
* @Title: getNodeCode
* @param @param n
* @param @return
* @return String
* @throws
* @Description: 从叶子节点向上遍历得到字符的编码
*/
public String getNodeCode(Node n) {
StringBuffer sb = new StringBuffer();
while (n != null && n.getCode() != null) {
sb.append(n.getCode());
n = n.getParent();
}
return sb.reverse().toString();
}
public static void main(String[] args) {
Huffman hm = new Huffman();
String code = hm.encode("System.out.println(decode);");
String decode = hm.decode(code);
System.out.println(code);
System.out.println(decode);
}
}
/**
* @ClassName: Node
* @author xhf
* @date 2017年11月18日
* @Description: 用于构造二叉树的节点
*/
class Node {
/**
* @Fields weight : 权重
*/
private Double weight;
/**
* @Fields code : 该节点的对应 0 1。左孩子0, 右孩子1
*/
private Byte code;
/**
* @Fields data : 节点中存储的字符
*/
private Character data;
/**
* @Fields leftChild : 左孩子
*/
private Node leftChild;
/**
* @Fields rightChild : 右孩子
*/
private Node rightChild;
/**
* @Fields parent : 父节点
*/
private Node parent;
public Double getWeight() {
return weight;
}
public void setWeight(Double weight) {
this.weight = weight;
}
public Byte getCode() {
return code;
}
public void setCode(Byte code) {
this.code = code;
}
public Character getData() {
return data;
}
public void setData(Character data) {
this.data = data;
}
public Node getLeftChild() {
return leftChild;
}
public void setLeftChild(Node leftChild) {
this.leftChild = leftChild;
}
public Node getRightChild() {
return rightChild;
}
public void setRightChild(Node rightChild) {
this.rightChild = rightChild;
}
public Node getParent() {
return parent;
}
public void setParent(Node parent) {
this.parent = parent;
}
}