使用哈夫曼树进行文本压缩

实际开发中经常用到压缩软件,比如WinRAR可以对所有文件进行压缩。这里实现一个文本压缩工具,可以对文本文件进行压缩。

实现思路:构建哈夫曼树进行压缩。对于大文件,单线程压缩效率低下,使用多线程并发压缩,频繁的创建并销毁线程会拖慢系统速度,使用线程池进一步优化

哈夫曼树简介

哈夫曼树是一种最优二叉树

定义:给定n个权值作为n个叶子结点,构造一棵二叉树,若树的带权路径长度达到最小,则这棵树被称为哈夫曼树。

带权路径长度计算如下所示

在这里插入图片描述

18和20到根结点的路径为1,7和11到根结点的路径为2,所以整个树的带全路径长度为7 * 2 + 11 * 2 + 18 * 1 + 20 * 1 = 74

这就是一颗最优二叉树,因为权值大的离根结点最近,路径最短,这样可以确保加权之后综合最小。哈夫曼树就采用这个原理,将权值大的结点(出现次数多的结点)放到离根结点最近的地方,编码后的bit数最小,实现一种压缩效果

原始压缩

代码实现如下

HufNode类是哈夫曼树的结点,除了记录左右子结点,还记录对应的字符

public class HufNode {

    char value;
    HufNode left;
    HufNode right;

    public HufNode(char value) {
        this.value = value;
    }

    public HufNode(char value, HufNode left, HufNode right) {
        this.value = value;
        this.left = left;
        this.right = right;
    }
}

封装TempNode类,聚合HufNode构建哈夫曼树。具体过程如下

统计每个字符出现次数后构建HufNode结点,使用出现次数和HufNode对象构建TempNode,将TempNode放到优先队列中,不断从优先队列中取出两个较小的元素构建新结点放回优先队列中,直到优先队列只有一个元素。由于优先队列默认是小根堆,且按照TempNode中的count属性排序,所以可以保证每次取出的是出现次数最少的两个元素

public class TempNode {

    int count;
    HufNode node; 

    public TempNode(int count, HufNode node) {
        this.count = count;
        this.node = node;
    }

}

哈夫曼树实现如下。

该压缩工具实现了异地压缩,可以将文件发送给别人,在另一台主机上解压缩。因此需要将对应的编码关系一起写入压缩文件。使用一个变量存储对应关系。读取文件时需要将编码后字符串及编码表分隔开,使用非asc码字符"李"进行划分,解码时也使用"李"进行划分

public class HuffmanTree {

    // 原始字符串
    public char[] origin;
    // 编码后的字符串
    public String transfer;
    // 哈夫曼树的根结点
    public HufNode root;
    // 编码对应关系
    public Map<Character, StringBuilder> relation = new HashMap<>();

    public HuffmanTree(Map<Character, StringBuilder> relation) {
        buildTree(relation);
    }

    public HuffmanTree(String s) {
        Map<Character, Integer> map = new HashMap<>();
        char[] arr = s.toCharArray();
        origin = arr;

        // 统计字符出现次数
        for (int i = 0; i < arr.length; i++) {
            map.put(arr[i], map.getOrDefault(arr[i], 0) + 1);
        }

        // 使用双向链表存储对应数据及权重,按照权重排序
        Node head = new Node(), tail = new Node();
        head.next = tail; tail.prev = head;
        for (Character c : map.keySet()) {
            Node newNode = new Node(c, map.get(c));
            insert(newNode, head.next, tail);
        }
        if (head.next == tail) {
            root = null;
            // 所有数据放在叶子结点,即使只有一个元素也不能放在根结点,这里放在左子树
        } else if (head.next.next == tail) {
            root = new HufNode('+');
            root.left = new HufNode(head.next.data);
        } else {
            PriorityQueue<TempNode> queue = new PriorityQueue<>(new Comparator<TempNode>() {
                @Override 
                public int compare(TempNode o1, TempNode o2) {
                    return o1.count - o2.count;
                }
            });
            // 从最小值开始取两个最小值依次构成左右子结点,构造父结点并添加到哈希表中
            // 便于后续该结点作为子结点。删除构造好的两个结点,并将新的数据插入到链表中
            for (Character c : map.keySet()) {
                HufNode node = new HufNode(c);
                queue.add(new TempNode(map.get(c), node));
            }
            while (queue.size() > 1) {
                TempNode left = queue.poll(), right = queue.poll();
                queue.add(new TempNode(left.count + right.count, new HufNode(' ', left.node, right.node)));
            }
            root = queue.poll().node;
        }
    }

    public Map<Character, StringBuilder> encode() {
        encode(root, new StringBuilder(""));
        return relation;
    }

    // 如果node为空,直接返回上一级,这样返回的时候字符串直接更新,不用判断
    // 添加到哈希表的时候注意要新建一个字符串,否则使用递归中的字符串,动态变化
    private void encode(HufNode node, StringBuilder res) {
        if (node == null) return;
        if (node.left == null && node.right == null) relation.put(node.value, new StringBuilder(res));
        encode(node.left, res.append('0'));
        res.delete(res.length() - 1, res.length());
        encode(node.right, res.append('1'));
        res.delete(res.length() - 1, res.length());
    }

    // 先将给定字符串转换为对应的编码,然后7位转asc码存储,最后记录剩余的个数
    // 为了跟之前的普通字符区分开,使用非asc码字符李进行区分
    // 最后一个字符是最后一位的位数
    public StringBuilder transfer() {
        StringBuilder temp = new StringBuilder("");
        for (int i = 0; i < origin.length; i++) {
            temp.append(relation.get(origin[i]));
        }
        StringBuilder res = new StringBuilder("");
        int count = 0, iter = 0, times = 1;
        while (iter + 6 < temp.length()) {
            count = 0; times = 1;
            for (int i = 0; i < 7; i++) {
                count += times * (temp.charAt(iter) - '0');
                iter++; times *= 2;
            }
            res.append((char) count);
        }
        count = 0; times = 1; int spare = temp.length() - iter;
        // 注意:这里正向遍历,但是times从1递增到64,所以相当于反过来的字符串转二进制
        // 因此这里times从1开始即可,不用更新到相应倍数
        while (iter < temp.length()) {
            count += times * (temp.charAt(iter) - '0');
            iter++; times *= 2;
        }
        res.append((char) count);
        res.append("李");
        res.append(spare);
        this.transfer = res.toString();
        return res;
    }

    // 将转换后的字符串恢复成原始字符串
    public StringBuilder decode() {
        char[] arr = transfer.toCharArray();
        StringBuilder temp = new StringBuilder("");
        int iter = 0;

        // 处理分割线之前的字符,转成二进制
        // 之前转换的时候相当于字符串反过来,所以这里要颠倒
        while (arr[iter] != '李') {
            temp.append(char2Bin(arr[iter]).reverse());
            iter++;
        }

        // 删除多余字符
        iter++; temp.delete(temp.length() - 7 + transfer.charAt(iter) - '0',
                temp.length());

        // 遍历哈夫曼树恢复字符串
        arr = temp.toString().toCharArray();
        StringBuilder res = new StringBuilder("");
        iter = 0;
        while (iter < arr.length) {
            HufNode node = root;
            while (!(node.left == null && node.right == null)) {
                if (arr[iter] == '0') node = node.left;
                else node = node.right;
                iter++;
            }
            // 到根结点后取对应数据
            res.append(node.value);
        }
        return res;
    }

    // 恢复的时候将字符转换为对应的二进制串,查找哈夫曼树并恢复
    private StringBuilder char2Bin(int num) {
        StringBuilder res = new StringBuilder("0000000");
        int iter = 6;
        while (num > 0) {
            res.setCharAt(iter, (char) (num % 2 + '0'));
            num = num / 2; iter--;
        }
        return res;
    }

    // 根据关系表恢复出对应的哈夫曼树,用于接受压缩文件后的初始化
    public void buildTree(Map<Character, StringBuilder> relation) {
        root = new HufNode('+');
        for (Character c : relation.keySet()) {
            StringBuilder path = relation.get(c);
            HufNode iter = root;
            for (int i = 0; i < path.length(); i++) {
                if (path.charAt(i) == '0') {
                    if (iter.left == null) iter.left = new HufNode('+');
                    iter = iter.left;
                } else {
                    if (iter.right == null) iter.right = new HufNode('+');
                    iter = iter.right;
                }
            }
            iter.value = c;
        }
    }
}

到这里就可以实现对于文本文件的压缩,中英文都可以,由于要存储编码表,中文字符重复出现次数较少,所以会出现中文文本压缩后比原文件还大的情况,没有想到合理的方法解决,算是一个bug吧~~

举例说明,对"我喜欢中国"进行压缩,得到的文件内容如下(示意),除了存储"我喜欢中国"以外还存储了其他字符串,占用内存比原文件还大。

xxxxxxxxxxxxx李
我0
喜100
欢101
中110
国111

不过对于英文文件压缩效果还是明显,压缩率能达到60%

多线程压缩

单线程压缩效率低下,对于大文件,光读取就需要较长时间,再遍历建树耗时较久。使用多线程优化。

Java中创建多线程有三种方式,由于要返回转换后的字符串,这里使用实现Callable接口的方式创建线程

与上面类似,多个线程并发压缩后写入文件,如何区分不同线程的压缩结果呢,这里使用非Asc码字符"竺"进行分隔,解码也通过"竺"划分

代码如下

package huffman;

import java.io.BufferedWriter;
import java.io.File;
import java.io.FileWriter;
import java.io.IOException;
import java.io.RandomAccessFile;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.Callable;
import java.util.concurrent.FutureTask;

public class ImproveZip {

    class ZipThread implements Callable<StringBuilder> {

        private HuffmanTree tree;
        private RandomAccessFile in;
        private int size;

        // 这里需要显示指明长度,否则不足长度位置补空格,也会写入文件
        public ZipThread(RandomAccessFile in, int size) {
            this.in = in;
            this.size = size;
        }

        @Override
        public StringBuilder call() {
            StringBuilder temp = null;
            try {
                // 读取文件,利用哈夫曼树转换,写出到文件
                byte[] filecontent = new byte[size];
                in.read(filecontent, 0, size);
                String content = new String(filecontent, encoding);
                // 构建哈夫曼树并返回转换后字符串
                tree = new HuffmanTree(content);
                temp = tree.transfer().append('\n');
                Map<Character, StringBuilder> relation = tree.relation;
                for (Character c : relation.keySet()) {
                    // 由于转换的时候0无法判断有几个,在最前面加上一个1用于判别
                    temp.append("" + c + bin2dec(relation.get(c).insert(0, '1'))).append('\n');
                }
                // 可能有多个片段,使用竺分隔
                temp.append("竺");
            } catch (Exception e) {
                e.printStackTrace();
            }
            return temp;
        }
    }

    class UnZipThread implements Callable<StringBuilder> {

        private HuffmanTree tree;
        private String content;

        public UnZipThread(String content) {
            this.content = content;
        }

        @Override
        public StringBuilder call() {
            // 转换后字符以"李"结束,分割字符串。可能出现多个"李",从第一个分开
            // 后面不知道有多少个数字,以第一个换行符为分隔
            int spilt = content.indexOf("李"), end = content.indexOf("\n", spilt + 1);
            // 对应关系的字符数组
            char[] arr = content.substring(end + 1).toCharArray();

            // 恢复出映射关系,构建哈夫曼树
            Map<Character, StringBuilder> relation = new HashMap<>();
            char c; int iter = 0, num = 0;
            while (iter < arr.length - 1) {
                c = arr[iter]; iter++; num = 0;
                while (arr[iter] != '\n') {
                    num = num * 10 + arr[iter] - '0';
                    iter++;
                }
                iter++;
                relation.put(c, dec2bin(num));
            }
            tree = new HuffmanTree(relation);

            // 设置编码后字符串并恢复出原字符串
            tree.transfer = content.substring(0, end);
            return tree.decode();
        }
    }

    private static final int MAX_SIZE = 1000000;
    private static final String encoding = "UTF-8";

    public void zip(String path) {
        RandomAccessFile in = null;
        BufferedWriter out = null; FileWriter writer = null;
        File file = new File(path);

        // 读取文件
        Long filelength = file.length();
        // 按容量划分线程,结果存储到哈希表中,拼串后写入文件
        int len = (int) (filelength / MAX_SIZE);
        Map<Integer, StringBuilder> map = new HashMap<>(len + 1);
        try {
            in = new RandomAccessFile(path, "r");
            // 多线程读取文件建树
            for (int i = 0; i < len + 1; i++) {
                FutureTask<StringBuilder> task = new FutureTask<>(new ZipThread(in, (int) Math.min(MAX_SIZE, filelength)));
                filelength -= MAX_SIZE;
                Thread thread = new Thread(task);
                thread.start();
                map.put(i, task.get());
            }

            // File类用于描述目录或文件,不能改变或读写文件内容
            // RandomAccessFile用于改变读写文件内容。
            File outputFile = new File(path.replace(".txt", "-zip.txt"));
            outputFile.createNewFile();
            writer = new FileWriter(outputFile);
            out = new BufferedWriter(writer);
            for (int i = 0; i < len + 1; i++) {
                String s = map.get(i).toString();
                if (s != null && s.length() != 0) out.write(s);
            }
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            try {
                if (out != null) out.close();
                if (in != null) in.close();
            } catch (IOException e) {
                System.out.println("文件流关闭异常");
            }
        }
    }

    public void unzip(String path) {
        RandomAccessFile in = null;
        BufferedWriter out = null; FileWriter writer = null;
        File file = new File(path);
        String encoding = "UTF-8";
        // 读取文件全部内容,以"竺"分隔,多线程解码,最后拼串
        Long filelength = file.length();
        byte[] filecontent = new byte[filelength.intValue()];
        try {
            in = new RandomAccessFile(path, "r");

            // 读取文件全部内容
            in.read(filecontent);
            String content = new String(filecontent, encoding);
            String[] split = content.split("竺");

            // 注意:最后的"竺"并不会多出来一个空串,所以长度恰好是个数
            Map<Integer, StringBuilder> map = new HashMap<>(split.length);
            for (int i = 0; i < split.length; i++) {
                FutureTask<StringBuilder> task = new FutureTask<>(new UnZipThread(split[i]));
                Thread thread = new Thread(task);
                thread.start();
                map.put(i, task.get());
            }

            // 拼串,写出到文件
            File outputFile = new File(path.replace("-zip.txt", "-recover.txt"));
            outputFile.createNewFile();
            writer = new FileWriter(outputFile);
            out = new BufferedWriter(writer);
            // 统一输出,保证输出顺序,没办法多线程同时写文件。
            for (int i = 0; i < split.length; i++) {
                out.write(map.get(i).toString());
            }
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            try {
                if (out != null) out.close();
                if (in != null) in.close();
            } catch (IOException e) {
                System.out.println("文件流关闭异常");
            }
        }
    }

    // 二进制转换为十进制存储
    private int bin2dec(StringBuilder bin) {
        int res = 0, times = 1;
        for (int i = bin.length() - 1; i > -1; i--, times *= 2) {
            res += (bin.charAt(i) - '0') * times;
        }
        return res;
    }

    // 十进制转换为对应的二进制编码
    private StringBuilder dec2bin(int num) {
        StringBuilder res = new StringBuilder();
        while (num > 1) {
            res.append(num % 2);
            num = num / 2;
        }
        res.append(num);
        // 由于存储的时候首位多存了1,这里删除
        // 注意顺序:先删除末尾字符,然后颠倒字符串
        return res.delete(res.length() - 1, res.length()).reverse();
    }
}

线程池优化

使用多线程频繁的创建销毁线程,拖慢系统速度,使用线程池进行优化,代码如下。逻辑思路与前面一模一样,只是将多线程的创建销毁改成了使用线程池

package huffman;

import java.io.BufferedWriter;
import java.io.File;
import java.io.FileWriter;
import java.io.IOException;
import java.io.RandomAccessFile;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.Callable;
import java.util.concurrent.Future;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;

public class ImproveZip2 {

    class ZipThread implements Callable<StringBuilder> {

        private HuffmanTree tree;
        private RandomAccessFile in;
        private int size;

        // 这里需要显示指明长度,否则不足长度位置补空格,也会写入文件
        public ZipThread(RandomAccessFile in) {
            this.in = in;
        }

        public void setSize(int size) {
            this.size = size;
        }

        @Override
        public StringBuilder call() {
            StringBuilder temp = null;
            try {
                // 读取文件,利用哈夫曼树转换,写出到文件
                byte[] filecontent = new byte[size];
                in.read(filecontent, 0, size);
                String content = new String(filecontent, encoding);
                tree = new HuffmanTree(content);
                temp = tree.transfer().append('\n');
                Map<Character, StringBuilder> relation = tree.relation;
                for (Character c : relation.keySet()) {
                    // 由于转换的时候0无法判断有几个,在最前面加上一个1用于判别
                    temp.append("" + c + bin2dec(relation.get(c).insert(0, '1'))).append('\n');
                }
                // 可能有多个片段,使用竺分隔
                temp.append("竺");
            } catch (Exception e) {
                e.printStackTrace();
            }
            return temp;
        }
    }

    class UnZipThread implements Callable<StringBuilder> {

        private HuffmanTree tree;
        private String content;

        public UnZipThread() { }

        public void setContent(String content) {
            this.content = content;
        }

        @Override
        public StringBuilder call() {
            // 转换后字符以"李"结束,分割字符串。可能出现多个"李",这里从第一个分开
            // 后面不知道有多少个数字,以第一个换行符为分隔
            int spilt = content.indexOf("李"), end = content.indexOf("\n", spilt + 1);
            System.out.println("spilt = " + spilt + ", end = " + end);
            // 对应关系的字符数组
            char[] arr = content.substring(end + 1).toCharArray();

            // 恢复出映射关系,构建哈夫曼树
            Map<Character, StringBuilder> relation = new HashMap<>();
            char c; int iter = 0, num = 0;
            while (iter < arr.length - 1) {
                c = arr[iter]; iter++; num = 0;
                while (arr[iter] != '\n') {
                    num = num * 10 + arr[iter] - '0';
                    iter++;
                }
                iter++;
                relation.put(c, dec2bin(num));
            }
            tree = new HuffmanTree(relation);

            // 设置编码后字符串并恢复出原字符串
            tree.transfer = content.substring(0, end);
            return tree.decode();
        }
    }

    private static final int MAX_SIZE = 10000;
    private static final String encoding = "UTF-8";

    private ThreadPoolExecutor zip = new ThreadPoolExecutor(
            20, 50, 2,
            TimeUnit.SECONDS, new ArrayBlockingQueue<>(100),
            new ThreadPoolExecutor.CallerRunsPolicy());
    private ThreadPoolExecutor unZip = new ThreadPoolExecutor(
            20, 50, 2,
            TimeUnit.SECONDS, new ArrayBlockingQueue<>(100),
            new ThreadPoolExecutor.CallerRunsPolicy());

    public void zip(String path) {

        RandomAccessFile in = null;
        BufferedWriter out = null; FileWriter writer = null;
        Long filelength = new File(path).length();
        int len = (int) (filelength / MAX_SIZE);
        Map<Integer, Future<StringBuilder>> map = new HashMap<>(len + 1);
        try {
            in = new RandomAccessFile(path, "r");
            ZipThread thread = new ZipThread(in);
            for (int i = 0; i < len + 1; i++) {
                thread.setSize(Math.min(MAX_SIZE, filelength.intValue()));
                Future<StringBuilder> res = zip.submit(thread);
                filelength -= MAX_SIZE;
                map.put(i, res);
            }
            File outputFile = new File(path.replace(".txt", "-zip.txt"));
            outputFile.createNewFile();
            writer = new FileWriter(outputFile);
            out = new BufferedWriter(writer);
            for (int i = 0; i < len + 1; i++) {
                StringBuilder s = map.get(i).get();
                if (s != null && s.length() != 0) out.write(s.toString());
            }
        } catch (IOException e) {
            e.printStackTrace();
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            zip.shutdown();
            try {
                if (out != null) out.close();
                if (writer != null) writer.close();
                if (in != null) in.close();
            } catch (IOException e) {
                System.out.println("文件流关闭异常");
            }
        }
    }

    public void unzip(String path) {
        RandomAccessFile in = null; BufferedWriter out = null;
        FileWriter writer = null;
        Long filelength = new File(path).length();
        byte[] filecontent = new byte[filelength.intValue()];
        try {
            in = new RandomAccessFile(path, "r");
            in.read(filecontent);
            // 读取文件全部内容
            String content = new String(filecontent, encoding);
            String[] split = content.split("竺");
            Map<Integer, Future<StringBuilder>> map = new HashMap<>(split.length);
            UnZipThread thread = new UnZipThread();
            for (int i = 0; i < split.length; i++) {
                thread.setContent(split[i]);
                Future<StringBuilder> res = unZip.submit(thread);
                map.put(i, res);
            }

            // 统一输出,为了确认顺序,没办法。多路同时写也会有线程安全问题
            File outputFile = new File(path.replace("-zip.txt", "-recover.txt"));
            outputFile.createNewFile();
            writer = new FileWriter(outputFile);
            out = new BufferedWriter(writer);
            for (int i = 0; i < split.length; i++) {
                out.write(map.get(i).get().toString());
            }
        } catch (IOException e) {
            e.printStackTrace();
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            unZip.shutdown();
            try {
                if (out != null) out.close();
                if (writer != null) writer.close();
                if (in != null) in.close();
            } catch (IOException e) {
                System.out.println("文件流关闭异常");
            }
        }
    }

    // 二进制转换为十进制存储
    private int bin2dec(StringBuilder bin) {
        int res = 0, times = 1;
        for (int i = bin.length() - 1; i > -1; i--, times *= 2) {
            res += (bin.charAt(i) - '0') * times;
        }
        return res;
    }

    // 十进制转换为对应的二进制编码
    private StringBuilder dec2bin(int num) {
        StringBuilder res = new StringBuilder();
        while (num > 1) {
            res.append(num % 2);
            num = num / 2;
        }
        res.append(num);
        // 由于存储的时候首位多存了1,这里删除
        // 注意顺序:先删除末尾字符,然后颠倒字符串
        return res.delete(res.length() - 1, res.length()).reverse();
    }
}

代码优化

上述代码为了调试方便,大多数代码直接复制粘贴。后续进行优化,提取出来了许多通用的方法,封装成Utils类,优化后所有类的代码如下所示

Node类

package huffman;

public class Node {
    
    char data;
    int count;
    Node prev;
    Node next;
    
    public Node() { }
    
    public Node(char data, int count) {
        this.data = data;
        this.count = count;
    }
    
    public Node(char data, int count, Node prev, Node next) {
        this.data = data;
        this.count = count;
        this.next = next;
        this.prev = prev;
    }
}

HufNode类

package huffman;

public class HufNode {

    char value;
    HufNode left;
    HufNode right;

    public HufNode(char value) {
        this.value = value;
    }

    public HufNode(char value, HufNode left, HufNode right) {
        this.value = value;
        this.left = left;
        this.right = right;
    }
}

HuffmanTree类

package huffman;

import java.util.Comparator;
import java.util.HashMap;
import java.util.Map;
import java.util.PriorityQueue;

public class HuffmanTree {

    // 原始字符串
    public char[] origin;
    // 哈夫曼树的根结点
    public HufNode root;
    // 编码对应关系
    public Map<Character, StringBuilder> relation = new HashMap<>();

    public Map<Character, StringBuilder> getRelation() {
        return relation;
    }

    // 根据字符串初始化哈夫曼树
    public HuffmanTree(String s) {
        Map<Character, Integer> map = new HashMap<>();
        char[] arr = s.toCharArray();
        this.origin = arr;

        // 统计字符出现次数
        for (int i = 0; i < arr.length; i++) {
            map.put(arr[i], map.getOrDefault(arr[i], 0) + 1);
        }

        if (map.isEmpty()) {
            root = null;
            // 所有数据放在叶子结点,即使只有一个元素也不能放在根结点,这里放在左子树
        } else if (map.size() == 1) {
            root = new HufNode(' ');
            char data = ' ';
            for (Character c : map.keySet()) {
                data = c;
            }
            root.left = new HufNode(data);
        } else {
            PriorityQueue<TempNode> queue = new PriorityQueue<>(new Comparator<TempNode>() {
                @Override 
                public int compare(TempNode o1, TempNode o2) {
                    return o1.count - o2.count;
                }
            });
            // 从最小值开始取两个最小值依次构成左右子结点,构造父结点并添加到哈希表中
            // 便于后续该结点作为子结点。删除构造好的两个结点,并将新的数据插入到链表中
            for (Character c : map.keySet()) {
                HufNode node = new HufNode(c);
                queue.add(new TempNode(map.get(c), node));
            }
            while (queue.size() > 1) {
                TempNode left = queue.poll(), right = queue.poll();
                queue.add(new TempNode(left.count + right.count, new HufNode(' ', left.node, right.node)));
            }
            root = queue.poll().node;
        }
        // 说明:构建哈夫曼树后需要编码,直接写死在构造方法中,防止遗忘
        // 注意大括号,都要编码,不能放在else里面
        encode();
    }

    // 根据关系表恢复出对应的哈夫曼树,用于接受压缩文件后的初始化
    public HuffmanTree(Map<Character, StringBuilder> relation) {
        root = new HufNode('+');
        for (Character c : relation.keySet()) {
            StringBuilder path = relation.get(c);
            HufNode iter = root;
            for (int i = 0; i < path.length(); i++) {
                if (path.charAt(i) == '0') {
                    if (iter.left == null) iter.left = new HufNode('+');
                    iter = iter.left;
                } else {
                    if (iter.right == null) iter.right = new HufNode('+');
                    iter = iter.right;
                }
            }
            iter.value = c;
        }
    }

    // 编码哈夫曼树中的结点
    public Map<Character, StringBuilder> encode() {
        encode(root, new StringBuilder(""));
        return relation;
    }

    // 如果node为空,直接返回上一级,这样返回的时候字符串直接更新,不用判断
    // 添加到哈希表的时候注意要新建一个字符串,否则使用递归中的字符串,动态变化
    private void encode(HufNode node, StringBuilder res) {
        if (node == null) return;
        if (node.left == null && node.right == null) relation.put(node.value, new StringBuilder(res));
        encode(node.left, res.append('0'));
        res.delete(res.length() - 1, res.length());
        encode(node.right, res.append('1'));
        res.delete(res.length() - 1, res.length());
    }

    // 先编码原字符串,然后7位转asc码存储,最后记录最后一个字符用到的位数
    // 为了跟之前的普通字符区分开,使用非asc码字符李进行区分,至于为什么,哈哈哈
    public StringBuilder transfer() {
        StringBuilder temp = new StringBuilder("");
        for (int i = 0; i < origin.length; i++) {
            temp.append(relation.get(origin[i]));
        }
        StringBuilder res = new StringBuilder("");
        int count = 0, iter = 0, times = 1;
        // 确保有7位
        while (iter + 6 < temp.length()) {
            count = 0; times = 1;
            for (int i = 0; i < 7; i++) {
                count += times * (temp.charAt(iter) - '0');
                iter++; times *= 2;
            }
            res.append((char) count);
        }
        // 最后一位可能不足7,单独写
        count = 0; times = 1; int spare = temp.length() - iter;
        // 注意:这里正向遍历,但是times从1递增到64,所以相当于反过来的字符串转二进制
        // 因此这里times从1开始
        while (iter < temp.length()) {
            count += times * (temp.charAt(iter) - '0');
            iter++; times *= 2;
        }
        res.append((char) count);
        res.append("李").append(spare);
        return res;
    }

    // 将转换后的字符串恢复成原始字符串
    public StringBuilder decode(String transfer) {
        char[] arr = transfer.toCharArray();
        StringBuilder temp = new StringBuilder("");
        int iter = 0;

        // 处理分割线之前的字符,转成二进制
        // 之前转换的时候相当于字符串反过来,所以这里要颠倒
        while (arr[iter] != '李') {
            temp.append(Utils.char2Bin(arr[iter]).reverse());
            iter++;
        }

        // 删除多余字符
        iter++; temp.delete(temp.length() - 7 + transfer.charAt(iter) - '0',
                temp.length());

        // 遍历哈夫曼树恢复字符串
        arr = temp.toString().toCharArray();
        StringBuilder res = new StringBuilder("");
        iter = 0;
        while (iter < arr.length) {
            HufNode node = root;
            while (!(node.left == null && node.right == null)) {
                if (arr[iter] == '0') node = node.left;
                else node = node.right;
                iter++;
            }
            // 到根结点后取对应数据
            res.append(node.value);
        }
        return res;
    }

}

Utils工具类

package huffman;

import java.io.BufferedWriter;
import java.io.File;
import java.io.FileWriter;
import java.io.IOException;
import java.io.RandomAccessFile;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.Future;

public class Utils {

    // 压缩线程,并发读文件,构建哈夫曼树,返回编码结果。分隔位置没有关系,反正都要转换
    // 这里需要显示指明长度,否则不足长度位置补空,也会写入文件
    static class ZipThread implements Callable<StringBuilder> {

        private HuffmanTree tree;
        private String path;
        private int size;

        public ZipThread(String path, int size) {
            this.path = path;
            this.size = size;
        }

        @Override
        public StringBuilder call() {

            // 读取文件,构建哈夫曼树
            String content = conReadFileWithLength(path, size);
            tree = new HuffmanTree(content);

            // 依次拼接编码后字符串及编码表
            StringBuilder res = joinInfo(tree);

            // 可能有多个片段,使用竺分隔
            return res.append("竺");
        }
    }

    // 并发解压缩,使用编码后的片段初始化
    static class UnZipThread implements Callable<StringBuilder> {

        private HuffmanTree tree;
        private String content;

        public UnZipThread(String content) {
            this.content = content;
        }

        @Override
        public StringBuilder call() {

            // 转换后字符以"李"结束,分割字符串。可能出现多个"李",这里从第一个分开
            int spilt = content.indexOf("李"), end = content.indexOf("\n", spilt + 1);

            // 分隔映射关系字符数组
            char[] arr = content.substring(end + 1).toCharArray();

            // 恢复出映射关系,构建哈夫曼树
            tree = new HuffmanTree(initialRelation(arr));

            // 恢复出原字符串
            return tree.decode(content.substring(0, end));
        }
    }

    // 解码的时候将字符转换为对应的二进制串
    // 存储的时候正向存取,越靠后次数越高,还原的时候逆向还原,高位放后面,确保顺序正确
    public static StringBuilder char2Bin(int num) {
        StringBuilder res = new StringBuilder("0000000");
        int iter = 6;
        while (num > 0) {
            res.setCharAt(iter, (char) (num % 2 + '0'));
            num = num / 2; iter--;
        }
        return res;
    }

    // 还原映射关系表时十进制转换为对应的二进制编码
    public static StringBuilder dec2bin(int num) {
        StringBuilder res = new StringBuilder();
        while (num > 1) {
            res.append(num % 2);
            num = num / 2;
        }
        res.append(num);
        // 由于存储的时候首位多存了1,这里删除
        // 注意顺序:先删除末尾字符,然后颠倒字符串
        return res.delete(res.length() - 1, res.length()).reverse();
    }

    // 存储映射关系表,二进制转换为十进制存储
    public static int bin2dec(StringBuilder bin) {
        int res = 0, times = 1;
        for (int i = bin.length() - 1; i > -1; i--, times *= 2) {
            res += (bin.charAt(i) - '0') * times;
        }
        return res;
    }

    // 恢复出对应的映射关系,先转整数,再转二进制,构建哈希表
    public static Map<Character, StringBuilder> initialRelation(char[] arr) {
        Map<Character, StringBuilder> relation = new HashMap<>();
        char c; int iter = 0, num = 0;
        while (iter < arr.length - 1) {
            c = arr[iter]; iter++; num = 0;
            while (arr[iter] != '\n') {
                num = num * 10 + arr[iter] - '0';
                iter++;
            }
            iter++;
            relation.put(c, dec2bin(num));
        }
        return relation;
    }

    // 编码后字符串 + 对应关系。为了节省空间,将对应编码转化为对应的十进制数据
    public static StringBuilder joinInfo(HuffmanTree tree) {
        StringBuilder temp = tree.transfer().append('\n');
        Map<Character, StringBuilder> relation = tree.getRelation();
        for (Character c : relation.keySet()) {
            // 由于转换的时候0无法判断有几个,在最前面加上一个1用于判别
            temp.append("" + c + Utils.bin2dec(relation.get(c).insert(0, '1')) + "\n");
        }
        return temp;
    }

    // 读取文件全部内容
    public static String readAllFile(String path) {
        String encoding = "UTF-8";
        Long filelength = new File(path).length();
        byte[] filecontent = new byte[filelength.intValue()];
        RandomAccessFile in = null; String content = null;
        try {
            in = new RandomAccessFile(path, "r");
            in.read(filecontent);
            content = new String(filecontent, encoding);
        } catch (IOException e) {
            e.printStackTrace();
        } finally {
            try {
                if (in != null) in.close();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
        return content;
    }

    // 并发读取文件指定大小内容
    public static String conReadFileWithLength(String path, int size) {
        String encoding = "UTF-8";
        RandomAccessFile in = null; String content = null;
        try {
            in = new RandomAccessFile(path, "r");
            byte[] filecontent = new byte[size];
            in.read(filecontent, 0, size);
            content = new String(filecontent, encoding);
        } catch (IOException e) {
            e.printStackTrace();
        } finally {
            try {
                if (in != null) in.close();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
        return content;
    }

    // 将指定内容写出到指定路径文件
    // content可以是字符串,直接写出到文件,也可以是哈希表,遍历写出文件
    public static void writeToFile(String path, Object content) {
        BufferedWriter out = null; FileWriter writer = null;
        try {
            File outputFile = new File(path);
            outputFile.createNewFile();
            writer = new FileWriter(outputFile);
            out = new BufferedWriter(writer);
            if (content instanceof String) {
                out.write((String) content);
            } else if (content instanceof HashMap) {
                HashMap map = (HashMap) content;
                for (int i = 0; i < map.size(); i++) {
                    Future f = (Future) map.get(i);
                    String s = f.get().toString();
                    if (s != null && s.length() != 0) out.write(s);
                }
            }
        } catch (IOException | ExecutionException | InterruptedException e) {
            e.printStackTrace();
        } finally {
            try {
                if (out != null) out.close();
                if (writer != null) writer.close();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }
}

SingleThreadZip类

package huffman;

public class SingleThreadZip {

    private HuffmanTree huffmanTree;

    public void zip(String path) {

        // 读取文件内容,构建哈夫曼树
        String content = Utils.readAllFile(path);
        huffmanTree = new HuffmanTree(content);

        // 编码并写出
        Utils.writeToFile(path.replace(".txt", "-zip.txt"), Utils.joinInfo(huffmanTree).toString());
    }

    public void unzip(String path) {

        // 读取文件
        String content = Utils.readAllFile(path);

        // 转换后字符以"李"结束,分割字符串。可能出现多个"李",这里从第一个分开
        int spilt = content.indexOf("李"), end = content.indexOf('\n', spilt + 1);

        // 恢复映射关系并构建哈夫曼树
        char[] arr = content.substring(end + 1).toCharArray();
        huffmanTree = new HuffmanTree(Utils.initialRelation(arr));

        // 解码并输出到对应文件
        StringBuilder res =  huffmanTree.decode(content.substring(0, end));
        Utils.writeToFile(path.replace("-zip.txt", "-recover.txt"), res.toString());
    }

}

MultiThreadZip类

package huffman;

import java.io.File;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.Future;
import java.util.concurrent.FutureTask;

public class MultiThreadZip {

    // 调参:
    private static final int MAX_SIZE = 1000000;

    public void zip(String path) {

        // 按容量划分线程,结果存储到哈希表中,拼串后写入文件
        // 也可以将StringBuilder结果写入哈希表,为了跟线程池统一,将Future写入哈希表
        Long filelength = new File(path).length();
        int len = (int) (filelength / MAX_SIZE);
        Map<Integer, Future> map = new HashMap<>(len + 1);

        // 多线程读取文件建树
        for (int i = 0; i < len + 1; i++) {
            FutureTask<StringBuilder> task = new FutureTask<>(new Utils.ZipThread(path, (int) Math.min(MAX_SIZE, filelength)));
            filelength -= MAX_SIZE;
            Thread thread = new Thread(task);
            thread.start();
            map.put(i, task);
        }

        // 写出到文件
        Utils.writeToFile(path.replace(".txt", "-zip.txt"), map);
    }

    public void unzip(String path) {

        // 读取文件全部内容,以"竺"分隔
        String content = Utils.readAllFile(path);
        String[] split = content.split("竺");

        // 注意:最后的"竺"并不会多出来一个空串,所以长度恰好是个数
        Map<Integer, Future> map = new HashMap<>(split.length);

        // 多线程解码
        for (int i = 0; i < split.length; i++) {
            FutureTask<StringBuilder> task = new FutureTask<>(new Utils.UnZipThread(split[i]));
            Thread thread = new Thread(task);
            thread.start();
            map.put(i, task);
        }

        // 拼串,写出到文件
        Utils.writeToFile(path.replace("-zip.txt", "-recover.txt"), map);
    }

}

ZipWithThreadPool类

package huffman;

import java.io.File;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.Future;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;

public class ZipWithThreadPool {

    // 调参:
    private static final int MAX_SIZE = 1000000;
    private ThreadPoolExecutor zip = new ThreadPoolExecutor(
            20, 50, 2,
            TimeUnit.SECONDS, new ArrayBlockingQueue<>(100),
            new ThreadPoolExecutor.CallerRunsPolicy());
    private ThreadPoolExecutor unZip = new ThreadPoolExecutor(
            20, 50, 2,
            TimeUnit.SECONDS, new ArrayBlockingQueue<>(100),
            new ThreadPoolExecutor.CallerRunsPolicy());

    public void zip(String path) {

        // 按容量划分线程,结果存储到哈希表中,拼串后写入文件
        Long filelength = new File(path).length();
        int len = (int) (filelength / MAX_SIZE);
        Map<Integer, Future<StringBuilder>> map = new HashMap<>(len + 1);

        // 多线程读取文件建树
        for (int i = 0; i < len + 1; i++) {
            Utils.ZipThread thread = new Utils.ZipThread(path, (int) Math.min(MAX_SIZE, filelength));
            Future<StringBuilder> res = zip.submit(thread);
            filelength -= MAX_SIZE;
            map.put(i, res);
        }

        // 写出到文件
        Utils.writeToFile(path.replace(".txt", "-zip.txt"), map);
        zip.shutdown();
    }

    public void unzip(String path) {

        // 读取文件全部内容并以"竺"分隔,分成编码子序列
        String content = Utils.readAllFile(path);
        String[] split = content.split("竺");

        // 注意:最后的"竺"并不会多出来一个空串,所以长度恰好是个数
        // 多线程解码并放到哈希表中,便于最后的结果输出
        Map<Integer, Future<StringBuilder>> map = new HashMap<>(split.length);
        for (int i = 0; i < split.length; i++) {
            Utils.UnZipThread thread = new Utils.UnZipThread(split[i]);
            Future<StringBuilder> res = unZip.submit(thread);
            map.put(i, res);
        }

        // 统一写出到文件,保证输出顺序,没办法。多路同时写也会有线程安全问题
        Utils.writeToFile(path.replace("-zip.txt", "-recover.txt"), map);
        unZip.shutdown();
    }

}

测试代码

public class Test {

    public static void main(String[] args) {
		long start = 0, end = 0;
        String path = "D:\\Files\\test.txt";

        File outputFile = new File(path.replace(".txt", "-zip.txt"));
        if (outputFile.exists()) outputFile.delete();

        outputFile = new File(path.replace(".txt", "-recover.txt"));
        if (outputFile.exists()) outputFile.delete();

        // 普通的压缩
        start = System.currentTimeMillis();
        TXTZip util = new TXTZip();
        util.zip(path);
        util.unzip(path.replace(".txt", "-zip.txt"));
        end = System.currentTimeMillis();
        System.out.println("程序执行时间:" + (end - start) + "ms");

        // 删除压缩以及解压的文件,便于后续创建,否则追加到原文件后面
        outputFile = new File(path.replace(".txt", "-zip.txt"));
        if (outputFile.exists()) outputFile.delete();

        outputFile = new File(path.replace(".txt", "-recover.txt"));
        if (outputFile.exists()) outputFile.delete();

        // 多线程压缩
        start = System.currentTimeMillis();
        MultiThreadZip util2 = new MultiThreadZip();
        util2.zip(path);
        util2.unzip(path.replace(".txt", "-zip.txt"));
        end = System.currentTimeMillis();
        System.out.println("程序执行时间:" + (end - start) + "ms");

        outputFile = new File(path.replace(".txt", "-zip.txt"));
        if (outputFile.exists()) outputFile.delete();

        outputFile = new File(path.replace(".txt", "-recover.txt"));
        if (outputFile.exists()) outputFile.delete();

        // 使用线程池优化
        start = System.currentTimeMillis();
        ZipWithThreadPool util3 = new ZipWithThreadPool();
        util3.zip(path);
        util3.unzip(path.replace(".txt", "-zip.txt"));
        end = System.currentTimeMillis();
        System.out.println("程序执行时间:" + (end - start) + "ms");

    }
}

针对92M文本文件,运行三次的结果如下
在这里插入图片描述
在这里插入图片描述

在这里插入图片描述

可以看到,原生压缩算法较慢,使用多线程之后效率加快,使用线程池之后效率进一步提高,但不太明显

  • 30
    点赞
  • 33
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值