DSA Greedy / Compression
Huffman Coding
The telemetry alphabet is tiny, but 'OK' appears constantly and 'PANIC' almost never. Fixed 8-bit symbols waste bits. You want a code that minimizes .
Huffman merges the two lightest trees repeatedly. Optimality intuition is an : rarest symbols become deepest siblings.
Algorithm journey
Count frequencies
Known model for this alphabet.
Min-heap of trees
Weight = subtree frequency sum.
Merge two lightest
New parent weight = sum.
Read root-to-leaf bits
Prefix-free by construction.
What actually happens
Naive idea: assign shorter codes ad hoc, or use fixed width. Breaks: ambiguous decoding if not prefix-free, or wasted bits on skewed frequencies.
Better idea: build an optimal binary prefix code for the given frequencies by always combining the two lightest weights. The tree depth of a symbol is its code length.
The improved design still fails as βuniversal compressionβ: wrong frequency model, ignoring headers, or needing dictionary/context methods (LZ) for repeated substrings.
Invariant
Every forest tree is a valid prefix code for its leaves. Merging two trees preserves prefix-freeness. The final tree minimizes weighted path length among binary prefix codes for those frequencies (up to ties).
How to choose Huffman
1. Known symbol frequencies? Huffman is the classic optimal prefix code.
2. Streaming unknown stats? Adaptive Huffman or arithmetic/ANS with models.
3. Repeated phrases? Dictionary methods first; Huffman may still code literals.
Quantitative check
Symbols with freqs 0.5, 0.25, 0.125, 0.125: Huffman lengths 1,2,3,3 give expected length 1.75 bits/symbol vs 2 bits for fixed-width 2-bit codes.
Huffman Tree Builder
Step 1 of 0
π Core Concepts
1. Prefix-Free Codes
No code is a prefix of another code. This ensures unambiguous decoding - you can read the bit stream left-to-right without needing separators!
Example: If 'A' = 0, then no other character can start with 0 (like 01, 001, etc.)
2. Variable-Length Encoding
A byte stream uses fixed 8-bit source symbols; Unicode text first needs a specified encoding such as UTF-8. Huffman uses different output lengths and minimizes average length within its prefix-code model.
Example: 'E' might be 2 bits, 'Z' might be 10 bits
3. Binary Tree Representation
Huffman codes are built from a binary tree where characters are leaves. Left edge = 0, right edge = 1. Path from root to leaf gives the code!
Example: Left-Left-Right = 001
π Did You Know? ZIP, JPEG, MP3!
Huffman coding is a component in many established formats.DEFLATE combines LZ77 with Huffman codes and is common in gzip and ZIP entries; many JPEG streams entropy-code quantized coefficients with Huffman tables; andMP3 uses specified Huffman codebooks for quantized spectral data. Developed by David Huffman in 1952 as a graduate student, it's been compressing data for 70+ years!
π How It Works
Count Character Frequencies
Scan the input and count how many times each character appears.
Create Leaf Nodes
Create a leaf node for each character with its frequency. Put all nodes in a priority queue (min-heap).
Build Tree (Greedy Merging)
Repeatedly: Extract two nodes with lowest frequencies, create a parent node with their sum, add parent back to queue. Continue until one node remains (the root).
Generate Codes
Traverse the tree: assign 0 for left edges, 1 for right edges. The path from root to each leaf is that character's code!
π― Key Insight: Why Greedy Works
Huffman's greedy strategy (always merge two smallest frequencies) is provably optimal for this model.
Proof sketch:
1. A prefix code can be represented by a full binary tree whose cost is Ξ£ frequency(symbol) Γ depth(symbol).
2. In an optimal tree, swap the two least-frequent symbols into a deepest sibling pair; the exchange cannot increase cost.
3. Contract those siblings into one pseudo-symbol with their summed frequency. An optimal solution to the smaller instance expands to an optimal original tree.
This is one of the few greedy algorithms with a mathematical proof of optimality!
π― Visual Example
Encoding "AABBBCCCC"
(ABC):9
/ \
0 1
/ \
(AB):5 C:4
/ \
0 1
/ \
A:2 B:3A = 00 (2 bits)
B = 01 (2 bits)
C = 1 (1 bit)
"AABBBCCCC" β 00 00 01 01 01 1 1 1 1 = 14 payload bits
vs ASCII: 9 chars Γ 8 bits = 72 bits
Payload only: 80.6% fewer bits; a real stream must also carry a header and padding length.
Swapping left and right children changes the bit patterns but not code lengths or cost. The deterministic reference implementation below emits C=0, A=10, B=11 for this histogram; the visual tree is an equally optimal alternative.
π» Deterministic byte-oriented implementation
The reference code models symbols as bytes, defines a total heap tie-break order, handles empty and one-symbol inputs, and validates malformed payloads. Bit strings keep the teaching example readable; a real codec packs bits into bytes.
Python
from __future__ import annotations
from collections import Counter
from dataclasses import dataclass
import heapq
@dataclass
class Node:
frequency: int
symbol: int | None = None
left: "Node | None" = None
right: "Node | None" = None
@property
def is_leaf(self) -> bool:
return self.left is None and self.right is None
def build_tree(data: bytes) -> Node | None:
"""Build a deterministic tree over byte symbols 0..255."""
heap = []
serial = 0
for symbol, frequency in sorted(Counter(data).items()):
node = Node(frequency, symbol)
heapq.heappush(heap, (frequency, symbol, serial, node))
serial += 1
# Invariant: heap entries are roots of disjoint subtrees whose leaves
# partition the source alphabet; their frequencies sum to len(data).
while len(heap) > 1:
left_item = heapq.heappop(heap)
right_item = heapq.heappop(heap)
left, right = left_item[3], right_item[3]
parent = Node(left.frequency + right.frequency, None, left, right)
minimum_symbol = min(left_item[1], right_item[1])
heapq.heappush(
heap, (parent.frequency, minimum_symbol, serial, parent)
)
serial += 1
return heap[0][3] if heap else None
def build_codes(root: Node | None) -> dict[int, str]:
codes: dict[int, str] = {}
def visit(node: Node, prefix: str) -> None:
if node.is_leaf:
assert node.symbol is not None
codes[node.symbol] = prefix or "0"
return
assert node.left is not None and node.right is not None
visit(node.left, prefix + "0")
visit(node.right, prefix + "1")
if root is not None:
visit(root, "")
return codes
def encode(data: bytes) -> tuple[str, dict[int, str], Node | None]:
root = build_tree(data)
codes = build_codes(root)
return "".join(codes[symbol] for symbol in data), codes, root
def decode(bits: str, root: Node | None, expected_symbols: int) -> bytes:
"""Reject invalid bits, truncated paths, extras, and bad singleton data."""
if expected_symbols < 0:
raise ValueError("negative symbol count")
if root is None:
if bits or expected_symbols:
raise ValueError("nonempty payload without a tree")
return b""
if root.is_leaf:
if bits != "0" * expected_symbols:
raise ValueError("invalid single-symbol payload")
assert root.symbol is not None
return bytes([root.symbol]) * expected_symbols
output = bytearray()
current = root
for bit in bits:
if bit not in "01":
raise ValueError("payload contains a non-bit")
current = current.left if bit == "0" else current.right
if current is None:
raise ValueError("bit path is absent from tree")
if current.is_leaf:
assert current.symbol is not None
output.append(current.symbol)
if len(output) > expected_symbols:
raise ValueError("payload contains too many symbols")
current = root
if current is not root or len(output) != expected_symbols:
raise ValueError("truncated or incorrectly sized payload")
return bytes(output)
if __name__ == "__main__":
for sample in (b"", b"AAAA", b"AABBBCCCC", bytes(range(32))):
bits, codes, tree = encode(sample)
assert decode(bits, tree, len(sample)) == sample
bits, codes, tree = encode(b"AABBBCCCC")
assert len(bits) == 14
print(bits, {chr(symbol): code for symbol, code in codes.items()})Java
import java.io.ByteArrayOutputStream;
import java.nio.charset.StandardCharsets;
import java.util.Arrays;
import java.util.Comparator;
import java.util.PriorityQueue;
public class HuffmanCoding {
static final class Node {
final long frequency;
final int symbol; // -1 for internal nodes
final Node left;
final Node right;
Node(long frequency, int symbol, Node left, Node right) {
this.frequency = frequency;
this.symbol = symbol;
this.left = left;
this.right = right;
}
boolean isLeaf() {
return left == null && right == null;
}
}
static final class Entry {
final long frequency;
final int minimumSymbol;
final long serial;
final Node node;
Entry(long frequency, int minimumSymbol, long serial, Node node) {
this.frequency = frequency;
this.minimumSymbol = minimumSymbol;
this.serial = serial;
this.node = node;
}
}
static final class Encoded {
final String bits;
final String[] codes;
final Node root;
final int originalLength;
Encoded(String bits, String[] codes, Node root, int originalLength) {
this.bits = bits;
this.codes = codes;
this.root = root;
this.originalLength = originalLength;
}
}
static Node buildTree(byte[] data) {
long[] frequencies = new long[256];
for (byte value : data) frequencies[value & 0xff]++;
Comparator<Entry> order = Comparator
.comparingLong((Entry entry) -> entry.frequency)
.thenComparingInt(entry -> entry.minimumSymbol)
.thenComparingLong(entry -> entry.serial);
PriorityQueue<Entry> heap = new PriorityQueue<>(order);
long serial = 0L;
for (int symbol = 0; symbol < frequencies.length; symbol++) {
if (frequencies[symbol] > 0L) {
Node leaf = new Node(frequencies[symbol], symbol, null, null);
heap.add(new Entry(frequencies[symbol], symbol, serial++, leaf));
}
}
// Invariant: entries are disjoint subtree roots whose leaf
// frequencies sum to data.length.
while (heap.size() > 1) {
Entry left = heap.remove();
Entry right = heap.remove();
long frequency = Math.addExact(left.frequency, right.frequency);
Node parent = new Node(frequency, -1, left.node, right.node);
heap.add(new Entry(frequency,
Math.min(left.minimumSymbol, right.minimumSymbol),
serial++, parent));
}
return heap.isEmpty() ? null : heap.remove().node;
}
static String[] buildCodes(Node root) {
String[] codes = new String[256];
if (root != null) generateCodes(root, "", codes);
return codes;
}
private static void generateCodes(Node node, String prefix, String[] codes) {
if (node.isLeaf()) {
codes[node.symbol] = prefix.isEmpty() ? "0" : prefix;
return;
}
generateCodes(node.left, prefix + "0", codes);
generateCodes(node.right, prefix + "1", codes);
}
static Encoded encode(byte[] data) {
Node root = buildTree(data);
String[] codes = buildCodes(root);
StringBuilder bits = new StringBuilder();
for (byte value : data) bits.append(codes[value & 0xff]);
return new Encoded(bits.toString(), codes, root, data.length);
}
static byte[] decode(String bits, Node root, int expectedSymbols) {
if (expectedSymbols < 0) throw new IllegalArgumentException("negative size");
if (root == null) {
if (!bits.isEmpty() || expectedSymbols != 0) {
throw new IllegalArgumentException("payload without tree");
}
return new byte[0];
}
if (root.isLeaf()) {
if (bits.length() != expectedSymbols
|| bits.chars().anyMatch(bit -> bit != '0')) {
throw new IllegalArgumentException("invalid singleton payload");
}
byte[] output = new byte[expectedSymbols];
Arrays.fill(output, (byte) root.symbol);
return output;
}
ByteArrayOutputStream output = new ByteArrayOutputStream();
Node current = root;
for (int index = 0; index < bits.length(); index++) {
char bit = bits.charAt(index);
if (bit != '0' && bit != '1') {
throw new IllegalArgumentException("non-bit in payload");
}
current = bit == '0' ? current.left : current.right;
if (current == null) {
throw new IllegalArgumentException("absent tree path");
}
if (current.isLeaf()) {
output.write(current.symbol);
if (output.size() > expectedSymbols) {
throw new IllegalArgumentException("too many symbols");
}
current = root;
}
}
if (current != root || output.size() != expectedSymbols) {
throw new IllegalArgumentException("truncated or mis-sized payload");
}
return output.toByteArray();
}
public static void main(String[] args) {
byte[][] samples = {
new byte[0],
"AAAA".getBytes(StandardCharsets.US_ASCII),
"AABBBCCCC".getBytes(StandardCharsets.US_ASCII)
};
for (byte[] sample : samples) {
Encoded encoded = encode(sample);
assert Arrays.equals(
decode(encoded.bits, encoded.root, encoded.originalLength),
sample);
}
Encoded encoded = encode(samples[2]);
assert encoded.bits.length() == 14;
System.out.println(encoded.bits);
}
}Tree, heap, and decoding invariants
Prefix-free by construction
Symbols appear only at leaves. A leaf cannot be an ancestor of another leaf, so no root-to-leaf code can prefix another. Decoding may emit immediately at a leaf and restart at the root.
Heap partition invariant
Every heap entry roots a subtree over a disjoint symbol set; together those sets partition the alphabet, and root frequency equals the sum of leaf frequencies. A merge preserves all three facts.
Deterministic ties
Equal frequencies can yield different valid trees. If a decoder rebuilds rather than receiving a tree, frequency, minimum symbol, and creation order must be part of the format, or use canonical code lengths.
β‘ Complexity Analysis
Time Complexity
- βO(N + k log k) - Histogram + tree
N source symbols and k distinct symbols; exactly kβ1 heap merges
- βO(N + B) / O(B) - Encode / decode
B is the emitted bit count, which any encoder/decoder must write/read
Space Complexity
- βO(k) metadata + O(B) payload
Tree/code lengths are O(k); materializing the teaching bit string costs O(B)
π‘ Pro Tip: Canonical Huffman Codes
Standard Huffman codes aren't unique - different tree structures can give same compression.Canonical Huffman standardizes codes for a given set of lengths:
- β’ Sort symbols by (code length, symbol value)
- β’ Left-shift when length grows; assign consecutive integers within one length
- β’ Transmit code lengths plus symbol order instead of pointer-shaped tree structure
Decoder and encoder reconstruct the same table from lengths alone. Formats such as DEFLATE specify their own compact representation of those lengths; canonicalization does not eliminate the header.
Canonical worked example
| Canonical order | Length | Assigned code | Rule |
|---|---|---|---|
| C | 1 | 0 | first code at shortest length |
| A | 2 | 10 | increment 0, then shift for the longer length |
| B | 2 | 11 | next integer at the same length |
Code lengths must satisfy Kraft's inequality; malformed or oversubscribed length sets must be rejected while rebuilding the canonical table. Canonical codes change bit patterns, not their lengths or weighted payload cost.
A codec is more than a tree
Header and bit packing
Store a tree preorder, a deterministic frequency table, or canonical lengths; also store the original symbol count or exact payload-bit count so byte padding is not decoded as data. Add a version and checksum when corruption matters.
Decode defensively
Reject non-bits, impossible branches, truncated final paths, oversubscribed canonical lengths, unexpected symbol counts, and resource-exhausting headers. Prefix-free does not mean corrupted input is automatically safe.
Empty and one-symbol alphabets
Empty input has no tree or payload. A one-symbol alphabet needs an explicit convention. Here one zero bit per symbol, plus the original count.
Bytes versus Unicode
Compress bytes for a portable file format. βCharacterβ differs between Python code points, Java UTF-16 code units, and user-perceived grapheme clusters; encode text (for example UTF-8) before building a byte histogram.
When Huffman expands or leaves savings behind
For an ideal memoryless source with at least two positive-probability symbols, the average Huffman length L satisfies H β€ L < H + 1 bits per symbol. That is a payload statement. This page's one-symbol stream deliberately assigns a one-bit code for simple framing, so that degenerate convention is outside the strict bound. A file is smaller only when payload savings exceed the codebook, original-length field, framing, and final-byte padding.
- Small inputs often lose because metadata dominates.
- A nearly uniform alphabet approaches fixed-width coding, leaving little or no payload gain.
- Already compressed, encrypted, or high-entropy bytes usually do not benefit.
- Independent-symbol Huffman ignores repeated strings and context; LZ, arithmetic, or ANS stages can exploit different structure.
- Production decoders may cap code length and use lookup tables, adding constraints absent from the pure greedy tree.
β Pros & β Cons
β Advantages
- βProvably optimal for symbol-by-symbol encoding
- βLossless compression (perfect reconstruction)
- βPrefix-free codes enable unambiguous decoding
- βFast encoding and decoding
- βSimple static model with predictable linear-time payload decoding
β Disadvantages
- βRequires two passes (count frequencies, then encode)
- βMust store/transmit the tree or code table
- βUniform frequencies leave little compression even though the prefix code is still optimal
- βDoesn't exploit patterns beyond single characters
- βOften needs a context/dictionary transform or a finer-grained entropy coder to approach source entropy
πΌ Real World Applications
π¦ File Compression (ZIP, GZIP)
Gzip uses DEFLATE, and ZIP can store DEFLATE-compressed entries. LZ77 first represents repeated strings; Huffman coding then encodes the resulting literal/length and distance alphabets.
Example: .zip, .gz files, HTTP compression
πΌοΈ Image Compression (JPEG)
Many JPEG bitstreams Huffman-code symbols derived from quantized DCT coefficients. Quantization is the lossy stage; Huffman coding itself is a lossless entropy-coding stage.
Example: .jpg, .jpeg images
π΅ Audio Compression (MP3)
MP3 uses Huffman coding to compress quantized frequency coefficients. After psychoacoustic modeling, Huffman reduces the final bitrate.
Example: .mp3 audio files
π‘ Data Transmission
Network protocols use Huffman-based compression to reduce bandwidth. HTTP/2 uses HPACK (Huffman + tables) for header compression.
Example: HTTP/2, fax machines
Short takeaway
Huffman builds an optimal binary prefix code for known frequencies by repeatedly merging the two lightest nodes. Optimality is about weighted path length under a fixed symbol model, not all of compression.
Exercises
- Frequencies 5,2,2,1. Build the Huffman tree (break ties consistently). What are the code lengths?
- Explain in one exchange-style sentence why two rarest symbols can be made siblings in some optimum.
- When would LZ77 + Huffman beat plain Huffman on English text?
Primary sources
- Huffman, D. A. (1952): A method for the construction of minimum-redundancy codes.
- Cormen et al., CLRS: Huffman codes and the greedy-choice / exchange argument.
- Cover & Thomas, Elements of Information Theory: prefix codes and entropy bounds.