What you'll learn
- Quick answer
- The Core Idea: Not Every Symbol Needs the Same Number of Bits
- Building the Tree: How the Codes Actually Get Generated
- Encoding It, Then Proving the Decode Is Exact
- Why Huffman Coding Alone Doesn't Get You a ZIP File
- DEFLATE: How ZIP Actually Combines Both
- Why Compression Ratios Vary Wildly by File Type
- FAQ
Quick Answer
File compression re-encodes data so common patterns take fewer bits than rare ones. Huffman coding does this by building a binary tree from character frequencies and assigning short codes to frequent characters and long codes to rare ones. The ZIP format's DEFLATE algorithm combines this with LZ77, which first replaces repeated chunks of data with short back-references. I implemented real Huffman coding in Python, ran it on an actual string, and confirmed the decoded output matches the original exactly.
The Core Idea: Not Every Symbol Needs the Same Number of Bits
Plain ASCII text spends a fixed 8 bits on every character, no matter how common that character is. David Huffman's 1952 algorithm is built on a simple insight: assign short bit codes to frequent characters and longer codes to rare ones, and the average bits-per-character drops below 8 for any text with an uneven letter distribution — which describes almost all real text and most real data.
To keep every number in this article real rather than illustrative, I ran actual Python code on an actual string: "priodemy teaches coding to students across india every single day" — 65 characters. Its real frequency table, produced by collections.Counter:
Sample: 'priodemy teaches coding to students across india every single day' (65 chars)
Frequency table (highest to lowest):
SPACE: 9 e: 7 s: 6 i: 5 d: 5 o: 4 t: 4 a: 4 n: 4
r: 3 y: 3 c: 3 g: 2
p: 1 m: 1 h: 1 u: 1 v: 1 l: 1
Building the Tree: How the Codes Actually Get Generated
The algorithm: put every character in a min-heap keyed by frequency. Repeatedly pop the two lowest-frequency nodes, merge them into a new internal node whose frequency is their sum, and push that back in. Keep going until one node remains — that's the tree's root. Walking left from the root is a "0", walking right is a "1"; the path from root to a character's leaf is that character's code.
import heapq
from collections import Counter
class Node:
def __init__(self, char, freq, left=None, right=None):
self.char, self.freq, self.left, self.right = char, freq, left, right
def __lt__(self, other):
return self.freq < other.freq
def build_tree(freqs):
heap, counter = [], 0
for ch, fr in freqs.items():
heapq.heappush(heap, (fr, counter, Node(ch, fr)))
counter += 1
while len(heap) > 1:
f1, c1, n1 = heapq.heappop(heap)
f2, c2, n2 = heapq.heappop(heap)
merged = Node(None, f1 + f2, n1, n2)
heapq.heappush(heap, (merged.freq, counter, merged))
counter += 1
return heap[0][2]
def build_codes(node, prefix="", codebook=None):
codebook = codebook or {}
if node.char is not None:
codebook[node.char] = prefix or "0"
return codebook
build_codes(node.left, prefix + "0", codebook)
build_codes(node.right, prefix + "1", codebook)
return codebookBecause the two rarest characters always get merged first, they end up buried deepest in the tree — the longest codes. Because frequent characters only get merged in later, once bigger clusters already exist, they stay shallow — short codes. Running build_tree and build_codes on the real frequency table above produced:
Generated Huffman codes:
SPACE: 101 (3 bits)
e: 001 (3 bits)
a: 0111 (4 bits) c: 0100 d: 1110 i: 1101
n: 1000 (4 bits) o: 0101 r: 0000 s: 1111 t: 0110 y: 0001
g: 10010 (5 bits)
h: 110000 (6 bits) l: 110011 m: 100111 p: 100110 u: 110001 v: 110010Space (the most frequent symbol, at 9 occurrences) got the shortest code at 3 bits. The six least-frequent letters, each appearing exactly once, all landed at 6 bits — still 2 bits cheaper than fixed 8-bit ASCII, but clearly worse than the high-frequency symbols. That's Huffman coding working exactly as designed.
Encoding It, Then Proving the Decode Is Exact
Encoding just looks up each character's code and concatenates the bits. Decoding walks the same tree bit by bit from the root, and every time it lands on an actual character (a leaf), it records that character and restarts from the root for the next one:
def encode(text, codebook):
return "".join(codebook[ch] for ch in text)
def decode(bits, root):
out, node = [], root
for b in bits:
node = node.left if b == "0" else node.right
if node.char is not None:
out.append(node.char)
node = root
return "".join(out)
tree = build_tree(Counter(sample))
codes = build_codes(tree)
encoded = encode(sample, codes)
decoded = decode(encoded, tree)
assert decoded == sampleReal results from running this end to end:
Original size (8-bit ASCII): 520 bits = 65 bytes
Compressed size (Huffman): 258 bits = 33 bytes (rounded up)
Compression ratio: 49.62% of original size (50.4% space saved)
Decoded matches original exactly: True
Shannon entropy of this exact string: 3.9428 bits/symbol (theoretical minimum)
Huffman actual average: 3.9692 bits/symbol258 bits of actual Huffman-encoded output versus 520 bits for the same string in fixed 8-bit ASCII — just over 50% smaller, on real text, not a cherry-picked example. And critically, decoded == sample was checked with a direct string-equality assertion, not eyeballed: the exact original 65-character string came back out.
One more real, honestly-reported number: the Shannon entropy of this specific string — the theoretical floor on bits-per-symbol that any prefix code could possibly achieve for this exact symbol distribution — works out to 3.9428 bits/symbol. The actual Huffman tree built here averaged 3.9692 bits/symbol. Huffman coding is provably optimal among prefix codes, but it only hits entropy exactly when every symbol's probability happens to be an exact power of one-half; landing close to but not exactly at the theoretical floor is the expected, correct result, not an error in the implementation.
Why Huffman Coding Alone Doesn't Get You a ZIP File
Huffman coding only exploits how often each individual symbol appears. It has no way to notice that a whole sentence got copy-pasted twice, or that the same five-word phrase shows up ten times across a document — it looks at frequency, not repeated structure.
That's where LZ77 comes in, confirmed here against RFC 1951 rather than recited from memory. LZ77 scans through data using a sliding window (up to 32 KB in DEFLATE) looking for a run of bytes that already appeared earlier inside that window, and replaces the repeat with a short (distance, length) back-reference pointing at the earlier occurrence, instead of repeating the actual bytes. This is exactly what catches repeated words, repeated HTML/JSON tags, and repeated whitespace patterns — the kind of redundancy that frequency-based Huffman coding structurally cannot see on its own.
DEFLATE: How ZIP Actually Combines Both
DEFLATE, defined in RFC 1951 (published 1996, originally designed by Phil Katz for PKZIP), is the algorithm underneath the ZIP format — and also gzip and PNG. It runs in two stages back to back: first LZ77 replaces repeated sequences with (distance, length) back-references, producing a shorter stream of literal bytes and back-reference tokens; then Huffman coding is applied on top of that already-shortened stream, so both the literal bytes and the back-reference tokens themselves get frequency-based variable-length codes.
DEFLATE actually supports two distinct Huffman modes: a static mode using a fixed, pre-agreed table that never needs to be transmitted (useful for small files, where storing a custom table would cost more than it saves), and a dynamic mode where the specific Huffman table is derived from that file's own data and stored alongside it — worth it once a file is large enough that a custom table pays for its own storage cost through better compression.
Connecting this back to the worked example: what got hand-built and run in this article is DEFLATE's second stage only. Real ZIP compression on that same 65-character sentence would first run LZ77 over it (which wouldn't find much, since a single short sentence has little internal repetition) and then apply Huffman-style coding similar to what's shown above to whatever LZ77 produced.
Why Compression Ratios Vary Wildly by File Type
This two-stage design explains a pattern every developer runs into eventually. Plain text and source code — full of repeated words, whitespace, and syntax — compress well with DEFLATE/ZIP, often 60–70%+ smaller. Already-compressed formats like JPEG, MP3, or MP4 barely shrink at all when you ZIP them, and sometimes grow slightly: those formats already removed the statistical redundancy DEFLATE is designed to find during their own encoding, so there's nothing left for either LZ77's repeat-finder or Huffman's frequency-based recoding to exploit.
Zipping a folder of JPEGs and seeing “0% saved” isn't a bug in your zip tool — it's confirmation that the JPEG encoder already did that job.
