Evanalysis
6.2Estimated reading time: 17 min

6.2 Huffman coding and heap applications

Construct and decode binary prefix codes with a min heap, audit their weighted storage cost, and apply the same priority-queue pattern to k-way merging.

Course contents

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

symbolabcdef
frequency4513121695

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

  1. f:5 + e:9 -> 14;
  2. c:12 + b:13 -> 25;
  3. (f,e):14 + d:16 -> 30;
  4. (c,b):25 + ((f,e),d):30 -> 55;
  5. 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

symbolcodelengthfrequency
a0145
b101313
c100312
d111316
e110149
f110045

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,000 bits excludes tree/header overhead.
  • Saying six symbols need two fixed bits because 2^2 is close to six. Two bits represent only four distinct values; six symbols require three bits.
  • Putting every element from all k lists in the merge heap. Only the current head of each nonempty list is eligible, so the heap should have at most k items.

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

  1. Starting from weights 5, 9, 12, 13, 16, 45, show every Huffman merge and the heap's remaining weights after each insertion.
  2. Using the code table in the complete construction, decode 11011100111. Show the leaf boundaries rather than guessing equal-length groups.
  3. 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.
  4. Decide whether the set {0, 01, 11} is prefix-free. Explain the practical decoding consequence.
  5. Give heap pseudocode for merging k sorted linked lists. State the heap invariant, time complexity in terms of total length n, 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.

Practice

Work out your answer, then check it. You can revise and try again.

Loading…

Key terms in this unit