LZW Compression: Dictionary-Based Encoding and Decoding Explained
Learn LZW compression: dictionary-based encoding and decoding with Python code, real-world incidents, debugging guide, and interview questions.
20+ years shipping performance-critical code where algorithms decide the bill. Drawn from code that ran under real load.
- ✓Basic understanding of data compression concepts
- ✓Familiarity with hash tables or dictionaries
- ✓Python programming experience
- LZW is a lossless compression algorithm that builds a dictionary of substrings during encoding.
- It replaces repeated substrings with single codes, achieving compression.
- The dictionary is adaptive and does not need to be stored separately.
- Decoding reconstructs the dictionary on the fly.
- Used in GIF, TIFF, and Unix compress.
Imagine you have a long list of words and you want to shorten it. You create a shorthand code for each unique word. When you see the word again, you just write the code. LZW does this automatically, learning new codes as it goes, so you don't need a separate codebook.
Have you ever wondered how GIF images or Unix 'compress' achieve such small file sizes? The secret lies in LZW (Lempel-Ziv-Welch) compression, a dictionary-based lossless algorithm that adapts to the data it compresses. Unlike fixed-dictionary methods, LZW builds its dictionary on the fly, encoding repeated patterns as single codes. This makes it incredibly effective for text and images with repetitive content.
In this tutorial, you'll learn the inner workings of LZW compression and decompression, with production-ready Python code. We'll walk through a real-world bug that caused data corruption, provide a debugging guide for production issues, and cover common mistakes. By the end, you'll be able to implement LZW and understand its trade-offs.
LZW is a cornerstone of data compression, and mastering it will deepen your understanding of adaptive algorithms. Let's dive in!
How LZW Compression Works
LZW compression builds a dictionary of substrings encountered during encoding. Initially, the dictionary contains all single characters (e.g., 0-255 for bytes). The encoder reads input character by character, building a current string. As long as the current string is in the dictionary, it continues reading. When the next character would make a string not in the dictionary, it outputs the code for the current string, adds the new string (current + next char) to the dictionary, and resets the current string to the next character.
This adaptive approach allows LZW to learn longer and longer patterns, achieving high compression ratios on repetitive data. The dictionary grows until a predefined maximum size (e.g., 4096 entries for 12-bit codes). After that, the encoder may stop adding new entries or reset the dictionary.
Let's look at a simple example with a string 'ABABABA'. Initial dictionary: A=0, B=1. Encoder steps: - Read A (current='A', in dict). Read B: 'AB' not in dict -> output 0, add 'AB'=2, current='B'. - Read A: 'BA' not in dict -> output 1, add 'BA'=3, current='A'. - Read B: 'AB' in dict -> current='AB'. Read A: 'ABA' not in dict -> output 2, add 'ABA'=4, current='A'. - Read B: 'AB' in dict -> current='AB'. Read A: 'ABA' in dict -> current='ABA'. End of input -> output 4. Output codes: 0,1,2,4. Original length 7, compressed to 4 codes (assuming fixed-width codes).
LZW Decompression: Rebuilding the Dictionary
LZW decompression reconstructs the original data from the compressed codes. It starts with the same initial dictionary (single bytes). It reads codes one by one. For each code, it outputs the corresponding string from the dictionary. Then it updates the dictionary by adding the previous string plus the first character of the current string. This is the classic LZW decoding algorithm.
A special case occurs when the decoder encounters a code that is not yet in the dictionary. This happens when the encoder added a new entry and immediately used it. The decoder can handle this by using the previous decoded string plus its first character.
Example: Decoding codes [0,1,2,4] from earlier. - Code 0 -> output 'A', prev='A'. Next code 1 -> output 'B', add 'A'+'B'='AB'=2, prev='B'. - Code 2 -> output 'AB', add 'B'+'A'='BA'=3, prev='AB'. - Code 4 -> not in dict yet (next code is 4). Use prev='AB' + first char of prev='A' -> 'ABA'=4. Output 'ABA', add 'AB'+'A'='ABA'=4, prev='ABA'. Output: 'A','B','AB','ABA' -> 'ABABABA'.
Handling Dictionary Overflow and Reset
In practice, the dictionary cannot grow indefinitely. A maximum size is set, typically 4096 entries (12-bit codes) or 65536 (16-bit). When the dictionary reaches its maximum, the encoder must decide what to do. Common strategies: 1. Stop adding new entries: The dictionary becomes static. Compression may degrade for new patterns. 2. Reset the dictionary: Clear all entries except the initial 256 single-byte codes. This allows the algorithm to adapt to new data but loses learned patterns. 3. LRU eviction: Remove least recently used entries, but this adds complexity.
In many implementations (e.g., GIF), the dictionary is reset when full. A special 'clear code' (often 256) is inserted into the output stream to signal the decoder to reset its dictionary. This ensures both encoder and decoder stay synchronized.
Let's implement a version with reset support.
Variable-Length Codes and Bit Packing
LZW codes are often stored as variable-length binary strings to save space. Initially, codes are 9 bits (since we have 256+2 codes). As the dictionary grows, the code width increases up to a maximum (e.g., 12 bits). This requires careful bit packing.
For simplicity, many tutorials use fixed-width codes (e.g., 12 bits), but that wastes space for small dictionaries. In production, you'll need to implement a bitstream that writes codes with varying bit lengths.
Here's a simple bit packing approach: start with 9 bits, and when the next code exceeds the current maximum representable value, increase the bit width by 1. The decoder must track the same bit width changes.
Example: Initial max code = 257 (9 bits). When next_code reaches 512, switch to 10 bits, etc.
Real-World Applications and Trade-offs
- GIF images: Compresses graphics with limited colors.
- TIFF images: Optional compression method.
- Unix compress: The 'compress' utility uses LZW.
- PDF: Can use LZW for text streams.
- Advantages: Simple, fast, good compression for repetitive data, no need to store dictionary.
- Disadvantages: Patented (expired now), not as good as modern algorithms like DEFLATE (used in PNG, gzip), dictionary size limits compression on large files.
LZW is a great educational tool for understanding adaptive dictionary compression. For new projects, consider using DEFLATE or LZMA, but LZW's simplicity makes it a classic.
Performance Analysis and Optimization
LZW compression time is O(n) where n is input length, assuming dictionary lookup is O(1) (hash table). Decompression is also O(n). Memory is O(dictionary size).
- Use a trie for dictionary to speed up prefix searches.
- Pre-allocate dictionary to avoid resizing overhead.
- Use integer codes instead of byte strings for keys.
- For large files, reset dictionary periodically to limit memory.
Example: Using a trie for dictionary can improve speed for long matches.
The GIF That Wouldn't Load: A Dictionary Reset Bug
- Always define a maximum dictionary size to prevent memory overflow.
- Reset the dictionary when full to maintain compression efficiency.
- Test with edge cases like highly repetitive data.
- Document the maximum code width (e.g., 12 bits) to ensure interoperability.
- Use clear code to indicate reset in the output stream.
print(len(dictionary))print(max_code)| File | Command / Code | Purpose |
|---|---|---|
| lzw_compress.py | def lzw_compress(data: bytes, max_dict_size: int = 4096) -> list: | How LZW Compression Works |
| lzw_decompress.py | def lzw_decompress(codes: list, max_dict_size: int = 4096) -> bytes: | LZW Decompression |
| lzw_compress_reset.py | def lzw_compress_with_reset(data: bytes, max_dict_size: int = 4096, clear_code: ... | Handling Dictionary Overflow and Reset |
| lzw_bitpacking.py | def write_codes(codes: list, max_bits: int = 12) -> bytes: | Variable-Length Codes and Bit Packing |
| lzw_trie.py | class TrieNode: | Performance Analysis and Optimization |
Key takeaways
Interview Questions on This Topic
Explain how LZW compression works with an example.
Frequently Asked Questions
20+ years shipping performance-critical code where algorithms decide the bill. Drawn from code that ran under real load.
That's Data Compression. Mark it forged?
3 min read · try the examples if you haven't