Greedy Algorithms/Huffman Codes

Lesson 7.33,472 words

Huffman Codes

Huffman coding builds a provably optimal prefix-free binary code by repeatedly merging the two least frequent symbols. We develop prefix-free codes as binary trees, give the algorithm with a priority queue, build a Huffman tree from example frequencies, prove optimality with the same greedy-choice-plus-substructure argument, and pin the running time at O(nlogn)O(n\log n).

╌╌╌╌

Suppose we want to store or transmit a file of text drawn from some alphabet of symbols, letters being the obvious case. A fixed-length code assigns every symbol a bit string of the same length: with symbols we would spend bits each (), regardless of how often each symbol appears. But real text is lopsided. In English, e and t are everywhere while q and z are rare. It is wasteful to spend as many bits on z as on e.

A variable-length code exploits this skew: give the frequent symbols short codewords and the rare symbols long ones, so the total bit count drops. Huffman's 1952 algorithm finds the variable-length code that compresses a given file as much as any such code possibly can, and it does so with a simple greedy rule.1 This lesson is the payoff of the greedy method: a non-obvious algorithm, a short correctness proof, and a result used billions of times a day inside JPEG, MP3, gzip, and PNG.

Prefix-free codes

Variable-length codes carry a hazard. If e is 0 and t is 01, then the stream 001 is ambiguous (is it e e t? e ??) because the codeword for e is a prefix of the codeword for t. To decode unambiguously without separator symbols, we insist on a prefix-free code (also called a prefix code): no codeword is a prefix of any other.2

Prefix-freeness gives instant, unambiguous decoding: read bits left to right, and the moment the bits so far match a codeword, that codeword is the only possible symbol, so emit it and start fresh. Restricting to prefix-free codes costs nothing in compression: for any uniquely decodable code there is a prefix-free code at least as good, so we lose no optimality by considering only these.

Every prefix-free code corresponds to a binary tree. Symbols sit at the leaves; the path from the root to a leaf spells its codeword, taking 0 for a left edge and 1 for a right edge. Because symbols are only at leaves, no codeword can be a prefix of another: a prefix would mean one symbol's leaf lies on the path to another's, impossible when both are leaves. The depth of a leaf is its codeword's length.

A prefix-free code as a binary tree. Symbols sit only at leaves; the root-to-leaf path spells the codeword ( left, right). No codeword is a prefix of another because no leaf lies on the path to another.

The compression problem

Let the alphabet be , and let symbol occur with frequency (its count, or its probability) in the file. In a code tree , let be the depth of 's leaf, the number of bits in its codeword. The total number of bits to encode the whole file is the cost of the tree:

Huffman's algorithm

Huffman's greedy insight is to build the tree bottom-up, starting from the question: which two symbols belong deepest in the tree? The two least frequent ones, since multiplying their long codewords by small frequencies costs little.3 So make the two rarest symbols siblings at the bottom, merge them into a single super-symbol whose frequency is their sum, and repeat on the smaller alphabet. Each merge fuses two nodes into one, so after merges a single tree remains.

A min-priority queue keyed on frequency makes the two least frequent cheap to extract.

Algorithm 1:Huffman(C)\textsc{Huffman}(C) — build an optimal prefix-free code tree
  1. 1
    nCn \gets \abs{C}
  2. 2
    QQ \gets a min-priority queue holding all symbols of CC, keyed on freq\mathit{freq}
  3. 3
    for i1i \gets 1 to n1n - 1 do
  4. 4
    allocate a new internal node zz
  5. 5
    z.leftxz.\mathit{left} \gets x \gets Extract-Min(Q)\textsc{Extract-Min}(Q)
    rarest remaining
  6. 6
    z.rightyz.\mathit{right} \gets y \gets Extract-Min(Q)\textsc{Extract-Min}(Q)
    next rarest
  7. 7
    z.freqx.freq+y.freqz.\mathit{freq} \gets x.\mathit{freq} + y.\mathit{freq}
  8. 8
    call Insert(Q,z)\textsc{Insert}(Q, z)
    re-insert merged super-symbol
  9. 9
    return Extract-Min(Q)\textsc{Extract-Min}(Q)
    last node is the root

Each iteration removes two nodes and inserts one, shrinking the queue by one; the single survivor after rounds is the root of the finished tree. Reading the tree from the root gives every symbol's codeword.

huffman.pypython
import heapq
from collections.abc import Hashable, Iterable, Mapping
from typing import Generic, Optional, TypeVar


Symbol = TypeVar("Symbol", bound=Hashable)


class HuffmanNode(Generic[Symbol]):
  """
    One node of a Huffman code tree.\n
    A leaf carries a `symbol` and its `frequency`; an internal node carries no\n
    symbol, a frequency equal to the sum of its children, and `left`/`right`\n
    links. The tie-breaking `order` gives every node a stable, distinct heap\n
    key so equal frequencies never force a comparison between nodes.\n
  """

  def __init__(
    self,
    frequency: float,
    order: int,
    symbol: Optional[Symbol] = None,
    left: Optional[HuffmanNode[Symbol]] = None,
    right: Optional[HuffmanNode[Symbol]] = None,
  ) -> None:
    self.frequency: float = frequency
    self.order: int = order
    self.symbol: Optional[Symbol] = symbol
    self.left: Optional[HuffmanNode[Symbol]] = left
    self.right: Optional[HuffmanNode[Symbol]] = right

  @property
  def is_leaf(self) -> bool:
    """
      Whether this node holds a symbol (no children).\n
    """
    return self.left is None and self.right is None

  def __lt__(self, other: HuffmanNode[Symbol]) -> bool:
    """
      Order nodes by frequency, breaking ties by insertion order so the\n
      min-heap is fully deterministic and never compares symbols.\n
    """
    if self.frequency != other.frequency:
      return self.frequency < other.frequency
    return self.order < other.order

  def __repr__(self) -> str:
    if self.is_leaf:
      return f"HuffmanNode(symbol={self.symbol!r}, freq={self.frequency})"
    return f"HuffmanNode(internal, freq={self.frequency})"


def build_huffman_tree(
  frequencies: Mapping[Symbol, float],
) -> Optional[HuffmanNode[Symbol]]:
  """
    Build an optimal prefix-free code tree from symbol frequencies.\n
    Seeds a min-priority queue with one leaf per symbol, then performs n - 1\n
    merges: each pops the two least-frequent nodes, joins them under a new\n
    internal node whose frequency is their sum, and pushes the merge back. The\n
    surviving node is the root. Returns None for an empty alphabet.\n
    Runs in O(n log n).\n
  """
  if not frequencies:
    return None

  # seed the heap with one leaf per symbol.
  counter: int = 0
  heap: list[HuffmanNode[Symbol]] = []
  for symbol, frequency in frequencies.items():
    heapq.heappush(heap, HuffmanNode(frequency, counter, symbol=symbol))
    counter += 1

  # n - 1 merges fuse the leaves into a single tree.
  while len(heap) > 1:
    # pop the two least-frequent nodes.
    rarest: HuffmanNode[Symbol] = heapq.heappop(heap)
    next_rarest: HuffmanNode[Symbol] = heapq.heappop(heap)

    # join them under a new internal node and push it back.
    merged: HuffmanNode[Symbol] = HuffmanNode(
      rarest.frequency + next_rarest.frequency,
      counter,
      left=rarest,
      right=next_rarest,
    )
    counter += 1
    heapq.heappush(heap, merged)

  return heap[0]

Building a Huffman tree by hand

Take a six-symbol alphabet with these frequencies (in thousands of occurrences), the classic CLRS example:

Symbolabcdef
Frequency4513121695

We repeatedly merge the two smallest frequencies:

  1. Merge f and e → node .
  2. Merge c and b → node .
  3. Merge and d → node .
  4. Merge and → node .
  5. Merge a and → root .

Each step extracts the two smallest weights and re-inserts their sum, so the queue loses one element per round until a single root remains.

The five priority-queue merges that build the Huffman tree. Each step extracts the two least-frequent nodes and inserts their sum; the queue shrinks by one until a single root of weight remains.

The resulting tree, with left edges labeled 0 and right edges 1, is:

Huffman code tree for the six-symbol example with edges labeled and .

Reading root-to-leaf gives the codewords:

Symbolabcdef
Codeword010110011111011100

The frequent a gets a single bit; the rare e and f get four. The cost is

thousand bits. A fixed-length -bit code would spend thousand bits, so Huffman saves about , and no prefix-free code does better.

Encoding concatenates codewords with nothing between them. The word face becomes 1100 0 100 1101, the eleven-bit stream 11000100 1101. Decoding runs the bits back through the tree: start at the root, turn left on 0 and right on 1, and the instant a leaf is reached emit its symbol and jump back to the root. Prefix-freeness is what makes this unambiguous — a leaf is reached exactly when a whole codeword has been consumed, never mid-codeword.

Decoding the stream 110001001101 with the example code. Each root-to-leaf descent consumes one codeword; the bits partition uniquely into f, a, c, e with no separators.
huffman.pypython
from collections import Counter
from collections.abc import Hashable, Iterable, Mapping
from typing import Generic, Optional, TypeVar


Symbol = TypeVar("Symbol", bound=Hashable)


class HuffmanNode(Generic[Symbol]):
  """
    One node of a Huffman code tree.\n
    A leaf carries a `symbol` and its `frequency`; an internal node carries no\n
    symbol, a frequency equal to the sum of its children, and `left`/`right`\n
    links. The tie-breaking `order` gives every node a stable, distinct heap\n
    key so equal frequencies never force a comparison between nodes.\n
  """

  def __init__(
    self,
    frequency: float,
    order: int,
    symbol: Optional[Symbol] = None,
    left: Optional[HuffmanNode[Symbol]] = None,
    right: Optional[HuffmanNode[Symbol]] = None,
  ) -> None:
    self.frequency: float = frequency
    self.order: int = order
    self.symbol: Optional[Symbol] = symbol
    self.left: Optional[HuffmanNode[Symbol]] = left
    self.right: Optional[HuffmanNode[Symbol]] = right

  @property
  def is_leaf(self) -> bool:
    """
      Whether this node holds a symbol (no children).\n
    """
    return self.left is None and self.right is None

  def __lt__(self, other: HuffmanNode[Symbol]) -> bool:
    """
      Order nodes by frequency, breaking ties by insertion order so the\n
      min-heap is fully deterministic and never compares symbols.\n
    """
    if self.frequency != other.frequency:
      return self.frequency < other.frequency
    return self.order < other.order

  def __repr__(self) -> str:
    if self.is_leaf:
      return f"HuffmanNode(symbol={self.symbol!r}, freq={self.frequency})"
    return f"HuffmanNode(internal, freq={self.frequency})"


def build_codebook(
  root: Optional[HuffmanNode[Symbol]],
) -> dict[Symbol, str]:
  """
    Map each symbol to its codeword by walking the tree, taking '0' on a left\n
    edge and '1' on a right edge. A single-symbol alphabet has no edges, so its\n
    lone symbol gets the one-bit codeword '0' (a valid, decodable code).\n
  """
  codebook: dict[Symbol, str] = {}
  if root is None:
    return codebook

  if root.is_leaf:
    # degenerate tree: assign one bit so the code is still usable.
    assert root.symbol is not None
    codebook[root.symbol] = "0"
    return codebook

  def walk(node: HuffmanNode[Symbol], codeword: str) -> None:
    # a leaf ends the path: record its codeword.
    if node.is_leaf:
      assert node.symbol is not None
      codebook[node.symbol] = codeword
      return

    # descend, appending 0 for a left edge and 1 for a right edge.
    if node.left is not None:
      walk(node.left, codeword + "0")
    if node.right is not None:
      walk(node.right, codeword + "1")

  walk(root, "")
  return codebook


class HuffmanCode(Generic[Symbol]):
  """
    An optimal prefix-free code over a fixed alphabet.\n
    Built from explicit frequencies or learned from a sample sequence, it\n
    exposes the code tree, the per-symbol codebook, encode/decode, and the\n
    total cost B(T) of the tree.\n
  """

  def __init__(self, frequencies: Mapping[Symbol, float]) -> None:
    self.frequencies: dict[Symbol, float] = dict(frequencies)
    self.root: Optional[HuffmanNode[Symbol]] = build_huffman_tree(frequencies)
    self.codebook: dict[Symbol, str] = build_codebook(self.root)

  @classmethod
  def from_data(cls, data: Iterable[Symbol]) -> HuffmanCode[Symbol]:
    """
      Build a code whose frequencies are the symbol counts in `data`.\n
    """
    return cls(Counter(data))

  def encode(self, data: Iterable[Symbol]) -> str:
    """
      Concatenate the codewords of `data` into one bit string.\n
      Every symbol must belong to the code's alphabet.\n
    """
    # look up each symbol's codeword, rejecting any outside the alphabet.
    pieces: list[str] = []
    for symbol in data:
      if symbol not in self.codebook:
        raise KeyError(f"symbol {symbol!r} is not in the code's alphabet")
      pieces.append(self.codebook[symbol])

    return "".join(pieces)

  def decode(self, bits: str) -> list[Symbol]:
    """
      Recover the original symbols from a bit string produced by `encode`.\n
      Reads bits left to right, descending the tree until a leaf is reached,\n
      then emits that symbol and restarts at the root.\n
    """
    symbols: list[Symbol] = []
    if self.root is None:
      if bits:
        raise ValueError("cannot decode bits with an empty code")
      return symbols

    # a one-symbol code has no internal nodes: each bit is one symbol.
    if self.root.is_leaf:
      assert self.root.symbol is not None
      return [self.root.symbol for _ in bits]

    # descend one edge per bit; at each leaf emit a symbol and restart.
    node: Optional[HuffmanNode[Symbol]] = self.root
    for bit in bits:
      node = node.left if bit == "0" else node.right
      if node is None:
        raise ValueError("invalid bit in encoded stream")

      if node.is_leaf:
        assert node.symbol is not None
        symbols.append(node.symbol)
        node = self.root

    # a clean decode lands back at the root with no leftover bits.
    if node is not self.root:
      raise ValueError("encoded stream ended mid-codeword")
    return symbols

  def cost(self) -> float:
    """
      The total cost B(T) = sum of freq * depth over all leaves: the number of\n
      bits to encode a file with these frequencies under this code.\n
    """
    return sum(
      self.frequencies[symbol] * len(codeword)
      for symbol, codeword in self.codebook.items()
    )

Why Huffman is optimal

Huffman is a greedy algorithm, so its proof follows the template from the previous lesson exactly: a greedy-choice property proved by an exchange argument, then optimal substructure to close the induction.4

The greedy choice is safe

The intuition is the exchange argument in one sentence: the rarest symbols belong deepest, so pushing them down and pulling frequent symbols up can never cost more.

The two swaps are easiest to see side by side. In any optimal tree the deepest sibling pair holds some leaves ; the globally rarest symbols may sit higher up. Exchanging with and with sends the rare symbols to the bottom and lifts the more frequent ones — and the cost only drops, because each moved-down leaf is rarer and each moved-up leaf is more frequent.

The greedy-choice exchange for Huffman. Left, an optimal tree with deepest siblings and the rarest symbols sitting higher. Right, after swapping and the rarest symbols are deepest; cost cannot rise since rarer leaves moved down and more frequent leaves moved up.

Optimal substructure

Together the two lemmas give the theorem by induction on .

Running time

The cost is dominated by the priority-queue operations. Building the initial min-heap from symbols takes . The loop runs times, and each iteration does two s and one Insert, each on a binary heap. Hence

If the frequencies arrive already sorted, two simple FIFO queues replace the heap (one of original leaves, one of merged nodes, both kept in nondecreasing frequency), and each extract two smallest is , giving an algorithm. The bound, like activity selection's, is really the cost of getting the symbols into sorted order.

huffman.pypython
from collections.abc import Hashable, Iterable, Mapping
from typing import Generic, Optional, TypeVar


Symbol = TypeVar("Symbol", bound=Hashable)


class HuffmanNode(Generic[Symbol]):
  """
    One node of a Huffman code tree.\n
    A leaf carries a `symbol` and its `frequency`; an internal node carries no\n
    symbol, a frequency equal to the sum of its children, and `left`/`right`\n
    links. The tie-breaking `order` gives every node a stable, distinct heap\n
    key so equal frequencies never force a comparison between nodes.\n
  """

  def __init__(
    self,
    frequency: float,
    order: int,
    symbol: Optional[Symbol] = None,
    left: Optional[HuffmanNode[Symbol]] = None,
    right: Optional[HuffmanNode[Symbol]] = None,
  ) -> None:
    self.frequency: float = frequency
    self.order: int = order
    self.symbol: Optional[Symbol] = symbol
    self.left: Optional[HuffmanNode[Symbol]] = left
    self.right: Optional[HuffmanNode[Symbol]] = right

  @property
  def is_leaf(self) -> bool:
    """
      Whether this node holds a symbol (no children).\n
    """
    return self.left is None and self.right is None

  def __lt__(self, other: HuffmanNode[Symbol]) -> bool:
    """
      Order nodes by frequency, breaking ties by insertion order so the\n
      min-heap is fully deterministic and never compares symbols.\n
    """
    if self.frequency != other.frequency:
      return self.frequency < other.frequency
    return self.order < other.order

  def __repr__(self) -> str:
    if self.is_leaf:
      return f"HuffmanNode(symbol={self.symbol!r}, freq={self.frequency})"
    return f"HuffmanNode(internal, freq={self.frequency})"


def two_queue_huffman_tree(
  sorted_symbols: list[tuple[Symbol, float]],
) -> Optional[HuffmanNode[Symbol]]:
  """
    Build the Huffman tree in O(n) when frequencies arrive pre-sorted.\n
    With the leaves already in nondecreasing frequency, two FIFO queues — one\n
    of original leaves, one of merged internal nodes — both stay sorted, so the\n
    two least-frequent nodes are always at the two queue fronts and each\n
    "extract two smallest" is O(1). `sorted_symbols` must be in nondecreasing\n
    frequency order.\n
  """
  if not sorted_symbols:
    return None

  from collections import deque

  # one FIFO of original leaves (already sorted), one of merged internals.
  counter: int = 0
  leaves: deque[HuffmanNode[Symbol]] = deque()
  for symbol, frequency in sorted_symbols:
    leaves.append(HuffmanNode(frequency, counter, symbol=symbol))
    counter += 1
  merges: deque[HuffmanNode[Symbol]] = deque()

  def take_smallest() -> HuffmanNode[Symbol]:
    """
      Pop the smaller of the two queue fronts, preferring leaves on a tie so\n
      the construction stays deterministic.\n
    """
    # if one queue is empty, the other front is the smallest.
    if not merges:
      return leaves.popleft()
    if not leaves:
      return merges.popleft()

    # otherwise compare fronts, preferring leaves on a tie.
    if leaves[0].frequency <= merges[0].frequency:
      return leaves.popleft()
    return merges.popleft()

  while len(leaves) + len(merges) > 1:
    # take the two least-frequent nodes off the queue fronts.
    first: HuffmanNode[Symbol] = take_smallest()
    second: HuffmanNode[Symbol] = take_smallest()

    # merge them and enqueue the new internal node.
    merged: HuffmanNode[Symbol] = HuffmanNode(
      first.frequency + second.frequency,
      counter,
      left=first,
      right=second,
    )
    counter += 1
    merges.append(merged)

  return merges[0] if merges else leaves[0]

Entropy, arithmetic coding, and the whole-bit floor

Huffman coding is optimal among codes that assign each symbol a whole number of bits independently. This section places it against the theoretical floor and against the codes that go beyond it.

The entropy bound. Shannon's source coding theorem (1948) sets the floor: no uniquely decodable code can average fewer than the entropy bits per symbol, and a Huffman code always lands within one bit of it, .5 The example above illustrates the gap: its entropy is about bits per symbol while Huffman spends — essentially on the floor, because the frequencies are close to powers of . The one-bit slack becomes visible only on skewed sources.

The whole-bit floor. When a symbol's ideal codeword length is fractional — for a symbol of probability , ideally bits — Huffman must round up to a full bit, wasting the difference. Arithmetic coding (Rissanen & Langdon, 1979) sidesteps the integer-bit floor by encoding the entire message as a single fraction in , so a symbol can cost a fractional number of bits and the total approaches arbitrarily closely.6 Modern asymmetric numeral systems (Duda, 2009) match arithmetic coding's ratio at Huffman-like speed and are now used in Zstandard, LZFSE, and JPEG XL.7

Where Huffman sits. The entropy is the hard floor (Shannon); Huffman lands within one bit of it, tight on near-dyadic sources and slack on skewed ones; arithmetic coding / ANS close the remaining gap toward .

Where it is still used. Huffman's simplicity, speed, and provable optimality within its class keep it embedded in DEFLATE (gzip, PNG, ZIP), JPEG, and MP3 to this day, usually as the final entropy-coding stage after a modeling transform. Two engineering variants matter in practice: canonical Huffman codes store the codebook as just the per-symbol lengths (the codewords are then reconstructed by a fixed rule), shrinking the header DEFLATE must transmit; and length-limited Huffman (the Package-Merge algorithm of Larmore & Hirschberg, 1990) caps the maximum codeword length so decode tables stay small, at a tiny cost in ratio.8 Huffman remains the textbook proof that a greedy algorithm, properly justified, can be exactly optimal, not merely a good heuristic.

Takeaways

  • A prefix-free code lets a stream decode unambiguously — it is a binary tree with symbols at the leaves, codeword length = leaf depth.
  • The goal is to minimize ; optimal trees are full.
  • Huffman's algorithm greedily merges the two least-frequent nodes via a min-priority queue, times, building the tree bottom-up.
  • Optimality follows the greedy template: an exchange argument shows the rarest symbols belong deepest (greedy choice), and optimal substructure closes the induction.
  • Running time is , the cost of the heap operations, dropping to when frequencies are pre-sorted.

Footnotes

  1. Skiena, §5 — Data Compression: Huffman's greedy construction of the optimal variable-length code for a given file.
  2. CLRS, Ch. 16 — Greedy Algorithms (§16.3): prefix-free codes and their representation as binary trees with symbols at the leaves.
  3. CLRS, Ch. 16 — Greedy Algorithms (§16.3): the greedy rule of repeatedly merging the two least-frequent symbols, implemented with a min-priority queue.
  4. Erickson, Ch. 4 — Greedy Algorithms (Huffman Codes): the optimality proof via greedy-choice exchange plus optimal substructure.
  5. Shannon, C. E. (1948), A mathematical theory of communication, Bell System Technical Journal 27, 379–423 & 623–656 — entropy as the lower bound on average code length; a Huffman code satisfies .
  6. Rissanen, J. & Langdon, G. G. (1979), Arithmetic coding, IBM Journal of Research and Development 23(2), 149–162 — encoding a whole message as one interval, escaping Huffman's whole-bit-per-symbol floor.
  7. Duda, J. (2009), Asymmetric numeral systems, arXiv:0902.0271 — near-entropy compression at table-lookup speed, now used in Zstandard, LZFSE, and JPEG XL.
  8. Larmore, L. L. & Hirschberg, D. S. (1990), A fast algorithm for optimal length-limited Huffman codes, Journal of the ACM 37(3), 464–473 — the Package-Merge algorithm building optimal Huffman codes under a maximum-length constraint.
Practice

╌╌ END ╌╌