目录
- 什么是哈夫曼树?
- 如何构造哈夫曼树?
- 构造过程
- 代码实现
- 哈夫曼树的结构
- 构建哈夫曼树并计算WPL值
- 测试代码
- 什么是哈夫曼编码?
- 如何构建哈夫曼编码?
- 构建过程
- 代码实现
什么是哈夫曼树?
哈夫曼树又称为最优树,是一类带权路径长度最短的树,在实际中有着广泛的应用。介绍哈夫曼树之前,我们需要了解下面几个概念:
路径:从树中的一个节点到另一个节点之间的分支构成这两个节点之间的路径。
路径长度:路径上的分支数目称为路径长度。
树的路径长度:从树根到每一节点的路径长度之和。
权:赋予某个实体的一个量,是对实体的某个属性或某些属性值的描述。
节点的带权路径长度:从该节点到树根之间的路径长度乘上权。
树的带权路经长度:树中所有叶子节点带权路径长度之和,记为 W P L = ∑ k = 1 n w k l k WPL=\sum\limits_{k=1}\limits^{n}w_kl_k WPL=k=1∑nwklk。
哈夫曼树:对于带有不同权值的节点,能够构造出的WPL最小的二叉树称为哈夫曼树。
- 例如下图中 a a a, b b b, c c c, d d d 四个叶子节点权值分别为:7、5、2、4。不同的构造方法会使得 W P L WPL WPL 不同。
如何构造哈夫曼树?
构造过程
- 以下图为例:给定了 a a a, b b b, c c c, d d d, e e e 五个节点,权值分别为7、5、5、2、4。我们将按照下述步骤构造哈夫曼树:
-
从给定的节点中选取权值最小的两个节点作为左右孩子构成一棵新的二叉树,新构成的二叉树的权值为其左右孩子权值之和。
-
从序列中删除用掉的这两个节点,并把新构成的树加入到序列中。
-
重复上述步骤1和步骤2,直到序列中只剩下一棵树,这棵树就是哈夫曼树。
- 经计算得到构造出的哈夫曼树 W P L = 52 WPL=52 WPL=52。这样的构造方法称为哈夫曼算法。
代码实现
哈夫曼树的结构
- 哈夫曼树是二叉树的一种,因此我们可以参考二叉排序树的构造方法创建一个哈夫曼树的节点类。详情可见:二叉排序树详解并通过Java代码实现
public class HTNode {
int weight; // 权值
int parent; //父亲节点
int lChild; // 左孩子
int rChild; // 右孩子
@Override
public String toString() {
return "HTNode{" +
"weight=" + weight +
", parent=" + parent +
", lChild=" + lChild +
", rChild=" + rChild +
'}';
}
}
构建哈夫曼树并计算WPL值
-
构造哈夫曼树主要分为两步:
- 初始化:申请
2n
个单元,从1号单元开始存放节点,1-n
的位置存放叶子节点,之后的n-1
个位置存放其余非叶子节点。 - 创建树:通过
n-1
次循环的选择、删除、合并的操作来完成。从当前森林中选择父亲节点为0且权值最小的两个节点,将其父亲节点改为非0并合并,合并后的节点存入第n+1
之后的单元中,记录其左右孩子的下标。
- 初始化:申请
-
计算 W P L WPL WPL 值时,我们从叶子节点开始遍历,直到父亲节点为0为止,记录下路径长度,并乘上叶子节点对应的权值,最后求和即可求得 W P L WPL WPL 值。
public class HuffmanTree {
// 选择两个最小的节点
public int[] select(HTNode[] htNodes, int n) {
int[] res = new int[2];
// 从下标1开始遍历数组
int i = 1;
// 找到还没参与构建树的节点
while (htNodes[i].parent != 0 && i <= n) {
++ i;
}
res[0] = i;
++ i;
while (htNodes[i].parent != 0 && i <= n) {
++ i;
}
// 对两个节点进行比较,res[0]是小的
if (htNodes[i].weight < htNodes[res[0]].weight) {
res[1] = res[0];
res[0] = i;
} else {
res[1] = i;
}
// 后续节点和已经存入的两个节点去比较
for (int j = i + 1; j <= n; j ++) {
// 如果有父亲节点,说明已经参与构建,则直接跳过
if (htNodes[j].parent != 0) {
continue;
}
// 如果比小的还小
if (htNodes[j].weight < htNodes[res[0]].weight) {
res[1] = res[0];
res[0] = j;
// 如果介于两者之间
} else if (htNodes[j].weight < htNodes[res[1]].weight) {
res[1] = j;
}
}
return res;
}
// 创建哈夫曼树
public HTNode[] createHuffmanTree(int[] weights) {
// 初始化
int n = weights.length;
if (n <= 1) {
return new HTNode[0];
}
HTNode[] htNodes = new HTNode[2 * n];
for (int i = 1; i <= n; i ++) {
htNodes[i] = new HTNode();
htNodes[i].weight = weights[i - 1];
}
// 构建哈夫曼树
for (int i = n + 1; i < 2 * n; i ++) {
htNodes[i] = new HTNode();
// 选择
int[] min = select(htNodes, i - 1);
// 删除合并
htNodes[min[0]].parent = i;
htNodes[min[1]].parent = i;
// 存放
htNodes[i].lChild = min[0];
htNodes[i].rChild = min[1];
htNodes[i].weight = htNodes[min[0]].weight + htNodes[min[1]].weight;
}
return htNodes;
}
// 计算构建出的哈夫曼树的WPL值
public int calculateWPL(int[] weights) {
HTNode[] htNodes = createHuffmanTree(weights);
int sum = 0;
for (int i = 1; i <= weights.length; i ++) {
int cnt = 0;
int index = i;
while (htNodes[index].parent != 0) {
cnt ++;
index = htNodes[index].parent;
}
cnt *= htNodes[i].weight;
sum += cnt;
}
return sum;
}
}
测试代码
public class Test {
public static void main(String[] args) {
int[] weights = new int[5];
weights[0] = 7;
weights[1] = 5;
weights[2] = 5;
weights[3] = 2;
weights[4] = 4;
HuffmanTree huffman = new HuffmanTree();
HTNode[] htNodes = huffman.createHuffmanTree(weights);
for (int i = 1; i < htNodes.length; i ++) {
System.out.println(htNodes[i]);
}
System.out.println("WPL = " + huffman.calculateWPL(weights));
}
}
- 测试结果:
什么是哈夫曼编码?
在远程通讯中,带传输的字符内容往往都是压缩成二进制的字符串进行传输。对于每一个字符,我们可以给定一个唯一确定的数字来代表(例如ASCII编码),这种形式称为编码。
编码包含定长编码和不定长编码,对于定长编码,我们在解码时直接通过字符串截取就可以实现;对于不定长编码,我们需要确保编码为前缀编码(任何一个编码都不是其他任何编码的前缀(左子串)),才能够实现正常的解码操作。
哈夫曼编码就是最优前缀编码。
如何构建哈夫曼编码?
构建过程
- 统计每个字符出现的频率作为其权值。
- 将每个字符作为叶子节点构建哈夫曼树。
- 在哈夫曼树的每个分支上标记 0 0 0 或 1 1 1。(左 0 0 0 右 1 1 1 )
- 从根节点开始读取,直到叶子节点即为该字符的编码。
PS:由于没有一个叶子节点是其他叶子节点的祖先,所以每个字符的编码都不可能包含其他字符编码的前缀。
代码实现
- 从叶子节点出发,逆序遍历每一个节点,最后再将结果反转即可得到编码。
public String[] HuffmanCoding(int[] weights) {
int n = weights.length;
HTNode[] htNodes = createHuffmanTree(weights);
String[] huffmanCoding = new String[n];
for (int i = 1; i <= n; i ++) {
huffmanCoding[i - 1] = "";
//从叶子结点出发,得到的哈夫曼编码是逆序的,需要在字符串数组中逆序存放
String s = "";
int last = i, cur = i;
while (htNodes[cur].parent != 0) {
cur = htNodes[cur].parent;
s += htNodes[cur].lChild == last ? "0" : "1";
last = cur;
}
// 调整顺序
for (int j = 0; j < s.length(); j ++) {
huffmanCoding[i - 1] += s.charAt(s.length() - j - 1);
}
}
return huffmanCoding;
}
- 测试代码:
public class Test {
public static void main(String[] args) {
int[] weights = new int[5];
weights[0] = 7;
weights[1] = 5;
weights[2] = 5;
weights[3] = 2;
weights[4] = 4;
HuffmanTree huffman = new HuffmanTree();
HTNode[] htNodes = huffman.createHuffmanTree(weights);
for (int i = 1; i < htNodes.length; i ++) {
System.out.println(htNodes[i]);
}
System.out.println();
System.out.println("WPL = " + huffman.calculateWPL(weights));
String[] code;
code = huffman.HuffmanCoding(weights);
System.out.println();
for (int i = 0; i < 5; i ++) {
System.out.println(code[i]);
}
}
}