简介
哈夫曼树的带权路径长度最小,故也称之为最优二叉树。
相关术语
①路径: 在一棵树中,一个结点到另一个结点之间的通路称为路径。如下图从根结点到结点a之间的通路就是一条路径。
②路径长度: 在一条路径中,每经过一个结点,路径长度都要加1。例如在一棵树中,规定根结点所在层数为1层,那么从根结点到第 i 层结点的路径长度为i-1 。如下图从根结点到结点c的路径长度为3。
③结点的权: 给每一个结点赋予一个新的数值,被称为这个结点的权。如下图中结点a的权为7,结点b的权为5。
④结点的带权路径长度: 从根结点到该结点之间的路径长度与该结点的权的乘积。如下图中结点b的带权路径长度为 2*5=10 。
树的带权路径长度为树中所有叶子结点的带权路径长度之和,通常记作WPL。
如下图树的带权路径长度为:WPL = 7 * 1 + 5 * 2 + 2 * 3 + 4 * 3
构建过程
①所有带权值的结点按权值从小到大排列。
②依次选取权值最小的结点放在树的底部,权值小的在左边(取出的结点相当于从这些结点的集合中剔除)。
③生成一个新节点作为这两个结点的父节点,且父节点的权值等于这两个结点权值之和,然后要把这个新结点放回我们需要构成树的结点中,继续进行排序。
④重复上述2、3步骤,直至全部节点形成一棵树,此树便是哈夫曼树,最后生成的结点即为根节点。这样构成的哈夫曼树,所有的存储有信息的结点都在叶子结点上。
哈夫曼编码
从根节点出发,每个父节点都会有分支,现在给左右分支各赋予一个数值,左分支表示0,右分支表示1。当从根节点一直走到叶子结点过程中所经历的左右分支以0、1串表示,每个叶子结点会形成一个特定的编码,这个就是哈夫曼编码,如上图中四个叶子结点的编码分别为:
a:0
b:10
c:110
d:111
在电文传输中,需要将电文中出现的每个字符进行二进制编码。在设计编码时需要遵守两个原则:
①发送方传输的二进制编码,到接收方解码后必须具有唯一性,即解码结果与发送方发送的电文完全一样;
②发送的二进制编码尽可能地短。
下面介绍两种编码方式。
1 等长编码
这种编码方式的特点是每个字符的编码长度相同(编码长度就是每个编码所含的二进制位数)。假设字符集只含有4个字符A,B,C,D,用二进制两位表示的编码分别为00,01,10,11。若现在有一段电文为:ABACCDA,则应发送二进制序列:00010010101100,总长度为14位。当接收方接收到这段电文后,将按两位一段进行译码。这种编码的特点是译码简单且具有唯一性,但编码长度并不是最短的。
2.不等长编码
在传送电文时,为了使其二进制位数尽可能地少,可以将每个字符的编码设计为不等长的,使用频度较高的字符分配一个相对比较短的编码,使用频度较低的字符分配一个比较长的编码。例如,可以为A,B,C,D四个字符分别分配0,00,1,01,并可将上述电文用二进制序列:000011010发送,其长度只有9个二进制位,但随之带来了一个问题,接收方接到这段电文后无法进行译码,因为无法断定前面4个0是4个A,1个B、2个A,还是2个B,即译码不唯一,因此这种编码方法不可使用。
因此,为了设计长短不等的编码,以便减少电文的总长,还必须考虑编码的唯一性,即在建立不等长编码时必须使任何一个字符的编码都不是另一个字符的前缀。 这个问题可以采用哈夫曼编码解决。
完整源码
/**
* 节点类
*/
public class Node implements Comparable<Node>{
private Node leftChild = null;
private Data data = null;
private Node rightChild = null;
public Node getLeftChild() {
return leftChild;
}
public void setLeftChild(Node leftChild) {
this.leftChild = leftChild;
}
public Data getData() {
return data;
}
public void setData(Data data) {
this.data = data;
}
public Node getRightChild() {
return rightChild;
}
public void setRightChild(Node rightChild) {
this.rightChild = rightChild;
}
@Override
public String toString() {
return "Node [leftChild=" + leftChild + ", data=" + data
+ ", rightChild=" + rightChild + "]";
}
@Override
public int compareTo(Node o) {
return this.data.compareTo(o.getData());
}
}
/**
* Data用于存储一个字符及其出现的次数
*/
public class Data implements Comparable<Data>{
// 字符
private char c = 0;
// 字符出现的次数
private int frequency = 0;
public char getC() {
return c;
}
public void setC(char c) {
this.c = c;
}
public int getFrequency() {
return frequency;
}
public void setFrequency(int frequency) {
this.frequency = frequency;
}
@Override
public String toString() {
return "Data [c=" + c + ", frequency=" + frequency + "]";
}
@Override
public int compareTo(Data o) {
if (this.frequency < o.getFrequency()) {
return -1;
} else if (this.frequency > o.getFrequency()) {
return 1;
} else {
return 0;
}
}
}
/**
* 对字符串编码后的结果:包括编码后的字符串和字符/编码对
*/
public class EncodeResult {
// 字符串编码后的结果
private String encode;
// 字符编码对
private Map<Character, String> letterCode;
public EncodeResult(String encode, Map<Character, String> letterCode) {
super();
this.encode = encode;
this.letterCode = letterCode;
}
public String getEncode() {
return encode;
}
public Map<Character, String> getLetterCode() {
return letterCode;
}
}
// 编解码接口类
public interface HuffmanAlgorithm {
/**
* 编码字符串。
* @param str 指定的需要编码的字符串
* @return 编码结果
*/
public EncodeResult encode(String str);
/**
* 根据编码结果返回原来的字符串。
* @param decodeResult 原来字符串的编码结果。
* @return 解码出来的字符串。
*/
public String decode(EncodeResult encodeResult);
}
// 编解码抽象算法类
public abstract class HuffmanAlgorithmAbstract implements HuffmanAlgorithm {
@Override
public EncodeResult encode(String str) {
ArrayList<Node> letterList = toList(str);
Node rootNode = createTree(letterList);
Map<Character, String> letterCode = getLetterCode(rootNode);
EncodeResult result = encode(letterCode, str);
return result;
}
/**
* 把一个字符串转化为节点列表
* @param letters
* @return
*/
private ArrayList<Node> toList(String letters) {
ArrayList<Node> letterList = new ArrayList<Node>();
Map<Character, Integer> ci = new HashMap<Character, Integer>();
for (int i = 0; i < letters.length(); i++) {
Character character = letters.charAt(i);
if (!ci.keySet().contains(character)) {
ci.put(character, 1);
} else {
Integer oldValue = ci.get(character);
ci.put(character, oldValue + 1);
}
}
Set<Character> keys = ci.keySet();
for (Character key : keys) {
Node node = new Node();
Data data = new Data();
data.setC(key);
data.setFrequency(ci.get(key));
node.setData(data);
letterList.add(node);
}
return letterList;
}
protected abstract Node createTree(ArrayList<Node> letterList);
/**
* 编码字符串。
* @param letterCode 字符/编码对集合。
* @param letters 指定的需要编码的字符串。
* @return 编码结果
*/
private EncodeResult encode(Map<Character, String> letterCode, String letters) {
StringBuilder encode = new StringBuilder();
for (int i = 0, length = letters.length(); i < length; i++) {
Character character = letters.charAt(i);
encode.append(letterCode.get(character));
}
EncodeResult result = new EncodeResult(encode.toString(), letterCode);
return result;
}
/**
* 获得所有字符编码对
*
* @param rootNode哈夫曼树的根节点
* @return 所有字符编码对
*/
private Map<Character, String> getLetterCode(Node rootNode) {
Map<Character, String> letterCode = new HashMap<Character, String>();
// 处理只有一个节点的情况
if (rootNode.getLeftChild() == null && rootNode.getRightChild() == null) {
letterCode.put(rootNode.getData().getC(), "1");
return letterCode;
}
getLetterCode(rootNode, "", letterCode);
return letterCode;
}
/**
* 先序遍历哈夫曼树,获得所有字符编码对。
*
* @param rooNode 哈夫曼树根结点
* @param suffix 编码前缀,也就是编码这个字符时,之前路径上的所有编码
* @param letterCode 用于保存字符编码结果
*/
private void getLetterCode(Node rooNode, String suffix,
Map<Character, String> letterCode) {
if (rooNode != null) {
if (rooNode.getLeftChild() == null
&& rooNode.getRightChild() == null) {
Character character = rooNode.getData().getC();
letterCode.put(character, suffix);
}
getLetterCode(rooNode.getLeftChild(), suffix + "0", letterCode);
getLetterCode(rooNode.getRightChild(), suffix + "1", letterCode);
}
}
public String decode(EncodeResult decodeResult) {
// 解码得到的字符串
StringBuffer decodeStr = new StringBuffer();
// 获得解码器
Map<String, Character> decodeMap = getDecoder(decodeResult
.getLetterCode());
// 解码器键集合
Set<String> keys = decodeMap.keySet();
// 待解码的(被编码的)字符串
String encode = decodeResult.getEncode();
// 从最短的开始匹配之所以能够成功,是因为哈夫曼编码的唯一前缀性质
// 临时的可能的键值
String temp = "";
// 改变temp值大小的游标
int i = 1;
while (encode.length() > 0) {
temp = encode.substring(0, i);
if (keys.contains(temp)) {
Character character = decodeMap.get(temp);
decodeStr.append(character);
encode = encode.substring(i);
i = 1;
} else {
i++;
}
}
return decodeStr.toString();
}
/**
* 获得解码器,也就是通过字母/编码对得到编码/字符对。
*
* @param letterCode
* @return
*/
private Map<String, Character> getDecoder(Map<Character, String> letterCode) {
Map<String, Character> decodeMap = new HashMap<String, Character>();
Set<Character> keys = letterCode.keySet();
for (Character key : keys) {
String value = letterCode.get(key);
decodeMap.put(value, key);
}
return decodeMap;
}
}
/**
* 算法实现参考《多媒体技术教程》
*/
public class HuffmanAlgorithmImpl1 extends HuffmanAlgorithmAbstract {
/*
* 创建哈夫曼树; 丢失了letterList中的数据,深拷贝letterList是需要完善的地方
*/
@Override
protected Node createTree(ArrayList<Node> letterList) {
init(letterList);
while (letterList.size() != 1) {
int size = letterList.size();
// 小的节点放在右边(眼睛看到的左边)
Node nodeLeft = letterList.get(size - 1);
Node nodeRight = letterList.get(size - 2);
Node nodeParent = new Node();
nodeParent.setLeftChild(nodeLeft);
nodeParent.setRightChild(nodeRight);
Data data = new Data();
data.setFrequency(nodeRight.getData().getFrequency()
+ nodeLeft.getData().getFrequency());
nodeParent.setData(data);
letterList.set(size - 2, nodeParent);
letterList.remove(size - 1);
sort(letterList);
}
Node rootNode = letterList.get(0);
return rootNode;
}
/**
* 初始化 让节点列表有序
*/
private void init(ArrayList<Node> letterList) {
sort(letterList);
}
/**
* 冒泡排序,把小的放在最后
*/
private void sort(ArrayList<Node> letterList) {
int size = letterList.size();
// 处理只有一个元素的情况,也就是说,不需要排序
if (size == 1) {
return;
}
for (int i = 0; i < size; i++) {
for (int j = 0; j < size - 1 - i; j++) {
if (letterList.get(j).getData().getFrequency() < letterList
.get(j + 1).getData().getFrequency()) {
Node tempNode = letterList.get(j);
letterList.set(j, letterList.get(j + 1));
letterList.set(j + 1, tempNode);
}
}
}
}
}
public class HuffmanAlgorithmImpl1Test {
@Test
public void testEncodeString() {
HuffmanAlgorithmImpl1 huffmanImpl1 = new HuffmanAlgorithmImpl1();
EncodeResult result = huffmanImpl1.encode("abcdda");
System.out.println(result.getEncode());
}
@Test
public void testDecode() {
HuffmanAlgorithmImpl1 huffmanImpl1 = new HuffmanAlgorithmImpl1();
EncodeResult result = huffmanImpl1.encode("abcdda");
String decode = huffmanImpl1.decode(result);
System.out.println(decode);
}
}
参考:
https://blog.csdn.net/l294265421/article/details/44778989
https://blog.csdn.net/u013728021/article/details/84036051