【问题标题】:My LZW compression program barely compresses我的 LZW 压缩程序几乎没有压缩
【发布时间】:2021-01-31 18:59:05
【问题描述】:

我正在学习一个算法类,我们必须在 Java 中实现 LZW 压缩。我决定为此使用 Trie 数据结构,并且我已经实现了 Trie 并让它工作,但是它非常很慢,而且几乎没有压缩。

我们应该使用 8 位符号并且能够压缩任何二进制文件。

给定一个约 4MB 的文件 (bible.txt),我的代码数组中有大约 549,012 个元素。当我将这些元素写入一个文件(每行一个整数代码)时,我最终得到一个 3.5MB 的“压缩”文件,所以我得到了 0.5MB 的压缩。

我怎样才能使这个程序更有效率?我觉得我在这里误解了一些基本的东西,或者我遗漏了一些明显的东西,但我不知道为什么这不会压缩。

(我从这个网站获得了我的测试文件 bible.txt:https://corpus.canterbury.ac.nz/descriptions/

我从这样的二进制文件中读取字节(读取为 int 并转换为 char 是必要的,因此 0x80 以上的值不是负数):

public String readFile(String path) throws IOException, FileNotFoundException {
    File file = new File(path);

    StringBuilder string = new StringBuilder();

    try (FileInputStream fileInputStream = new FileInputStream(file)) {
        int singleCharInt;
        char singleChar;
        while((singleCharInt = fileInputStream.read()) != -1) {
            singleChar = (char) singleCharInt;
            string.append(singleChar);
        }
    } 

    return string.toString();
}

我的主要方法是这样的

    public static void main(String args[]) throws FileNotFoundException, IOException {
        String bytes = new FileReader().readFile("/home/user/Code/Trie/bible.txt");

        ArrayList<Integer> codes = new Compress().compress(bytes);
    }

我的 Compress 类如下所示

public class Compress {

    private int code = 0;

    public ArrayList<Integer> compress(String data) {
        Trie trie = new Trie();

        // Initialize Trie Data Structure with alphabet (256 possible values with 8-bit
        // symbols)
        for (code = 0; code <= 255; code++) {
            trie.insert(Character.toString((char) code), code);
        }

        code++;

        String s = Character.toString(data.charAt(0));

        ArrayList<Integer> codes = new ArrayList<Integer>();

        for (int i = 1; i < data.length(); i++) {
            String c = Character.toString(data.charAt(i));

            if (trie.find(s + c) > 0) {
                s += c;
            } else {
                codes.add(trie.find(s));
                trie.insert(s + c, code);
                code++;
                s = c;
            }
        }

        codes.add(trie.find(s));

        return codes;
    }

}

我的 Trie 类如下所示:

public class Trie {
    private TrieNode root;

    public Trie() {
        this.root = new TrieNode(false);
    }

    public void insert (String word, int code) {
        TrieNode current = root;

        for (char l: word.toCharArray()) {
            current = current.getChildren().computeIfAbsent(Character.toString(l), c -> new TrieNode(false));
        }
        current.setCode(code);
        current.setWordEnd(true);
    }

    public int find(String word) {
        TrieNode current = root;

        for (int i = 0 ; i < word.length(); i++) {
            char ch = word.charAt(i);

            TrieNode node = current.getChildren().get(Character.toString(ch));

            if (node == null) {
                return -1;
            }

            current = node;
        }

        return current.getCode();
    }
}

我的 TrieNode 类如下所示:

public class TrieNode {
    private HashMap<String, TrieNode> children;
    private int code;
    private boolean wordEnd;

    public TrieNode(boolean wordEnd) {
        this.children = new HashMap<String, TrieNode>();
        this.wordEnd = wordEnd;
    }

    public HashMap<String, TrieNode> getChildren() {
        return this.children;
    }

    public void setWordEnd(boolean wordEnd) {
        this.wordEnd = wordEnd;
    }
    
    public boolean isWordEnd() {
        return this.wordEnd;
    }

    public int getCode() {
        return this.code;
    }

    public void setCode(int code) {
        this.code = code;
    }
}

感谢您的宝贵时间!

【问题讨论】:

  • 您需要先了解intcharbyte 之间的区别,它们如何表示数据,以及Java 如何在它们之间进行转换。您没有压缩,因为您使用 4 个字节(int)来存储每个字节,即占用 4 倍的空间。此外,虽然我没有详细检查您的代码,但您可能还会通过 int 到 char 转换引入损坏。这些基础知识对于教程中的整个章节来说已经足够了,而对于 Stack Overflow 来说太宽泛了。
  • @JimGarrison 我不明白如果 int 不适合存储代码,我应该使用什么。我得到一个大文件的大约 250,000 个代码,它比字节大而且短。下一个选项是 int,但您说它们太大了,所以我不确定替代方案是什么。不过,在我缺乏的基础上,你绝对是对的。
  • 在内部,您最终将使用int,但您使用的输入值都是 1 字节长 (0x00-0xFF),并且由于您预计 250k 代码您将使用可变长度编码。要将压缩数据实际写入文件,您将这些代码转换为可变长度位字符串,将它们打包在缓冲区中,然后写出结果字节。 NIO 缓冲区在这里很有用。您的代码中不应有charString,因为输入数据必须被视为二进制,无需字符集转换。

标签: java compression lzw


【解决方案1】:

这是什么意思:“当我将这些元素写入文件时(每行一个整数代码)”?您为每个代码向文件写入四个字节?您正在写四个字节和换行符?你是在写一个十进制数字和一个新行吗?

无论如何,所有这些都是错误的。您需要将代码存储为 bits。在通常的 LZW 实现中,代码中的位数从 9 开始,然后随着更多代码的创建而增加。进一步进入文件,代码可能是例如 12 位或 13 位。解码器从数据中知道编码器何时递增,因此它总是知道要为下一个代码获取多少位。每隔一段时间重置回 9 位是有意义的,这是由编码器向解码器发出信号的。

那么你如何读取和写入文件的位?您会很快发现没有用于此的功能。你需要自己写。

简而言之,您可以在一个整数中保存一个位缓冲区,使用位移和/或操作将位添加到缓冲区中,以另一个整数跟踪缓冲区中有多少位。对于编码,在将位添加到缓冲区后,您会查看其中是否至少有 8 位。如果是这样,将一个字节写入文件,并从缓冲区中删除 8 位。重复直到缓冲区少于 8 位。

最后必须注意将最后几位写入一个字节,确保您已经考虑过解码器如何知道何时停止解码位。

在解码器端也是一样,从输入文件中读取字节并一次向缓冲区添加 8 位,直到您有足够的位来提取下一个代码。

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2021-02-09
    • 1970-01-01
    • 2012-07-29
    • 2022-08-18
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多