Motivation
A fixed-length code spends the same number of bits on every symbol. That is
simple, but it ignores a useful fact about real data: symbols usually occur at
very different frequencies. If a occupies almost half a file while f
appears only occasionally, making their codewords equally long wastes space.
Huffman coding uses a binary tree and a min-priority queue to assign short paths
to frequent symbols and longer paths to rare ones.
There are three separate questions to keep straight. First, construction asks how repeated minimum-frequency merges produce a tree. Second, decodability asks why a stream of variable-length codewords can be split without separators. Third, cost asks how many bits the resulting code uses under a stated frequency model. A correct solution must answer all three; a plausible-looking tree alone is not enough.
The same heap pattern appears in a different setting. When merging k sorted
lists, the next output is the smallest among the lists' current heads. A
min-heap keeps precisely those candidates and avoids rescanning all k heads
after every output. Both applications repeatedly maintain a small frontier of
currently eligible minimum items.
Definitions
Definition
Frequency model
Let the alphabet contain m distinct symbols. A symbol x has frequency
w(x), which may be a positive count or a probability. Counts determine how
many copies occur; probabilities should sum to 1. Multiplying every weight by
the same positive constant does not change which pairs are smallest, so the
Huffman merge structure is unchanged.
Definition
Binary prefix code
A binary code assigns each symbol a finite bit string. It is prefix-free if
no symbol's complete codeword is a prefix of another symbol's codeword. In a
prefix-code tree, symbols label leaves, left edges are conventionally 0, and
right edges are conventionally 1; a codeword is the root-to-leaf path.
Definition
Weighted code length
For codeword length ell(x), the weighted length is
L = sum_x w(x) ell(x). With probabilities, L is the expected bits per
symbol. With occurrence counts, it is the exact number of encoded payload bits.
This calculation does not include storage for the code tree, a frequency
header, file metadata, or byte padding; those costs must be added in a complete
file-format analysis.
The repeated two-minimum construction
Create one leaf for each symbol and key it by frequency. Put all leaves in a
min-heap. Repeatedly remove the two minimum-weight roots, attach them below a
new internal node whose weight is their sum, and insert that node back into the
heap. With m leaves there are exactly m - 1 merges, because every merge
reduces the number of heap items by one.
HUFFMAN(symbols with positive weights)
H = a min-heap containing one leaf per symbol
while size(H) > 1
x = deleteMin(H)
y = deleteMin(H)
z = a new internal node with weight x.weight + y.weight
z.left = x
z.right = y
insert(H, z)
return deleteMin(H)
Choosing x as the left child gives a reproducible convention, but exchanging
the left and right children of any internal node remains valid. Such an
exchange changes bit strings, not codeword lengths or storage cost. Equal
weights can also permit different merge orders. Therefore a Huffman tree need
not have a unique printed code table.
At every stage the heap contains roots of disjoint subtrees whose leaves partition the alphabet. A merge preserves that forest invariant: it replaces two disjoint parts by their union and gives the new root the sum of all leaf weights below it. When one root remains, every symbol occurs exactly once as a leaf of one full binary tree.
If the input contains N symbols, counting frequencies costs O(N). A
bottom-up heap can be built from the m leaves in O(m) time. The m - 1
rounds each perform two deleteMin operations and one insertion, each bounded
by O(log m), so tree construction is O(m log m). Traversing the final tree
to record all codes is O(m). Thus the complete preprocessing cost is
O(N + m log m); if a frequency table is already supplied, the O(N) scan is
absent. Encoding performs one code-table lookup per input symbol plus work
proportional to the number of bits emitted; decoding is linear in the number of
bits read.
For an alphabet with just one symbol, the merge loop produces a one-node tree
and an empty path. Practical formats normally assign that symbol the code 0
so repeated occurrences still have an explicit representation. The six-symbol
course example does not need this edge-case convention.
Theorem/Proposition
Theorem
Leaf codes are prefix-free
If every source symbol labels a leaf of a binary tree and its code is the root-to-leaf edge sequence, then no source-symbol codeword is a prefix of another source-symbol codeword.
Theorem
A prefix-code stream has unique symbol boundaries
For a known prefix-code tree, every bit string that is a concatenation of its leaf codewords has exactly one left-to-right decoding into source symbols.
Theorem
Merge-sum identity
For a tree made by repeatedly joining two weighted roots, the weighted external
path length equals the sum of the weights of all newly created internal nodes.
Equivalently, summing w(x) ell(x) over leaves gives the same number as summing
the combined weight written at every merge.
These propositions establish the properties used below. The construction is the Huffman greedy algorithm, but a full exchange proof that it minimizes weighted length among all binary prefix trees is beyond this unit's source scope. We do not assume such a proof in the numerical or decoding arguments.
Proof Sketch or Proof Idea
For the first proposition, suppose one leaf code were a prefix of another. Following the shorter code from the root would reach its leaf and then would have to continue along more edges to reach the second symbol. A leaf has no children, so this is impossible. The claim depends on symbols being stored at leaves rather than at internal nodes.
For unique decoding, start at the root and consume bits one at a time. The first reached leaf forces the first symbol: stopping earlier would mean an internal node had a code, while continuing would mean a leaf code was a prefix of a longer code. Output that leaf, return to the root, and repeat. Induction on the remaining codewords gives a unique complete decoding. If the stream ends at an internal node, it is truncated or invalid rather than a second valid decoding.
For the merge-sum identity, joining roots of weights p and q increases the
depth of every leaf in those two subtrees by one. The weighted-length increase
is therefore exactly p + q, the new internal-node weight. Starting from
isolated leaves at depth zero and summing this increase across all merges gives
the final weighted external path length. This is also a powerful arithmetic
cross-check: a code-length calculation and a merge-total calculation should
agree.
For heap-based multiway merging, maintain the invariant that the heap contains exactly the first unconsumed item of every nonempty list. Because each list is sorted, every unconsumed item in that list is at least its head. The smallest heap key is therefore the smallest item remaining anywhere. Removing it is safe; inserting the successor from the same list restores the invariant.
Worked Examples
Worked example
Complete six-symbol Huffman construction
The tutorial gives the percentage frequencies
| symbol | a | b | c | d | e | f |
|---|---|---|---|---|---|---|
| frequency | 45 | 13 | 12 | 16 | 9 | 5 |
The first audit is 45 + 13 + 12 + 16 + 9 + 5 = 100. Start from the ascending
heap view 5, 9, 12, 13, 16, 45. The five merges are
f:5 + e:9 -> 14;c:12 + b:13 -> 25;(f,e):14 + d:16 -> 30;(c,b):25 + ((f,e),d):30 -> 55;a:45 + 55 -> 100.
After every insertion, the next two minima are selected from the whole heap,
not merely from the original leaves. With the lighter child placed left and
left/right edges labeled 0/1, the full tree is
(100)
0 / \ 1
a:45 (55)
0 / \ 1
(25) (30)
0/ \1 0/ \1
c:12 b:13 (14) d:16
0/ \1
f:5 e:9
Reading root-to-leaf paths gives
| symbol | code | length | frequency |
|---|---|---|---|
a | 0 | 1 | 45 |
b | 101 | 3 | 13 |
c | 100 | 3 | 12 |
d | 111 | 3 | 16 |
e | 1101 | 4 | 9 |
f | 1100 | 4 | 5 |
The length pattern also satisfies the complete-tree check
2^-1 + 3(2^-3) + 2(2^-4) = 1. Swapping children would produce different
bit strings, but the lengths 1, 3, 3, 3, 4, 4 would remain unchanged.
Worked example
Three independent checks of the 224,000-bit result
For a 100,000-character file, the counts are 45,000, 13,000, 12,000,
16,000, 9,000, and 5,000. The weighted average length is
0.45(1) + 0.13(3) + 0.12(3) + 0.16(3) + 0.09(4) + 0.05(4)
= 0.45 + 0.39 + 0.36 + 0.48 + 0.36 + 0.20 = 2.24 bits per character.
Hence the payload uses 100,000(2.24) = 224,000 bits. Direct integer
accounting gives the same value:
45,000 + 39,000 + 36,000 + 48,000 + 36,000 + 20,000 = 224,000.
The merge-sum check also agrees:
14 + 25 + 30 + 55 + 100 = 224 bit-percent units, hence 224,000 bits for
100,000 characters. Agreement among all three methods catches a wrong
frequency, merge, or code length.
Under the tutorial's explicit 8-bit ASCII comparison, the same payload would
use 100,000(8) = 800,000 bits, so Huffman saves 576,000 bits, or 72% of
that payload cost. A shortest fixed-length binary code for six symbols needs
ceil(log2 6) = 3 bits per character, so it uses 300,000 bits. Huffman saves
76,000 bits, which is 76,000 / 300,000 = 25 1/3% of the fixed-length cost.
Worked example
Unique decoding without separators
Using the table above, decode 1000101. Start at the root. Bits 100 reach
leaf c, so output c and return to the root. The next bit 0 reaches a.
The remaining bits 101 reach b. The only decoding is therefore cab, with
the forced split 100 | 0 | 101. Splitting into equal-width groups would be
incorrect because this is a variable-length code.
Worked example
Heap trace for merging three sorted lists
Let L1 = [1, 7, 10], L2 = [2, 3, 11], and L3 = [4, 5, 6]. Initialize a
min-heap with (1,L1), (2,L2), (4,L3). Remove 1 and insert 7; remove 2
and insert 3; remove 3 and insert 11; remove 4 and insert 5. Continuing
in the same way outputs 1, 2, 3, 4, 5, 6, 7, 10, 11.
MERGE_K_SORTED_LISTS(lists)
H = empty min-heap
for each nonempty list i
insert(H, (lists[i].head.value, i, lists[i].head))
output = empty list
while H is not empty
(value, i, node) = deleteMin(H)
successor = node.next
append value to output
if successor exists
insert(H, (successor.value, i, successor))
return output
For k sorted lists containing n elements in total, the heap contains at
most k heads. Each output performs one deleteMin and at most one insertion,
both O(log k), so for k >= 2 the total time is O(n log k) after heap
initialization. The auxiliary heap space is O(k). A direct scan of all heads
would take O(nk), while concatenating and sorting ignores the supplied order
and costs O(n log n). When k = 1, copying the one list is simply O(n).
Common Mistakes
- Combining the two largest weights, or continuing to use a stale sorted list of original frequencies. Each round must select the two current minima, including internal nodes inserted by earlier rounds.
- Reading codes from leaves upward. Codes are root-to-leaf paths; reversing a path changes the code.
- Placing a source symbol at an internal node. Then its code can prefix a descendant's code, destroying separator-free decoding.
- Claiming that frequent symbols must always have strictly shorter codes. The construction tends to make them shallow, but equal lengths and tie-dependent trees are normal. The exact tree, not frequency rank alone, determines a codeword.
- Comparing only the number of codewords. Storage is the weighted sum of
lengths, and the stated
224,000bits excludes tree/header overhead. - Saying six symbols need two fixed bits because
2^2is close to six. Two bits represent only four distinct values; six symbols require three bits. - Putting every element from all
klists in the merge heap. Only the current head of each nonempty list is eligible, so the heap should have at mostkitems.
Summary
Huffman construction repeatedly joins the two minimum-weight roots in a
min-heap. The resulting symbols are leaves, so root-to-leaf codes are
prefix-free and concatenated codewords decode uniquely. For the normalized
six-symbol table, the merges are 5+9=14, 12+13=25, 14+16=30,
25+30=55, and 45+55=100; the lengths are 1,3,3,3,4,4, and a
100,000-character payload occupies exactly 224,000 bits. That saves
576,000 bits against the stated 8-bit ASCII comparison and 76,000 bits
against the shortest three-bit fixed-length code.
The heap is not specific to compression. For k sorted lists, keeping one
current head per nonempty list lets each of n outputs choose the global
minimum in O(log k) time, for O(n log k) total time and O(k) auxiliary
space.
Exercises
- Starting from weights
5, 9, 12, 13, 16, 45, show every Huffman merge and the heap's remaining weights after each insertion. - Using the code table in the complete construction, decode
11011100111. Show the leaf boundaries rather than guessing equal-length groups. - Recompute the six-symbol payload cost by both weighted code lengths and the merge-sum identity. Then state the savings against 8-bit ASCII and a shortest fixed-length code for six symbols.
- Decide whether the set
{0, 01, 11}is prefix-free. Explain the practical decoding consequence. - Give heap pseudocode for merging
ksorted linked lists. State the heap invariant, time complexity in terms of total lengthn, and auxiliary space complexity.
Solutions
Solution · 1. Merge trace
The states are: merge 5+9=14, leaving 12,13,14,16,45; merge 12+13=25,
leaving 14,16,25,45; merge 14+16=30, leaving 25,30,45; merge
25+30=55, leaving 45,55; merge 45+55=100, leaving only the root. There
are five merges because six leaves require five reductions to one root.
Solution · 2. Decoding
The forced split is 1101 | 1100 | 111. These codewords reach e, f, and
d, respectively, so the decoded sequence is efd.
Solution · 3. Storage audit
The weighted sum is 45(1)+13(3)+12(3)+16(3)+9(4)+5(4)=224 bits per 100
characters. The merge sum is also 14+25+30+55+100=224. Scaling by 1,000
gives 224,000 bits. ASCII uses 800,000, so the saving is 576,000 bits.
Three-bit fixed coding uses 300,000, so the saving is 76,000 bits.
Solution · 4. Prefix test
The set is not prefix-free because 0 is a prefix of 01. On seeing an
initial 0, a decoder cannot know from that bit alone whether to output the
first symbol or wait for the second bit. Leaf codes avoid this ambiguity.
Solution · 5. Multiway merge
Insert each nonempty list's first node into a min-heap keyed by value and tagged
with its source list. Repeatedly remove the minimum, append it to the output,
and insert its successor if one exists. The heap always holds exactly one next
candidate per nonempty list. Its size is at most k, so n removals and at
most n insertions cost O(n log k) for k >= 2; the heap uses O(k) extra
space.