1.定义一个内嵌类.
2.每个节点的内容包括: 字符 (仅对叶节点有效)、权重 (用的整数, 该字符的个数)、指向子节点父节点的引用. 指向父节点的引用是必须的。
3.NUM_CHARS 是指 ASCII 字符集的字符个数. 为方便起见, 仅支持 ASCII。
4.inputText 的引入只是想把程序尽可能细分成独立的模块, 这样便于学习和调拭。
5.alphabet 仅存 inputText 出现过的字符。
6.alphabetLength 完全可以用 alphabet.length() 代替, 但我就喜欢写成独立的变量。
7.charCounts 要为所有的节点负责, 其元素对应于 HuffmanNode 里面的 weight. 为了节约, 可以把其中一个省掉。
8.charMapping 是为了从 ASCII 里面的顺序映射到 alphabet 里面的顺序. 这也是我只采用 ASCII 字符集 (仅 256 字符) 的原因。
9.huffmanCodes 将个字符映射为一个字符串, 其实应该是二进制串. 我这里不是想偷懒么。
10.nodes 要先把所有的节点存储在一个数组里面, 然后再链接它们. 这是常用招数。
11.构造方法仅初始化了 charMapping, 读入了文件。
12.readText 采用了最简单粗暴的方式. 还可以有其它的逐行读入的方式。
13.要自己弄个文本文件, 里面存放一个字符串 abcdedgsgs 之类, 或者几行英文文本。
package java21to30;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.util.Arrays;
import java.util.stream.Collectors;
public class D28_Huffman {
public static void main(String args[]) {
D28_Huffman tempHuffman = new D28_Huffman("E:\\temp\\test.txt");
tempHuffman.constructAlphabet();
tempHuffman.constructTree();
HuffmanNode root = tempHuffman.getRoot();
System.out.println("根字母是: " + root);
System.out.println("先序遍历:");
tempHuffman.preOrderVisit(tempHuffman.getRoot());
System.out.println("\n");
tempHuffman.generateCodes();
String tempCoded = tempHuffman.coding("abcdb");
System.out.println("编码: " + tempCoded);
String tempDecoded = tempHuffman.decoding(tempCoded);
System.out.println("已解码: " + tempDecoded);
}
class HuffmanNode {
char character;
int weight;
HuffmanNode leftChild;
HuffmanNode rightChild;
HuffmanNode parent;
public HuffmanNode(char paraCharacter, int paraWeight, HuffmanNode paraLeftChild, HuffmanNode paraRightChild,
HuffmanNode paraParent) {
character = paraCharacter;
weight = paraWeight;
leftChild = paraLeftChild;
rightChild = paraRightChild;
parent = paraParent;
}
public String toString() {
String resultString = "(" + character + ", " + weight + ")";
return resultString;
}
}
public static final int NUM_CHARS = 256;
String inputText;
int alphabetLength;
char[] alphabet;
int[] charCounts;
int[] charMapping;
String[] huffmanCodes;
HuffmanNode[] nodes;
public D28_Huffman(String paraFilename) {
charMapping = new int[NUM_CHARS];
readText(paraFilename);
}
public void readText(String paraFilename) {
try {
inputText = Files.newBufferedReader(Paths.get(paraFilename), StandardCharsets.UTF_8).lines()
.collect(Collectors.joining("\n"));
} catch (Exception ee) {
System.out.println(ee);
System.exit(0);
}
System.out.println("文档内容为:\r\n" + inputText + "\n");
}
/* Huffman 编码 (建树)
1、Arrays.fill(charMapping, -1);这种初始化工作非常重要。搞不好就会调拭很久才找到bug。
2、变量多的时候你才能体会到用temp,para这些前缀来区分不同作用域变量的重要性,没有特殊前缀的就是成员变量。
3、建树就是一个自底向上,贪心选择的过程。确定子节点、父节点的代码是核心。
4、最后生成的节点就是根节点。
5、手绘相应的Huffman树对照,才能真正理解。
*/
public void constructAlphabet() {
Arrays.fill(charMapping, -1);
int[] tempCharCounts = new int[NUM_CHARS];
int tempCharIndex;
char tempChar;
String outStr = "";
System.out.print("ASCII码对照:\r\n");
for (int i = 0; i < inputText.length(); i++) {
tempChar = inputText.charAt(i);
tempCharIndex = (int) tempChar;
if (i != 0 && i % 20 == 0) {
outStr = String.format(" %s,\r\n", tempCharIndex);
} else {
outStr = String.format(" %s,", tempCharIndex);
}
System.out.print(outStr);
tempCharCounts[tempCharIndex]++;
}
alphabetLength = 0;
for (int i = 0; i < 255; i++) {
if (tempCharCounts[i] > 0) {
alphabetLength++;
}
}
alphabet = new char[alphabetLength];
charCounts = new int[2 * alphabetLength - 1];
int tempCounter = 0;
for (int i = 0; i < NUM_CHARS; i++) {
if (tempCharCounts[i] > 0) {
alphabet[tempCounter] = (char) i;
charCounts[tempCounter] = tempCharCounts[i];
charMapping[i] = tempCounter;
tempCounter++;
}
}
System.out.println("\n字母表为: " + Arrays.toString(alphabet));
System.out.println("\n字母统计: " + Arrays.toString(charCounts));
System.out.println("\n字符影射表: " + Arrays.toString(charMapping));
}
public void constructTree() {
nodes = new HuffmanNode[alphabetLength * 2 - 1];
boolean[] tempProcessed = new boolean[alphabetLength * 2 - 1];
for (int i = 0; i < alphabetLength; i++) {
nodes[i] = new HuffmanNode(alphabet[i], charCounts[i], null, null, null);
}
int tempLeft, tempRight, tempMinimal;
for (int i = alphabetLength; i < 2 * alphabetLength - 1; i++) {
tempLeft = -1;
tempMinimal = Integer.MAX_VALUE;
for (int j = 0; j < i; j++) {
if (tempProcessed[j]) {
continue;
}
if (tempMinimal > charCounts[j]) {
tempMinimal = charCounts[j];
tempLeft = j;
}
}
tempProcessed[tempLeft] = true;
tempRight = -1;
tempMinimal = Integer.MAX_VALUE;
for (int j = 0; j < i; j++) {
if (tempProcessed[j]) {
continue;
}
if (tempMinimal > charCounts[j]) {
tempMinimal = charCounts[j];
tempRight = j;
}
}
tempProcessed[tempRight] = true;
System.out.println("选择 " + tempLeft + " 和 " + tempRight);
charCounts[i] = charCounts[tempLeft] + charCounts[tempRight];
nodes[i] = new HuffmanNode('*', charCounts[i], nodes[tempLeft], nodes[tempRight], null);
nodes[tempLeft].parent = nodes[i];
nodes[tempRight].parent = nodes[i];
System.out.println(" " + i + " 的孩子是 " + tempLeft + " 和 " + tempRight);
}
}
public HuffmanNode getRoot() {
return nodes[nodes.length - 1];
}
/* Huffman 编码(编码与解码)
1、前序遍历代码的作用仅仅是调拭。
2、双重循环有一点点难度。
4、编码是从叶节点到根节点,解码就是反过来。
5、解码获得原先的字符串,就验证正确性了。
*/
public void preOrderVisit(HuffmanNode paraNode) {
System.out.print("(" + paraNode.character + ", " + paraNode.weight + ") ");
if (paraNode.leftChild != null) {
preOrderVisit(paraNode.leftChild);
}
if (paraNode.rightChild != null) {
preOrderVisit(paraNode.rightChild);
}
}
public void generateCodes() {
huffmanCodes = new String[alphabetLength];
HuffmanNode tempNode;
for (int i = 0; i < alphabetLength; i++) {
tempNode = nodes[i];
String tempCharCode = "";
while (tempNode.parent != null) {
if (tempNode == tempNode.parent.leftChild) {
tempCharCode = "0" + tempCharCode;
} else {
tempCharCode = "1" + tempCharCode;
}
tempNode = tempNode.parent;
}
huffmanCodes[i] = tempCharCode;
System.out.println("编码 " + alphabet[i] + " 是 " + tempCharCode);
}
}
public String coding(String paraString) {
String resultCodeString = "";
int tempIndex;
for (int i = 0; i < paraString.length(); i++) {
tempIndex = charMapping[(int) paraString.charAt(i)];
resultCodeString += huffmanCodes[tempIndex];
}
return resultCodeString;
}
public String decoding(String paraString) {
String resultCodeString = "";
HuffmanNode tempNode = getRoot();
for (int i = 0; i < paraString.length(); i++) {
if (paraString.charAt(i) == '0') {
tempNode = tempNode.leftChild;
System.out.println(tempNode);
} else {
tempNode = tempNode.rightChild;
System.out.println(tempNode);
}
if (tempNode.leftChild == null) {
System.out.println("解码:\n" + tempNode);
resultCodeString += tempNode.character;
tempNode = getRoot();
}
}
return resultCodeString;
}
}
输出结果:
文档内容为:
Bad luck often brings good luck.
ASCII码对照:
66, 97, 100, 32, 108, 117, 99, 107, 32, 111, 102, 116, 101, 110, 32, 98, 114, 105, 110, 103,
115,32, 103, 111, 111, 100, 32, 108, 117, 99, 107, 46,
字母表为: [ , ., B, a, b, c, d, e, f, g, i, k, l, n, o, r, s, t, u]
字母统计: [5, 1, 1, 1, 1, 2, 2, 1, 1, 2, 1, 2, 2, 2, 3, 1, 1, 1, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0]
字符影射表: [-1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1,
-1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, 0, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1,
-1, 1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, 2, -1, -1, -1,
-1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1,
-1, -1, -1, 3, 4, 5, 6, 7, 8, 9, -1, 10, -1, 11, 12, -1, 13, 14, -1, -1, 15, 16, 17, 18, -1, -1,
-1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1,
-1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1,
-1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1,
-1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1,
-1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1,
-1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1]
选择 1 和 2
19 的孩子是 1 和 2
选择 3 和 4
20 的孩子是 3 和 4
选择 7 和 8
21 的孩子是 7 和 8
选择 10 和 15
22 的孩子是 10 和 15
选择 16 和 17
23 的孩子是 16 和 17
选择 5 和 6
24 的孩子是 5 和 6
选择 9 和 11
25 的孩子是 9 和 11
选择 12 和 13
26 的孩子是 12 和 13
选择 18 和 19
27 的孩子是 18 和 19
选择 20 和 21
28 的孩子是 20 和 21
选择 22 和 23
29 的孩子是 22 和 23
选择 14 和 24
30 的孩子是 14 和 24
选择 25 和 26
31 的孩子是 25 和 26
选择 27 和 28
32 的孩子是 27 和 28
选择 29 和 0
33 的孩子是 29 和 0
选择 30 和 31
34 的孩子是 30 和 31
选择 32 和 33
35 的孩子是 32 和 33
选择 34 和 35
36 的孩子是 34 和 35
根字母是: (*, 32)
先序遍历:
(*, 32) (*, 15) (*, 7) (o, 3) (*, 4) (c, 2) (d, 2) (*, 8) (*, 4) (g, 2) (k, 2) (*, 4) (l, 2)
(n, 2) (*, 17) (*, 8) (*, 4) (u, 2) (*, 2) (., 1) (B, 1) (*, 4) (*, 2) (a, 1) (b, 1) (*, 2)
(e, 1) (f, 1) (*, 9) (*, 4) (*, 2) (i, 1) (r, 1) (*, 2) (s, 1) (t, 1) ( , 5)
编码 是 111
编码 . 是 10010
编码 B 是 10011
编码 a 是 10100
编码 b 是 10101
编码 c 是 0010
编码 d 是 0011
编码 e 是 10110
编码 f 是 10111
编码 g 是 0100
编码 i 是 11000
编码 k 是 0101
编码 l 是 0110
编码 n 是 0111
编码 o 是 000
编码 r 是 11001
编码 s 是 11010
编码 t 是 11011
编码 u 是 1000
编码: 10100101010010001110101
(*, 17)
(*, 8)
(*, 4)
(*, 2)
(a, 1)
解码:
(a, 1)
(*, 17)
(*, 8)
(*, 4)
(*, 2)
(b, 1)
解码:
(b, 1)
(*, 15)
(*, 7)
(*, 4)
(c, 2)
解码:
(c, 2)
(*, 15)
(*, 7)
(*, 4)
(d, 2)
解码:
(d, 2)
(*, 17)
(*, 8)
(*, 4)
(*, 2)
(b, 1)
解码:
(b, 1)
已解码: abcdb