Huffman Coding
Build an optimal prefix-free binary code by repeatedly merging the two least frequent symbols with a min-heap.
Overview
Given symbol frequencies, Huffman coding produces a prefix-free binary code (no codeword is a prefix of another) that minimizes the total encoded length Σ f_i · len_i. The greedy step: take the two least frequent nodes, merge them into a parent whose frequency is their sum, and repeat until one tree remains. Left edges are 0, right edges are 1; each symbol's code is the path from the root.
It is the prototypical greedy with a [[priority-queue]]: n − 1 merges, each an O(log n) extract-min pair and insert. Huffman codes are optimal among symbol-by-symbol codes and sit inside DEFLATE (zip, PNG), JPEG, and MP3.
Intuition
A mental model before the formal terms.
You are assigning Morse-like codes: frequent letters deserve short codes, rare ones can afford long ones. Building the tree bottom-up, the two rarest symbols are the ones you can most afford to push deepest — make them siblings at the bottom, treat their pair as one new "symbol" with the combined frequency, and repeat.
Prefix-free means the decoder never needs a separator: walk the tree from the root bit by bit; when you hit a leaf, emit that symbol and jump back to the root.
How it works
- Create a leaf for each symbol with its frequency and push all leaves into a min-heap keyed by frequency.
- While the heap has more than one node: pop the two smallest
a,b; create an internal node with frequencya.f + b.fand childrena,b; push it. - The last node is the root. Traverse it, appending
0for left and1for right, to obtain each leaf's codeword. - Encoding is table lookup; decoding walks the tree. The tree (or code lengths) must be transmitted with the data.
Why it works
Greedy-choice property (exchange). In any optimal tree, the two least frequent symbols x, y can be made sibling leaves at maximum depth: take any two deepest siblings a, b in an optimal tree and swap x↔a, y↔b. Since f_x ≤ f_a and f_y ≤ f_b, and x, y move to depth ≥ their old depth while a, b move up, the cost Σ f · depth does not increase. So some optimal tree has x and y as siblings — exactly what the first merge produces.
Optimal substructure. Replace the sibling pair x, y by a single symbol z with f_z = f_x + f_y. Any tree T for the original alphabet with x, y as siblings has cost cost(T') + f_x + f_y where T' is the tree for the reduced alphabet. Minimizing cost(T) therefore equals minimizing cost(T'), and induction on the alphabet size finishes the proof.
Prefix-freeness is automatic because symbols are leaves: no leaf is an ancestor of another, so no codeword is a prefix of another.
Recognition
How to tell a problem wants this.
- "Minimize total cost where cost = Σ weight × depth" — file compression, but also minimum cost to merge files / ropes / stones when the merge cost is the sum (the merge order is a Huffman tree).
- "Prefix-free code", "variable-length encoding", "optimal binary tree for given leaf weights".
- A heap-based loop that repeatedly combines the two smallest items.
Interactive visualization
Play, step, change the input. ← → and space work too.
No interactive visualization for this topic yet
Related visualizations are linked under Related.
Pseudocode
1heap = min-heap of leaves (freq, symbol)2while len(heap) > 1:3 a = pop(heap); b = pop(heap)4 push(heap, node(freq = a.freq + b.freq, left = a, right = b))5root = pop(heap)6assign codes: dfs(root, prefix): left -> prefix + "0", right -> prefix + "1"Implementations
1import heapq2from typing import Optional3 4# Huffman coding: build an optimal prefix-free code by repeatedly merging the5# two least frequent symbols. The greedy choice is provable — the two rarest6# symbols can always be placed as siblings at the deepest level.7 8 9class HuffNode:10 __slots__ = ("symbol", "freq", "left", "right")11 12 def __init__(self, symbol: Optional[str], freq: int,13 left: Optional["HuffNode"] = None, right: Optional["HuffNode"] = None) -> None:14 self.symbol = symbol # None on internal nodes15 self.freq = freq16 self.left = left17 self.right = right18 19 201 · A min-heap ordered by frequency; ties broken by insertion order21def build_tree(freq: dict[str, int]) -> Optional[HuffNode]:22 # the tie-breaker is essential: HuffNode is not comparable, so a frequency23 # tie would make heapq try to compare two nodes and raise TypeError24 heap = [(f, i, HuffNode(sym, f)) for i, (sym, f) in enumerate(freq.items())]25 heapq.heapify(heap)26 if not heap:27 return None28 tie = len(heap)29 302 · Merge the two rarest into a parent whose frequency is their sum31 while len(heap) > 1:32 f1, _, a = heapq.heappop(heap)33 f2, _, b = heapq.heappop(heap)34 heapq.heappush(heap, (f1 + f2, tie, HuffNode(None, f1 + f2, a, b)))35 tie += 136 return heap[0][2]37 38 393 · Walk the tree: left appends 0, right appends 140def collect_codes(node: Optional[HuffNode], prefix: str = "",41 out: Optional[dict[str, str]] = None) -> dict[str, str]:42 if out is None:43 out = {}44 if node is None:45 return out46 if node.left is None and node.right is None:47 # A single-symbol alphabet still needs one bit, hence the empty-prefix case48 out[node.symbol] = prefix or "0"49 return out50 collect_codes(node.left, prefix + "0", out)51 collect_codes(node.right, prefix + "1", out)52 return out53 54 554 · The codes are prefix-free, so decoding needs no separators56def encode(text: str, codes: dict[str, str]) -> str:57 return "".join(codes[c] for c in text)58 59 605 · Total encoded length = sum over symbols of freq * codeLength61def encoded_bits(freq: dict[str, int], codes: dict[str, str]) -> int:62 return sum(f * len(codes[sym]) for sym, f in freq.items())- The tie-breaker in
(f, i, node)is *load-bearing*:HuffNodedefines no__lt__, so on a frequency tieheapqwould try to compare two nodes and raiseTypeError. This is the single most common Huffman bug in Python. heapq.heapify(heap)builds the initial heap in O(k) rather than k pushes at O(k log k).out: Optional[dict] = Nonewithif out is None: out = {}is the correct mutable-default idiom — a literal{}default would be shared across every call to the function.prefix or "0"uses the falsiness of the empty string to supply the single-symbol code, which is idiomatic but worth the comment."".join(...)over a generator is the efficient string build; repeated+=would be quadratic for a long text.
heapqcompares tuples element by element and falls through to later elements on a tie, which is exactly why a non-comparable payload must be preceded by a unique tie-breaker.- A mutable default argument (
out: dict = {}) is evaluated once at definition time and shared by every call — the classic Python gotcha, avoided here with theNonesentinel. collections.Counter(text)builds the frequency table in one call.__slots__onHuffNodeavoids a per-instance__dict__, which matters for a large alphabet.
- Pushing
(freq, node)without a tie-breaker and hittingTypeError: '<' not supported between instances of 'HuffNode'— but only when two frequencies happen to tie, so it passes small tests. - Using
out: dict = {}as a default and accumulating codes across calls. - Building the encoded string with
+=in a loop instead of"".join.
- The non-comparable-payload problem is sharpest in Python, where a frequency tie makes
heapqcompare twoHuffNodes and raiseTypeError— the tie-breaker is mandatory, not stylistic. C++ and JS/TS need an explicit comparator anyway, so the issue surfaces at design time instead. - Ownership: C++ must decide between
unique_ptrjuggling,shared_ptr, or an arena, while JS/TS and Python simply let the collector handle a tree that is built once and discarded. - Heap construction from a full list is O(k) in Python (
heapify) and C++ (make_heap), and O(k log k) in JS/TS where each element must be pushed individually. - Mutable default arguments are a Python-only hazard: the
out=Nonesentinel here has no counterpart in the other three, where default parameters are re-evaluated per call.
Complexity
n symbols, n − 1 merges of O(log n) each. O(n) if frequencies arrive sorted (two-queue method). Tree has 2n − 1 nodes.
Compare growth rates in the Complexity Explorer →When to use — and when not to
- Symbol-by-symbol lossless compression with a known frequency table.
- Any "merge two smallest, cost is their sum" problem: minimum cost to connect ropes, merge sorted files, combine stones with sum cost.
- Constructing an optimal-depth tree for weighted leaves (alphabetic order not required).
- Symbols must keep their order (alphabetic / optimal BST): Huffman reorders leaves freely. Weights
[1, 10, 1]in fixed order: Huffman would merge the two 1s first, but they are not adjacent, so the tree is invalid — use theO(n²)Garsia–Wachs / Interval (Range) DP optimal BST DP. - Merge cost is not the sum (e.g.
max, or cost depends on position): the exchange argument relies oncost = Σ weight × depth. Stone merging where only adjacent piles merge is Interval (Range) DP (Minimum Cost to Merge Stones), not Huffman. - Streaming data with unknown or drifting frequencies — use adaptive Huffman or arithmetic coding; arithmetic coding also beats Huffman when a symbol has probability well above 0.5 (Huffman cannot use fewer than 1 bit per symbol).
Alternatives
Common mistakes
- Forgetting the single-symbol edge case (the root is a leaf; it needs a 1-bit code, not an empty one).
- Pushing a tuple
(freq, node)into Python's heap without a tiebreaker — comparing nodes raisesTypeErroron equal frequencies. - Assuming Huffman output is unique; ties can be broken differently, yielding different but equally optimal codes.
- Building the tree top-down by splitting frequencies in half (Shannon–Fano) — that is not optimal.
Interview patterns
- Minimum Cost to Connect Sticks / Ropes: pure Huffman with a heap, return the sum of merge costs.
- Explain the two-part proof (siblings lemma + reduction) — the interviewer wants the exchange argument, not just the algorithm.
- Decoding with a trie of codewords; encoding via a lookup table.
- Where does O(n log n) come from?Beginner
- Top K from a streamIntermediate
- When a hash map is the wrong choiceIntermediate
- Greedy or dynamic programming?Advanced
- Kth Largest Element in an ArrayIntermediate
- Network Delay TimeAdvanced
- Merge IntervalsIntermediate