Java学习第28天~第30天,Huffman编码

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]
选择 12
 19 的孩子是 12
选择 34
 20 的孩子是 34
选择 78
 21 的孩子是 78
选择 1015
 22 的孩子是 1015
选择 1617
 23 的孩子是 1617
选择 56
 24 的孩子是 56
选择 911
 25 的孩子是 911
选择 1213
 26 的孩子是 1213
选择 1819
 27 的孩子是 1819
选择 2021
 28 的孩子是 2021
选择 2223
 29 的孩子是 2223
选择 1424
 30 的孩子是 1424
选择 2526
 31 的孩子是 2526
选择 2728
 32 的孩子是 2728
选择 290
 33 的孩子是 290
选择 3031
 34 的孩子是 3031
选择 3233
 35 的孩子是 3233
选择 3435
 36 的孩子是 3435
根字母是: (*, 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
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值