Hello! Welcome back to your daily dose of computer science.
In our last lesson, we explored our first greedy algorithm, solving the Interval Scheduling problem by always picking the activity with the earliest finish time. We rigorously proved its correctness using an exchange argument, establishing a key principle: a greedy algorithm is only as good as its proof.
Today, we continue our journey into the greedy paradigm with one of its most famous applications: Huffman Coding. You'll see how a simple, greedy choice can solve a fundamental problem in data compression, and you'll learn how to implement it efficiently using a data structure you've seen before—the priority queue. This is a classic topic that frequently appears in interviews, not just for the algorithm itself, but for the principles of data compression and optimality it embodies.
1. The Core Problem: Efficient, Lossless Compression
At its heart, Huffman Coding is a technique for lossless data compression. Let's break down what that means and why it's necessary.
Technical Explanation
Computers store everything as bits (0s and 1s). A simple way to represent text is with a fixed-length code, like ASCII, where every character gets the same number of bits (e.g., 8).
Consider the string "AAAAABBC".
- Unique characters: A, B, C.
- With a fixed-length code, we need at least 2 bits to represent 3 unique characters (e.g., A=00, B=01, C=10).
- The encoded string would be
0000000000010110(16 bits total).
But notice 'A' is far more common than 'B' or 'C'. What if we could use shorter codes for frequent characters and longer codes for rare ones? This is the idea behind variable-length coding.
- Maybe A =
0, B =10, C =11. - The encoded string becomes
00000101011(11 bits total) — a significant saving!
However, this introduces a new problem: ambiguity. If your code is 100, is that "BA" or something else? To solve this, Huffman coding generates a prefix code.
A prefix code is a set of codes where no code is a prefix of another.
{0, 10, 11}is a valid prefix code.{0, 1, 01}is not, because0is a prefix of01.
Prefix codes are instantly decodable. When you read a bitstream, as soon as you match a valid codeword, you can be sure that's the character, without having to look ahead.
Huffman Coding | Greedy Algo-3
To solidify these concepts, please read the introductory section of the GeeksforGeeks article on Huffman Coding.
Read the first few paragraphs, down to the heading 'Steps to build Huffman Tree'. Focus on the definition of a prefix code and the counter-example provided to understand why it's crucial for avoiding ambiguity.
Metaphor: The Stenographer's Shorthand
Think of a court stenographer who needs to write down everything said, as fast as possible. They don't write out "the honorable judge" every time. Instead, they develop a shorthand.
- Common words like "the," "a," "is" get very short symbols.
- Rare words like "obstreperous" get longer, more complex symbols.
A prefix code is like ensuring no shorthand symbol is the beginning of another. If . meant "the", you couldn't have .. mean "therefore", because after seeing the first dot, you'd be confused. Huffman coding is the algorithm that automatically generates the most efficient, non-confusing shorthand system for any given text.
2. The Greedy Algorithm: Build a Tree from the Bottom Up
How do we find the optimal prefix code? David Huffman's ingenious insight was to use a greedy, bottom-up approach.
Technical Explanation
The algorithm represents the prefix code as a binary tree, where characters are stored at the leaf nodes. The path from the root to a leaf gives its binary code (e.g., left = 0, right = 1). The core idea is that characters with high frequency should have a short path from the root (i.e., be at a shallow depth), and low-frequency characters should have a long path.
The greedy choice that achieves this is:
At each step, find the two nodes with the lowest frequencies and merge them into a new internal node whose frequency is the sum of its children's frequencies.
You repeat this process until only one node remains—the root of the tree.
Let's watch a visual demonstration of this process.
9.1 Huffman Coding -Greedy Method |Data Structures Tutorials
This video from Jenny's Lectures provides a clear, step-by-step walkthrough of building a Huffman tree. It's an excellent way to see the greedy choice in action.
Please watch from 13:06 to 23:05. The first part (13:06 - 20:34) shows how to build the tree by repeatedly merging the two least frequent nodes. The second part (20:34 - 23:05) shows how to traverse the final tree to assign the binary codes to each character.

3. The Implementation Tool: The Priority Queue
In the algorithm described above, we repeatedly need to "find the two nodes with the lowest frequencies." How do we do this efficiently?
Technical Explanation
If we just stored the nodes in a list and scanned it each time, finding the two minimums would take time at each step, where is the number of nodes remaining. Since we do this times (for initial characters), the total time complexity would be roughly .
We can do much better. A min-priority queue, typically implemented with a min-heap, is the perfect data structure for this job.
- Initialization: Insert all character leaf nodes into the min-priority queue, with their frequency as the priority. This takes time (using a "heapify" operation) or if you insert one by one.
- Iteration: In a loop that runs times:
a. Callextract-mintwice to get the two nodes with the lowest frequencies. This takes time.
b. Create a new internal node.
c. Insert the new node back into the priority queue. This also takes time.
This brings the total time complexity down to a much more efficient .
Huffman Codes: An Information Theory Perspective
The Reducible channel offers a fantastic explanation of the practical implementation details, specifically highlighting the role of the priority queue.
Watch the segment from 25:04 to 27:12. It concisely explains how a priority queue is used to manage the nodes and efficiently build the tree.
Metaphor: The Self-Sorting Inbox
Imagine you have a magic inbox for tasks, where each task has a priority number (its frequency). Whenever you add a task, it automatically slides into the right place. The two tasks with the lowest priority always float to the top.
The Huffman algorithm is like this:
- Throw all your initial character "tasks" into the inbox.
- Repeatedly pull the top two (lowest priority) tasks out.
- Bundle them into a new, combined "project" task, with a priority equal to their sum.
- Drop this new project task back into the inbox.
- Eventually, you'll be left with just one giant project task: your completed Huffman tree.
4. Proving Optimality: Why This Greedy Choice Works
As we learned in the last lesson, a greedy algorithm is useless without a proof of correctness. Why does repeatedly merging the two lowest-frequency nodes guarantee an optimal code?
Technical Explanation
The proof of Huffman's algorithm relies on two fundamental properties of greedy algorithms:
-
Greedy Choice Property: There exists an optimal prefix code tree where the two characters with the lowest frequencies are sibling leaves at the maximum depth of the tree.
- Proof sketch (by exchange argument): Take any optimal tree. If the two least frequent characters,
xandy, are not siblings, find the two charactersaandbthat are siblings at the maximum depth. Sincexandyhave the lowest frequencies, we knowfreq(x) <= freq(a)andfreq(y) <= freq(b). We can swapxwithaandywithb. This move will either decrease the total cost of the tree or keep it the same, proving that a tree withxandyas deep siblings is at least as good as the original optimal tree.
- Proof sketch (by exchange argument): Take any optimal tree. If the two least frequent characters,
-
Optimal Substructure: Let
xandybe the two characters with the lowest frequencies. LetC'be the alphabetCbut withxandyremoved and replaced by a new characterzwherefreq(z) = freq(x) + freq(y). If we find an optimal treeT'for the alphabetC', we can then construct an optimal treeTfor the original alphabetCby takingT', finding the leaf forz, and replacing it with an internal node that hasxandyas its children. The cost of the two trees are directly related, so an optimal solution to the subproblem leads to an optimal solution for the original problem.
These two properties together prove that the greedy strategy of merging the two lowest-frequency nodes at each step yields an optimal solution.
Huffman Codes: An Information Theory Perspective
Grasping the proof can be tricky. This part of the Reducible video provides an excellent, intuitive explanation of why the greedy choice is safe and leads to an optimal structure.
Watch from 19:08 to 25:04. The video builds the logic for the bottom-up approach and shows how reformulating the cost function makes the greedy choice obvious. This is the 'aha!' moment of the algorithm.
Test your understanding! (Interview-style Question)
An interviewer says: "You've explained that Huffman's algorithm is greedy. But greedy algorithms can get stuck in local optima. For Interval Scheduling, we proved it worked with an exchange argument. Give me the high-level intuition for why the Huffman greedy choice doesn't get stuck. Why is it always 'safe' to merge the two least frequent characters?"
Show answer
The intuition is that to minimize the total cost, Σ(frequency * depth), we absolutely must give the longest possible codewords (greatest depth) to the characters that will contribute the least to the total sum. Those are the characters with the lowest frequencies.
By merging the two least frequent characters, x and y, we are essentially "pushing them down" the tree together. We commit them to being at a deep level, which is exactly where they belong in an optimal solution. This choice doesn't prevent us from making optimal choices for the more frequent characters later on; in fact, it simplifies the problem by combining x and y into a single subproblem (z) that can now compete with other, more frequent characters for shallower positions in the tree. Because this greedy choice aligns perfectly with the properties of an optimal solution (least frequent items must be deep), it's a "safe" move that will provably lead to the global optimum.
5. Implementation in Python
Let's bring all these concepts together into a clean, interview-ready Python implementation using the heapq module for our priority queue.
import heapq
# Define the Node for our Huffman Tree
class Node:
def __init__(self, char, freq):
self.char = char
self.freq = freq
self.left = None
self.right = None
# This is crucial for the priority queue to compare nodes.
# We want a min-heap, so we compare by frequency.
def __lt__(self, other):
return self.freq < other.freq
def generate_huffman_codes(text):
"""
Builds a Huffman tree and generates codes for a given text.
Time Complexity: O(n log n) where n is the number of unique characters.
Space Complexity: O(n) to store the frequency map and the tree.
"""
if not text:
return {}, None
# 1. Calculate frequencies
freq_map = {}
for char in text:
freq_map[char] = freq_map.get(char, 0) + 1
# 2. Initialize the min-heap (priority queue) with leaf nodes
priority_queue = [Node(char, freq) for char, freq in freq_map.items()]
heapq.heapify(priority_queue)
# Handle the edge case of text with only one unique character
if len(priority_queue) == 1:
char = priority_queue[0].char
codes = {char: '0'}
return codes, priority_queue[0]
# 3. Build the Huffman Tree
while len(priority_queue) > 1:
# a. Extract the two nodes with the lowest frequencies
left_child = heapq.heappop(priority_queue)
right_child = heapq.heappop(priority_queue)
# b. Create a new internal node
# The character can be a placeholder like '$' as it's not a leaf
merged_freq = left_child.freq + right_child.freq
parent_node = Node('$', merged_freq)
parent_node.left = left_child
parent_node.right = right_child
# c. Insert the new node back into the priority queue
heapq.heappush(priority_queue, parent_node)
# The remaining node is the root of the Huffman tree
root = priority_queue[0]
# 4. Traverse the tree to generate codes
codes = {}
def build_codes_recursive(node, current_code):
if node is None:
return
# If it's a leaf node, we've found a character's code
if node.char != '$':
codes[node.char] = current_code
return
build_codes_recursive(node.left, current_code + "0")
build_codes_recursive(node.right, current_code + "1")
build_codes_recursive(root, "")
return codes, root
# --- Example Usage ---
text = "AAAAABBC"
codes, tree = generate_huffman_codes(text)
print(f"Text: {text}")
print("Frequencies:")
freq_map = {char: text.count(char) for char in set(text)}
for char, freq in freq_map.items():
print(f" '{char}': {freq}")
print("\nHuffman Codes:")
for char, code in codes.items():
print(f" '{char}': {code}")
encoded_string = "".join([codes[char] for char in text])
print(f"\nEncoded string: {encoded_string}")
print(f"Original size (fixed 2-bit): {len(text) * 2} bits")
print(f"Compressed size: {len(encoded_string)} bits")
Conclusion
Today, you've added another powerful greedy algorithm to your toolkit. Huffman Coding is a perfect example of how a simple, locally optimal choice—merging the two least frequent nodes—can lead to a provably global optimum for a complex problem.
Key Takeaways:
- The Problem: Huffman coding provides lossless data compression by assigning variable-length, unambiguous prefix codes.
- The Greedy Choice: Always merge the two nodes with the lowest frequencies into a new subtree. This builds the optimal encoding tree from the bottom up.
- The Right Tool: A min-priority queue (min-heap) is the ideal data structure to efficiently implement the greedy choice, yielding an algorithm.
- The Proof: The algorithm's optimality is guaranteed by the greedy-choice property (least frequent characters belong at the deepest levels) and optimal substructure (an optimal solution to a subproblem extends to an optimal global solution).
Preview of the Next Lesson:
You have now completed our module on foundational algorithms, covering both Dynamic Programming and Greedy strategies. We are now prepared to shift gears from single-machine algorithms to the challenges of large-scale, distributed systems. In our next lesson, we will begin our System Design module. Our first topic will be Capacity Estimation, where you'll learn the "back-of-the-envelope" calculation skills essential for planning the storage, bandwidth, and computational needs of systems like Twitter or Netflix.