基于哈夫曼树的文本压缩

概览

本文原载于我的博客,地址:https://blog.guoziyang.top/archives/24/

哈夫曼树也称为最优二叉树,是加权路径长度最短的二叉树。权值越大的叶子结点越靠近根结点,而权值越小的叶子结点越远离根结点。每一个父节点的权值等于子节点的权值之和。

可以这么说,哈夫曼树中,真正有效的、存储着数据的,只有那些叶节点,而其它节点仅仅是为了构造树结构、以及保持总权值最小而存在的。

那么问题是,哈夫曼树和文本压缩有什么关系呢?这就不得不提到文本编码问题。

编码与哈夫曼树

编码(主要是二进制码),是指将一组对象(如字符集)中的每个对象用唯一的一个二进制位串表示。编码的方式有很多种,主要分为两类:等长的和不等长的。等长的自然好理解,每一个字符的二进制码长度都相等,那么不等长的呢?

很容易就可以想到,不等长的编码有一个问题:二义性,即一串编码可以有多种不同的方式解码,如以下这个例子:

字符E编码为00,字符T编码为01,字符W编码为0001
二进制串0001的解码则存在两种方式:ET和W

所以,我们需要这种编码保持前缀性,即一组编码中任意一个编码都不是其它任何一个编码的前缀。而哈夫曼树就可以为我们解决这个问题,即:从根节点到叶节点到路径不存在任何前缀性重复。

所以,我们引入了哈夫曼编码问题:对于给定的字符集及其每个字符出现的概率(使用频度),求该字符集的最优的前缀性编码。那么总体的思想就很明确了:

  1. 计算每个字符在文本中的出现频率
  2. 构造频率对应的哈夫曼树
  3. 利用这棵树对文本进行编码存储

编码

计算字符出现的频率

这段算法思想比较简单粗暴,直接遍历一遍,把字符和频次存在一个Map里,遍历到一个字符就把那个字符对应的频次加1,最后根据频次计算出该字符对应的频率即可,返回的也是个<字符,频率>的Map。

Java实现如下:

private static HashMap<Character, Double> calculateCharChance(String fileContent) {
        if(fileContent == null) {
            return null;
        }
        HashMap<Character, Double> charTimes = new HashMap<>();
        for(int i = 0; i < fileContent.length(); i ++) {
            char tempChar = fileContent.charAt(i);
            if(charTimes.containsKey(tempChar)) {
                charTimes.put(tempChar, charTimes.get(tempChar) + 1d);
            } else {
                charTimes.put(tempChar, 1d);
            }
        }
        HashMap<Character, Double> perChance = new HashMap<>();
        for(Map.Entry<Character, Double> entry : charTimes.entrySet()) {
            perChance.put(entry.getKey(), entry.getValue() / fileContent.length());
        }
        return perChance;
    }

构造哈夫曼树

接着我们就需要构造一棵哈夫曼树,我们首先定义一下每个节点的类:

class TreeNode implements Serializable{
    private static final long serialVersionUID = 1L;

    private Character currentChar = null;
    private Double chance;
    private TreeNode parentNode;
    private TreeNode leftNode;
    private TreeNode rightNode;

    public TreeNode(char currentChar, double chance) {
        this.currentChar = currentChar;
        this.chance = chance;
    }

    public TreeNode(double chance) {
        this.chance = chance;
    }

    public Character getCurrentChar() {
        return currentChar;
    }

    public Double getChance() {
        return chance;
    }

    public void setParentNode(TreeNode parentNode) {
        this.parentNode = parentNode;
    }

    public TreeNode getParentNode() {
        return parentNode;
    }

    public void setLeftNode(TreeNode leftNode) {
        this.leftNode = leftNode;
    }

    public TreeNode getLeftNode() {
        return leftNode;
    }

    public void setRightNode(TreeNode rightNode) {
        this.rightNode = rightNode;
    }

    public TreeNode getRightNode() {
        return rightNode;
    }
}

其中,currentChar字段表示该节点代表的字符,当然,只有叶节点才会有这个字段,chance表示该节点的权重(叶节点的字符的概率)。

哈夫曼树的构造算法如下:

1. 由给定的权值构造n棵只有一个根节点、左右子树均为空的二叉树,得到一个二叉树集合
2. 当集合中的元素大于1时,循环执行:
	2.1. 选取集合中根节点权值最小的两棵树作为左右子树,权值之和作为父节点的权值构造二叉树
	2.2. 将这棵树添加进集合,并且删去原来的两棵树
3. 集合中留下的最后一棵树即为哈夫曼树

代码比较简单:

private static TreeNode buildHuffmanTree(HashMap<Character, Double> perChance) {
        if(perChance == null) {
            return null;
        }
        ArrayList<TreeNode> treeNodes = new ArrayList<>();
        leafNodes = new HashMap<>();
        for(Map.Entry<Character, Double> entry : perChance.entrySet()) {
            TreeNode tempNode = new TreeNode(entry.getKey(), entry.getValue());
            treeNodes.add(tempNode);
            leafNodes.put(tempNode.getCurrentChar(), tempNode);
        }
        TreeNodeComparator comparator = new TreeNodeComparator();
        while(treeNodes.size() != 1) {
            Collections.sort(treeNodes, comparator);
            TreeNode tempNode1 = treeNodes.remove(0);
            TreeNode tempNode2 = treeNodes.remove(0);
            TreeNode tempTop = new TreeNode(tempNode1.getChance() + tempNode2.getChance());
            tempTop.setLeftNode(tempNode1);
            tempTop.setRightNode(tempNode2);
            tempNode1.setParentNode(tempTop);
            tempNode2.setParentNode(tempTop);
            treeNodes.add(tempTop);
        }
        return treeNodes.get(0);
    }

其中,leafNodes是一个Map,Key为字符,Value是该字符对应的叶节点,便于直接获取哈夫曼树的叶节点而不需要遍历搜索。

对文本编码

对文本编码分为两个步骤:获得每个字符对应的编码,对文本进行编码

获得字符编码

通过leafNodes,我们可以直接获取到文本中出现过的每一个字符,以及对应的叶节点。从叶节点开始,一层一层向上寻找,直到根节点为止,若该节点为父节点的左节点,则编码字符串后面添一个1,否则添0。最后我们可以得到这个叶节点到根节点的一个01序列。注意,这个序列是叶节点到根节点的路径,而哈夫曼编码是根节点到叶节点的路径,所以得到的字符串还应该倒置一下才是哈夫曼编码。

HashMap<Character, String> codeMap = new HashMap<>();
for(Map.Entry<Character, TreeNode> entry : leafNodes.entrySet()) {            		TreeNode tempNode = entry.getValue();
    TreeNode tempHeadNode = null;
    String code = "";
    while(tempNode.getParentNode() != null) {
        tempHeadNode = tempNode.getParentNode();
        if(tempNode.equals(tempHeadNode.getLeftNode())) {
        	code += "0";
        } else {
            code += "1";
        }
        tempNode = tempNode.getParentNode();
     }
     codeMap.put(entry.getValue().getCurrentChar(), new StringBuilder(code).reverse().toString());
}

对文本编码

对文本编码就比较容易了,遍历原文本,在codeMap中找到对应字符的01序列即可。

StringBuilder encraptedContent = new StringBuilder();
for(int i = 0; i < fileContent.length(); i ++) {
	encraptedContent.append(codeMap.get(fileContent.charAt(i)));
}
binaryNumber = encraptedContent.length();
encraptedContent = encraptedContent.insert(0, toFullBinaryString(binaryNumber));
int moreZero = 8 - encraptedContent.length() % 8;
for(int i = 0; i < moreZero; i ++) {
	encraptedContent.append("0");
}
return encraptedContent.toString();

写入文件

本来以为是最简单的一部分,没想到确实最让人头疼的一部分。

也许你已经注意到了,在对文本编码的部分,我将最后的编完码的字符串用0补位,补成8的倍数,就是因为写入的时候,是一个字节一个字节写入的,一个字节是8位。

而且最重要的问题是,我们不可以使用字符流!试想,如果我们使用字符流,会怎么样呢?

譬如一个a,在文件中的存储形式是ascii码,也就是0110 0001,一个字节。编码之后,假设是00101,如果按照字符流写入的话,00101还会被转化为ascii码写入,占空间高达5个字节!越压缩越大了。

所以,我们压缩之后的文件应当是按照字节写入的,同时,每个01应当是一个二进制位。

Java中有一个基本数据类型byte,对应着存储结构中的一个字节,我们需要改变byte的每一个位,按位或即可。

譬如,我们需要将一个byte的第1位改为1,我们只需要将这个byte强转为int之后,和0x80(10000000)相与,最后再转为byte即可。

最后还会遇到一个问题,当我们对01序列凑整的时候(凑整为8的倍数),我们使用的是补0操作,将序列补为8的倍数。可是如果解码的时候,当正常的文本内容读取完成时,会对那些0进行解码,如果恰巧可以解码出字符的话,文本后面就会出现一些奇怪的字符。

我们处理方式你们可能也注意到了,在对文本编码的时候,我记录了有效的01序列的个数,并将其转化为32位的二进制数,对文本编码时调用的toFullBinaryString()就是将int转化为32位二进制数的。编码时将这个二进制数存在这个密码文件的开头4个字节处,理论上这种方法可以记录有效的2^32 = 4294967296位,也就是42亿多,应该是足够了!

当然,我们解码的时候也需要这棵哈夫曼树帮助解码,所以我们也需要保存这棵哈夫曼树,这里我只是简单地将这棵树用ObjectOutputStream序列化到一个key文件中了。

代码如下:

private static void writeIntoFile(String encraptedContent) {
    if(encraptedContent == null) {
        return;
    }
    char num1 = "1".charAt(0);
    int byteLength = encraptedContent.length() / 8;
    byte[] bytes = new byte[byteLength];
    for(int i = 0; i < byteLength; i ++) {
        bytes[i] = (byte)0;
    }
    for(int i = 0; i < encraptedContent.length(); i ++) {
        char tempBinary = encraptedContent.charAt(i);
        if(tempBinary == num1) {
            switch(i % 8) {
                case 0: bytes[i/8] = (byte)((int)bytes[i/8] | 0x80);break;
                case 1: bytes[i/8] = (byte)((int)bytes[i/8] | 0x40);break;
                case 2: bytes[i/8] = (byte)((int)bytes[i/8] | 0x20);break;
                case 3: bytes[i/8] = (byte)((int)bytes[i/8] | 0x10);break;
                case 4: bytes[i/8] = (byte)((int)bytes[i/8] | 0x8);break;
                case 5: bytes[i/8] = (byte)((int)bytes[i/8] | 0x4);break;
                case 6: bytes[i/8] = (byte)((int)bytes[i/8] | 0x2);break;
                case 7: bytes[i/8] = (byte)((int)bytes[i/8] | 0x1);break;
            }
        }
    }
    File encrapyFile = new File(file.getName().split("\\.")[0] + ".encrapy");
    File keyFile = new File(file.getName().split("\\.")[0] + ".key");
    try{
        FileOutputStream outputStream = new FileOutputStream(encrapyFile);
        outputStream.write(bytes);
        outputStream.flush();
        outputStream.close();
        ObjectOutputStream objectOutputStream = new ObjectOutputStream(new FileOutputStream(keyFile));
        objectOutputStream.writeObject(topNode);
        objectOutputStream.close();
    }catch(Exception e) {
        System.out.println("加密后文件写入失败!");
    }
    System.out.println("\n密码文件为 " + encrapyFile.getName() + "\nKey文件为 " + keyFile.getName());
    System.out.println("压缩前文件大小 " + file.length() + " Byte\n压缩后文件大小 " + encrapyFile.length() + " Byte");
    System.out.printf("压缩率:%.2f%%\n", (((float)file.length() - (float)encrapyFile.length()) / (float)file.length()) * 100);
}

效果

GuodeMacBook-Air:HuffmanHomework guoziyang$ javaHuffmanHomework


1. 原始文件压缩
2. 压缩文件解码
3. 退出
请输入选择:1


请输入加密的文件路径:example4.txt

密码文件为 example4.encrapy
Key文件为 example4.key
压缩前文件大小 1015585 Byte
压缩后文件大小 564063 Byte
压缩率:44.46%

解码

解码的方式也是比较简单的,读入密码文件,转化为二进制字符串,读入key文件,转化为哈夫曼树,根据序列查找字符就行了。

读入文件

读入key文件很简单,也就是用ObjectInputStream读进来就好了。

主要是读入密码文件以及处理。

读入的时候我们当然不可以用字符流,否则将会把01序列认作ascii码序列转化为字符,我们需要的是原始的01序列。所以我们选择字节流。

同时我们还需要一个类,ByteArrayOutputStream,这个类内部有一个缓冲区,可以使用toByteArray()方法将缓冲区内部的所有byte转化为一个byte数组。

InputStream inputStream = new FileInputStream(encrapyFileName);
ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
byte[] buffer = new byte[1024 * 4];
int n = 0;
while((n = inputStream.read(buffer)) != -1) {
    byteArrayOutputStream.write(buffer, 0, n);
}
bytes = byteArrayOutputStream.toByteArray();

循环读到的字节直接写到ByteArrayOutputStream里,最后toByteArray()即可。

现在我们拿到了代表密码内容的字节数组,转化为二进制String的方法如下:

StringBuilder builder = new StringBuilder();
for(byte tempByte : bytes) {
    builder.append(Integer.toBinaryString((tempByte & 0xFF) + 0x100).substring(1));
}
return builder.toString();

我主要解释下Integer.toBinaryString((tempByte & 0xFF) + 0x100).substring(1))的作用。

这条语句将byte转化为对应的二进制串。当一个byte类型直接转化为int时,JVM会对它进行补位操作,将8位补为32位。譬如,byte存储的是10000001,转为int之后1111111111111111111111111 10000001(32位),很显然这两个补码保存的十进制数字依旧是相同的。可是问题在于,我们做byte -> int的转化,并不是为了保持十进制的一致性,而是为了拿到其二进制补码,所以我们使用0xFF与这个byte相与,保证前24位依旧是0。将这个码加上0x100(100000000)将第9位置为1,这样即使前几位为0的话也可以不被舍掉,最后使用substring(1)将第九位上的1去掉即可。

按照哈夫曼树解码

我们拿到了原始的二进制字串和哈夫曼树,只需要解码就完事了。

解码算法如下:

1. 将指针置于哈夫曼树的头节点
2. 依次读入二进制码
	2.1 如果读入0,则指针指向当前节点的左支,否则指向右支
	2.2 判断当前节点是否是叶节点,如果是则将该节点的字符输出并将指针置于头节点
	2.3 继续循环

由此即可输出原文本内容,注意我们在编码的时候把一个四字节32位的二进制数存在文件头,应当首先读出,以确认有效的二进制码位数。

代码如下:

StringBuilder resultBuilder = new StringBuilder();
binaryNumber = binaryToTen(encraptedContent.substring(0, 32));
encraptedContent = encraptedContent.substring(32);
TreeNode tempNode = topNode;
for(int i = 0; i < binaryNumber; i ++) {
    if(encraptedContent.charAt(i) == '0') {
            tempNode = tempNode.getLeftNode();
    } else {
        tempNode = tempNode.getRightNode();
    }
    if(tempNode.getCurrentChar() != null) {
        resultBuilder.append(tempNode.getCurrentChar());
        tempNode = topNode;
    }
}
return resultBuilder.toString()

完整代码

import java.io.BufferedReader;
import java.io.BufferedWriter;
import java.io.ByteArrayOutputStream;
import java.io.DataOutputStream;
import java.io.FileReader;
import java.io.FileWriter;
import java.io.IOException;
import java.io.InputStream;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.io.FileOutputStream;
import java.io.Serializable;
import java.util.ArrayList;
import java.util.Comparator;
import java.util.HashMap;
import java.util.Map;
import java.util.Scanner;
import java.util.Collections;
import java.io.File;
import java.io.FileInputStream;

public class HuffmanHomework {
    private static Scanner scanner = new Scanner(System.in);
    private static HashMap<Character, TreeNode> leafNodes = null;
    private static TreeNode topNode = null;
    private static File file = null;
    private static int binaryNumber = 0;

    public static void main(String[] args) {
        while(true) {
            menu();
        }
    }

    private static void menu() {
        System.out.println();
        System.out.println();
        System.out.println("1. 原始文件压缩");
        System.out.println("2. 压缩文件解码");
        System.out.println("3. 退出");
        System.out.print("请输入选择:");
        int choice = scanner.nextInt();
        if(choice != 1 && choice != 2 && choice != 3) {
            System.out.println("输入有误,请重新选择!");
            System.out.println();
            System.out.println();
            return;
        }
        System.out.println();
        System.out.println();
        switch(choice) {
            case 1:
                encrapt();
                break;
            case 2:
                decrapt();
                break;
            case 3:
                System.exit(0);
        }
    }

    private static void encrapt() {
        String fileContent = readFromFile();
        HashMap<Character, Double> perChance =  calculateCharChance(fileContent);
        topNode = buildHuffmanTree(perChance);
        String encraptedContent = encraptContent(fileContent);
        writeIntoFile(encraptedContent);
    }

    private static String readFromFile() {
        System.out.print("请输入加密的文件路径:");
        scanner = new Scanner(System.in);
        String fileName = scanner.nextLine();
        file = new File(fileName);
        try{
            BufferedReader reader = new BufferedReader(new FileReader(file));
            StringBuilder fileContentBuilder = new StringBuilder();
            String tempString = null;
            while((tempString = reader.readLine()) != null) {
                fileContentBuilder.append(tempString);
                fileContentBuilder.append("\n");
            }
            reader.close();
            return fileContentBuilder.toString();
        }catch(IOException exception) {
            System.out.println("读入文件失败!请检查文件路径!");
            return null;
        }
    }

    private static HashMap<Character, Double> calculateCharChance(String fileContent) {
        if(fileContent == null) {
            return null;
        }
        HashMap<Character, Double> charTimes = new HashMap<>();
        for(int i = 0; i < fileContent.length(); i ++) {
            char tempChar = fileContent.charAt(i);
            if(charTimes.containsKey(tempChar)) {
                charTimes.put(tempChar, charTimes.get(tempChar) + 1d);
            } else {
                charTimes.put(tempChar, 1d);
            }
        }
        HashMap<Character, Double> perChance = new HashMap<>();
        for(Map.Entry<Character, Double> entry : charTimes.entrySet()) {
            perChance.put(entry.getKey(), entry.getValue() / fileContent.length());
        }
        return perChance;
    }

    private static TreeNode buildHuffmanTree(HashMap<Character, Double> perChance) {
        if(perChance == null) {
            return null;
        }
        ArrayList<TreeNode> treeNodes = new ArrayList<>();
        leafNodes = new HashMap<>();
        for(Map.Entry<Character, Double> entry : perChance.entrySet()) {
            TreeNode tempNode = new TreeNode(entry.getKey(), entry.getValue());
            treeNodes.add(tempNode);
            leafNodes.put(tempNode.getCurrentChar(), tempNode);
        }
        TreeNodeComparator comparator = new TreeNodeComparator();
        while(treeNodes.size() != 1) {
            Collections.sort(treeNodes, comparator);
            TreeNode tempNode1 = treeNodes.remove(0);
            TreeNode tempNode2 = treeNodes.remove(0);
            TreeNode tempTop = new TreeNode(tempNode1.getChance() + tempNode2.getChance());
            tempTop.setLeftNode(tempNode1);
            tempTop.setRightNode(tempNode2);
            tempNode1.setParentNode(tempTop);
            tempNode2.setParentNode(tempTop);
            treeNodes.add(tempTop);
        }
        return treeNodes.get(0);
    }

    private static String encraptContent(String fileContent) {
        if(fileContent == null) {
            return null;
        }
        HashMap<Character, String> codeMap = new HashMap<>();
        for(Map.Entry<Character, TreeNode> entry : leafNodes.entrySet()) {
            TreeNode tempNode = entry.getValue();
            TreeNode tempHeadNode = null;
            String code = "";
            while(tempNode.getParentNode() != null) {
                tempHeadNode = tempNode.getParentNode();
                if(tempNode.equals(tempHeadNode.getLeftNode())) {
                    code += "0";
                } else {
                    code += "1";
                }
                tempNode = tempNode.getParentNode();
            }
            codeMap.put(entry.getValue().getCurrentChar(), new StringBuilder(code).reverse().toString());
        }
        StringBuilder encraptedContent = new StringBuilder();
        for(int i = 0; i < fileContent.length(); i ++) {
            encraptedContent.append(codeMap.get(fileContent.charAt(i)));
        }
        binaryNumber = encraptedContent.length();
        encraptedContent = encraptedContent.insert(0, toFullBinaryString(binaryNumber));
        int moreZero = 8 - encraptedContent.length() % 8;
        for(int i = 0; i < moreZero; i ++) {
            encraptedContent.append("0");
        }
        return encraptedContent.toString();
    }

    public static String toFullBinaryString(int num) {
        char[] chs = new char[Integer.SIZE];
        for (int i = 0; i < Integer.SIZE; i++) {
            chs[Integer.SIZE - 1 - i] = (char) (((num >> i) & 1) + '0');
        }
        return new String(chs);
    }

    private static void writeIntoFile(String encraptedContent) {
        if(encraptedContent == null) {
            return;
        }
        char num1 = "1".charAt(0);
        int byteLength = encraptedContent.length() / 8;
        byte[] bytes = new byte[byteLength];
        for(int i = 0; i < byteLength; i ++) {
            bytes[i] = (byte)0;
        }
        for(int i = 0; i < encraptedContent.length(); i ++) {
            char tempBinary = encraptedContent.charAt(i);
            if(tempBinary == num1) {
                switch(i % 8) {
                    case 0: bytes[i/8] = (byte)((int)bytes[i/8] | 0x80);break;
                    case 1: bytes[i/8] = (byte)((int)bytes[i/8] | 0x40);break;
                    case 2: bytes[i/8] = (byte)((int)bytes[i/8] | 0x20);break;
                    case 3: bytes[i/8] = (byte)((int)bytes[i/8] | 0x10);break;
                    case 4: bytes[i/8] = (byte)((int)bytes[i/8] | 0x8);break;
                    case 5: bytes[i/8] = (byte)((int)bytes[i/8] | 0x4);break;
                    case 6: bytes[i/8] = (byte)((int)bytes[i/8] | 0x2);break;
                    case 7: bytes[i/8] = (byte)((int)bytes[i/8] | 0x1);break;
                }
            }
        }
        File encrapyFile = new File(file.getName().split("\\.")[0] + ".encrapy");
        File keyFile = new File(file.getName().split("\\.")[0] + ".key");
        try{
            FileOutputStream outputStream = new FileOutputStream(encrapyFile);
            outputStream.write(bytes);
            outputStream.flush();
            outputStream.close();
            ObjectOutputStream objectOutputStream = new ObjectOutputStream(new FileOutputStream(keyFile));
            objectOutputStream.writeObject(topNode);
            objectOutputStream.close();
        }catch(Exception e) {
            System.out.println("加密后文件写入失败!");
        }
        System.out.println("\n密码文件为 " + encrapyFile.getName() + "\nKey文件为 " + keyFile.getName());
        System.out.println("压缩前文件大小 " + file.length() + " Byte\n压缩后文件大小 " + encrapyFile.length() + " Byte");
        System.out.printf("压缩率:%.2f%%\n", (((float)file.length() - (float)encrapyFile.length()) / (float)file.length()) * 100);
    }

    private static void decrapt() {
        String encraptedContent = readEncraptedContent();
        readKeyFile();
        decraptContent(encraptedContent);
    }

    private static String readEncraptedContent() {
        scanner = new Scanner(System.in);
        System.out.print("请输入密码文件路径:");
        String encrapyFileName = scanner.nextLine();
        byte[] bytes = null;
        try{
            InputStream inputStream = new FileInputStream(encrapyFileName);
            ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
            byte[] buffer = new byte[1024 * 4];
            int n = 0;
            while((n = inputStream.read(buffer)) != -1) {
                byteArrayOutputStream.write(buffer, 0, n);
            }
            bytes = byteArrayOutputStream.toByteArray();
            byteArrayOutputStream.close();
            inputStream.close();
        }catch(IOException e) {
            System.out.println("读入密码文件失败!请检查文件路径!");
            return null;
        }
        StringBuilder builder = new StringBuilder();
        for(byte tempByte : bytes) {
            builder.append(Integer.toBinaryString((tempByte & 0xFF) + 0x100).substring(1));
        }
        return builder.toString();
    }

    private static void readKeyFile() {
        scanner = new Scanner(System.in);
        System.out.print("请输入Key文件路径:");
        String keyFileName = scanner.nextLine();
        try{
            ObjectInputStream objectInputStream = new ObjectInputStream(new FileInputStream(keyFileName));
            topNode = (TreeNode)objectInputStream.readObject();
            objectInputStream.close();
        }catch(Exception e) {
            System.out.println("读入Key文件失败!请检查文件路径!");
            topNode = null;
            return;
        }
    }

    private static void decraptContent(String encraptedContent) {
        if(encraptedContent == null) {
            return;
        }
        StringBuilder resultBuilder = new StringBuilder();
        binaryNumber = binaryToTen(encraptedContent.substring(0, 32));
        encraptedContent = encraptedContent.substring(32);
        TreeNode tempNode = topNode;
        for(int i = 0; i < binaryNumber; i ++) {
            if(encraptedContent.charAt(i) == '0') {
                tempNode = tempNode.getLeftNode();
            } else {
                tempNode = tempNode.getRightNode();
            }
            if(tempNode.getCurrentChar() != null) {
                resultBuilder.append(tempNode.getCurrentChar());
                tempNode = topNode;
            }
        }
        System.out.print("解密完成,是否输出(y or n)?");
        String choice = scanner.next();
        if("y".equals(choice)) {
            System.out.println("\n\n" + resultBuilder.toString());
        }
        System.out.print("是否写入文件(y or n)?");
        choice = scanner.next();
        if("y".equals(choice)) {
            System.out.print("请输入待写入文件路径:");
            scanner = new Scanner(System.in);
            String resultFileName = scanner.nextLine();
            try{
                BufferedWriter writer = new BufferedWriter(new FileWriter(resultFileName));
                writer.write(resultBuilder.toString());
                writer.close();
            }catch(Exception e) {
                System.out.println("写入结果文件失败!请检查文件路径!");
            }
        }
    }

    private static int binaryToTen(String binary) {
        int res = 0;
        for(int i = 0; i < 32; i ++) {
            if(binary.charAt(i) == '1') {
                res += Math.pow(2, 31 - i);
            }
        }
        return res;
    }
}

class TreeNodeComparator implements Comparator<TreeNode> {
    @Override
    public int compare(TreeNode node1, TreeNode node2) {
        return node1.getChance() > node2.getChance() ? 1 : -1;
    }
}

class TreeNode implements Serializable{
    private static final long serialVersionUID = 1L;

    private Character currentChar = null;
    private Double chance;
    private TreeNode parentNode;
    private TreeNode leftNode;
    private TreeNode rightNode;

    public TreeNode(char currentChar, double chance) {
        this.currentChar = currentChar;
        this.chance = chance;
    }

    public TreeNode(double chance) {
        this.chance = chance;
    }

    public Character getCurrentChar() {
        return currentChar;
    }

    public Double getChance() {
        return chance;
    }

    public void setParentNode(TreeNode parentNode) {
        this.parentNode = parentNode;
    }

    public TreeNode getParentNode() {
        return parentNode;
    }

    public void setLeftNode(TreeNode leftNode) {
        this.leftNode = leftNode;
    }

    public TreeNode getLeftNode() {
        return leftNode;
    }

    public void setRightNode(TreeNode rightNode) {
        this.rightNode = rightNode;
    }

    public TreeNode getRightNode() {
        return rightNode;
    }
}

猜你喜欢

转载自blog.csdn.net/qq_40856284/article/details/106499243