哈夫曼树
哈夫曼树是一种特殊的二叉树,它的每个叶子节点都对应着一个字符,并且树的形状是根据字符出现的频率来构建的。频率越高的字符离根节点越近,频率越低的字符离根节点越远。这样,出现频率高的字符可以用较短的编码表示,而出现频率低的字符可以用较长的编码表示。
public class Huffman implements Comparable<Huffman> {
private Integer weight; // 权重
private Character name; // 编码字符
private String encode; // 编码值
private Huffman lChildren; // 左子树
private Huffman rChildren; // 右子树
public Huffman() {
}
public Huffman(Integer weight, Character name, String encode, Huffman lChildren, Huffman rChildren) {
this.weight = weight;
this.name = name;
this.encode = encode;
this.lChildren = lChildren;
this.rChildren = rChildren;
}
@Override
public int compareTo(Huffman o) {
return this.getWeight() - o.getWeight();
}
@Override
public String toString() {
return "Huffman{" +
"weight=" + weight +
", name=" + name +
", encode=" + encode +
", lChildren=" + lChildren +
", rChildren=" + rChildren +
'}';
}
public Integer getWeight() {
return weight;
}
public Character getName() {
return name;
}
public Huffman getLChildren() {
return lChildren;
}
public Huffman getRChildren() {
return rChildren;
}
}
哈夫曼树的构建过程如下:
- 统计字符的出现频率,将每个字符与其频率构成一个节点。
- 将所有节点按照频率从小到大进行排序。
- 取出频率最小的两个节点,创建一个新的节点作为它们的父节点,该节点的频率为两个子节点频率之和。
- 将新创建的节点放回节点列表中,并重新排序。
- 重复步骤3和步骤4,直到只剩下一个节点,即为哈夫曼树的根节点。
/**
* 根据字符频率
*
* @param str 需要编码数据
* @return
*/
public Huffman createHuffmanRootNode(String str) {
Huffman root = null;
HashMap<Character, Integer> map = new HashMap<Character, Integer>();
for (int i = 0; i < str.length(); i++) {
if (map.containsKey(str.charAt(i))) {
Integer val = map.get(str.charAt(i));
map.put(str.charAt(i), val + 1);
} else {
map.put(str.charAt(i), 1);
}
}
LinkedList<Huffman> listNode = new LinkedList<>();
for (Map.Entry<Character, Integer> en : map.entrySet()) {
Huffman huffman = new Huffman(en.getValue(), en.getKey(), "", null, null);
listNode.add(huffman);
}
Collections.sort(listNode);
root = createTree(listNode);
return root;
}
/**
* 直接根据权重
*
* @param str 待编码字符串
* @param weight 对应权重集
* @return
*/
public Huffman createHuffmanRootNode(String str, Integer[] weight) {
HashMap<Character, Integer> map = new HashMap<Character, Integer>();
for (int i = 0; i < str.length(); i++) {
map.put(str.charAt(i), weight[i]);
}
LinkedList<Huffman> listNode = new LinkedList<>();
for (Map.Entry<Character, Integer> en : map.entrySet()) {
Huffman huffman = new Huffman(en.getValue(), en.getKey(), "", null, null);
listNode.add(huffman);
}
Collections.sort(listNode);
return createTree(listNode);
}
/**
* 1, 如果该树只有一个节点直接设置为左树
* 2, 找出最小的两个节点构建成树 并且构建的新树根节点权重为两子节点的和
* 3, 重新排序节点列表
*
* @param listNode
* @return
*/
private Huffman createTree(LinkedList<Huffman> listNode) {
Huffman root = null;
while (listNode.size() > 0) {
if (listNode.size() == 1) {
Huffman left = listNode.removeFirst();
return new Huffman(left.getWeight(), null, "", left, null);
}
Huffman left = listNode.removeFirst();
Huffman right = listNode.removeFirst();
if (listNode.size() == 0) {
root = new Huffman(left.getWeight() + right.getWeight(), null, "", left, right);
} else {
Huffman huffman = new Huffman(left.getWeight() + right.getWeight(), null, "", left, right);
listNode.add(huffman);
Collections.sort(listNode);
}
}
return root;
}
哈夫曼编码
哈夫曼编码是一种前缀编码方式,即没有任何一个编码是另一个编码的前缀。通过哈夫曼树的结构,我们可以为每个字符生成对应的编码。生成编码的过程如下:
- 从哈夫曼树的根节点开始,沿着左子树走一步表示编码为0,沿着右子树走一步表示编码为1。
- 遍历哈夫曼树的每个叶子节点,生成对应字符的编码。
哈夫曼编码的特点是没有编码是其他编码的前缀,这样可以避免在解码时出现歧义。
/**
* 获取编码表
*/
public HashMap<Character, String> getCode(Huffman root) {
HashMap<Character, String> huffmanCode = new HashMap<>();
getHuffmanCode(root, huffmanCode, "");
return huffmanCode;
}
private static void getHuffmanCode(Huffman root, HashMap<Character, String> huffmanCode, String code) {
if (null == root) return;
if (null == root.getLChildren() && null == root.getRChildren()) {
huffmanCode.put(root.getName(), code);
}
if (null != root.getLChildren()) {
getHuffmanCode(root.getLChildren(), huffmanCode, code+"0");
}
if (null != root.getRChildren()) {
getHuffmanCode(root.getRChildren(), huffmanCode, code+"1");
}
}
/**
* 编码
*
* @param root 数
* @param str 待编码字符串
* @return 编码字符串
*/
public String encode(Huffman root, String str) {
StringBuffer encode = new StringBuffer();
HashMap<Character, String> code = this.getCode(root);
for (int i = 0; i < str.length(); i++) {
encode.append(code.get(str.charAt(i)));
}
return encode.toString();
}
/**
* 解码
*
* @param root 哈夫曼树的根节点
* @param code 待解码的编码字符串
* @return 解码后的字符串
*/
public String decode(Huffman root, String code) {
StringBuilder decode = new StringBuilder();
Huffman currentNode = root;
for (int i = 0; i < code.length(); i++) {
char bit = code.charAt(i);
if (bit == '0') {
currentNode = currentNode.getLChildren();
} else if (bit == '1') {
currentNode = currentNode.getRChildren();
}
if (currentNode.getLChildren() == null && currentNode.getRChildren() == null) {
decode.append(currentNode.getName());
currentNode = root; // 重置当前节点为根节点,继续下一个字符的解码
}
}
return decode.toString();
}
哈夫曼编码实现文件压缩
利用哈夫曼编码可以实现文件的压缩和解压缩。压缩过程如下:
- 统计文件中每个字符的出现频率。
- 根据字符频率构建哈夫曼树。
- 生成每个字符对应的哈夫曼编码。
- 将原始文件中的每个字符用对应的哈夫曼编码替代。
- 将编码后的内容写入压缩文件,同时保存哈夫曼树的结构和字符频率信息。
解压缩过程如下:
- 读取压缩文件中的哈夫曼树和字符频率信息。
- 根据哈夫曼树重建哈夫曼树的结构。
- 读取压缩文件中的编码内容。
- 根据哈夫曼树将编码内容解码为原始字符。
- 将解码后的字符写入解压缩文件,即得到原始文件。
public static void main(String[] args) {
Huffman huffman = new Huffman();
String str = txt2String(new File("D:\\JAVA\\DataStructuresAndAlgorithms\\src\\com\\DataStructures\\Tree\\Huffman\\Huffman.java"));
Huffman rootNode = huffman.createHuffmanRootNode(str);
// System.out.println(rootNode);
HashMap<Character, String> code = huffman.getCode(rootNode);
System.out.println(code);
String encode = huffman.encode(rootNode, str);
System.out.println(encode);
String decode = huffman.decode(rootNode, encode);
// System.out.println(decode);
// 将编码字符写入文件
try (FileOutputStream fos = new FileOutputStream("D:\\JAVA\\DataStructuresAndAlgorithms\\src\\com\\DataStructures\\Tree\\Huffman\\output.bin")) {
// 将二进制字符串转换为字节数组
byte[] bytes = binaryStringToBytes(encode);
// 将字节数组转为二进制字符串
String str2 = bytesToBinaryString(bytes);
// 写入字节数组到文件
fos.write(bytes);
System.out.println("数据写入成功!");
System.out.println(str2);
System.out.println(huffman.decode(rootNode, str2));
} catch (IOException e) {
e.printStackTrace();
}
}
/**
* 文件转字符串
* @param file
* @return
*/
public static String txt2String(File file)
{
StringBuilder result = new StringBuilder();
try
{
//构造一个BufferedReader类来读取文件
BufferedReader br = new BufferedReader(new FileReader(file));
String s = null;
while((s = br.readLine())!=null)
{//使用readLine方法,一次读一行
result.append(System.lineSeparator()+s);
}
br.close();
}
catch(Exception e)
{
e.printStackTrace();
}
return result.toString();
}
/**
* string转为字节数据
* @param binaryString
* @return
*/
private static byte[] binaryStringToBytes(String binaryString) {
int length = binaryString.length();
int bytesLength = length / 8; // 字节数组的长度
if (length % 8 != 0) {
bytesLength++; // 如果二进制字符串长度不能被8整除,需要额外的一个字节存储剩余的二进制字符
}
byte[] bytes = new byte[bytesLength];
int index = 0; // 字节数组中的索引位置
for (int i = 0; i < length; i += 8) {
int endIndex = i + 8;
if (endIndex > length) {
endIndex = length; // 如果末尾位置超过二进制字符串长度,将末尾位置设置为字符串末尾
}
String byteString = binaryString.substring(i, endIndex);
bytes[index] = (byte) Integer.parseInt(byteString, 2);
index++;
}
return bytes;
}
/**
* 把字节数据还原为string
* @param bytes
* @return
*/
private static String bytesToBinaryString(byte[] bytes) {
StringBuilder sb = new StringBuilder();
for (byte b : bytes) {
String binaryString = Integer.toBinaryString(b & 0xFF);
// 每个字节的二进制表示长度都是8位,在不足8位的情况下在前面补0
while (binaryString.length() < 8) {
binaryString = "0" + binaryString;
}
sb.append(binaryString);
}
return sb.toString();
}
通过哈夫曼编码,我们可以有效地减小文件的大小,节省存储空间。这是因为出现频率高的字符使用了较短的编码,而出现频率低的字符使用了较长的编码,从而整体上减小了编码后的文件大小。
总结起来,哈夫曼树和哈夫曼编码是一种高效的数据压缩算法。通过构建哈夫曼树和生成对应的哈夫曼编码,我们可以实现文件的压缩和解压缩。哈夫曼编码的特点是高频字符使用短编码,低频字符使用长编码,这种编码方式避免了解码时的歧义。通过哈夫曼编码,我们可以大幅度减小文件的大小,提高存储和传输效率。