A Huffman code tree starts with one leaf per training symbol and repeatedly joins the two lowest-frequency roots. The merged node's weight is their sum. A left edge contributes zero and a right edge contributes one, so every leaf receives a codeword and no leaf code is a prefix of another. This model uses a binary heap to build a fixed codebook, then encodes only symbols present in that training alphabet. The decoder walks the retained tree and emits a symbol whenever it reaches a leaf. A one-symbol alphabet gets code zero so its encoded length remains explicit. The codebook is part of the decoding contract, not an optional afterthought.
Huffman trees: assign prefix codes from symbol frequencies
Operational case
A codebook trained on dispatch dispatch urgent has fourteen distinct symbols. Encoding dispatch urgent yields 57 bits in this run, and the decoder restores the exact phrase. Equal-frequency symbols are resolved by a serial tie value, making the output reproducible without implying a unique Huffman bit assignment across all implementations. An unknown symbol cannot be encoded through this codebook. A trailing bit sequence that ends inside an internal node is incomplete and rejected; for a one-symbol alphabet, only zero is a valid code bit.
Working Python program
from collections import Counter
from heapq import heappop, heappush
class CodeNode:
def __init__(self, symbol=None, left=None, right=None):
self.symbol, self.left, self.right = symbol, left, right
class IncidentCodebook:
def __init__(self, training_text):
frequencies = Counter(training_text)
if not frequencies:
raise ValueError("training text must contain a symbol")
heap = []
serial = 0
for symbol, count in sorted(frequencies.items()):
heappush(heap, (count, serial, CodeNode(symbol=symbol)))
serial += 1
while len(heap) > 1:
left_count, _, left = heappop(heap)
right_count, _, right = heappop(heap)
heappush(heap, (left_count + right_count, serial, CodeNode(left=left, right=right)))
serial += 1
self.root = heap[0][2]
self.codes = {}
def assign(node, bits):
if node.symbol is not None:
self.codes[node.symbol] = bits or "0"
return
assign(node.left, bits + "0")
assign(node.right, bits + "1")
assign(self.root, "")
def encode(self, text):
return "".join(self.codes[symbol] for symbol in text)
def decode(self, bits):
if any(bit not in "01" for bit in bits):
raise ValueError("nonbinary input")
if self.root.symbol is not None:
if any(bit != "0" for bit in bits):
raise ValueError("invalid one-symbol code")
return self.root.symbol * len(bits)
output, cursor = [], self.root
for bit in bits:
cursor = cursor.left if bit == "0" else cursor.right
if cursor.symbol is not None:
output.append(cursor.symbol)
cursor = self.root
if cursor is not self.root:
raise ValueError("incomplete final codeword")
return "".join(output)
if __name__ == "__main__":
training = "dispatch dispatch urgent"
book = IncidentCodebook(training)
payload = "dispatch urgent"
bits = book.encode(payload)
print(len(book.codes), len(bits))
print(book.decode(bits))Output
14 57
dispatch urgentTime, space, and tradeoff
For A distinct symbols, frequency counting takes O(N) time on N training characters and heap construction takes O(A log A) in this direct implementation. Encoding and decoding visit O(B) code bits for encoded length B, while codebook and tree storage are O(A) nodes plus the stored code strings. The output in the example is a Python string of zero and one characters, not packed bits, and it excludes any header needed to transmit the tree. A canonical codebook and bit packing would change serialization details. A frequency change after publication requires a newly agreed codebook.
Common Mistakes
- Do not assume the bitstream is decodable without the matching codebook.
- Do not give the single-symbol alphabet an empty codeword.
- Do not accept a stream ending in the middle of a codeword.
- Do not compare Python text bits with packed file bytes as if they were equal storage.
Connected lessons
- Trees and Heaps
- Data Structures
- Binary heaps: select the next priority with a tie rule
- Front-coded lexicons: store shared prefixes within sorted term blocks
- Tournament trees: merge sorted runs through one winner path
- Projects
- Quizzes
Compare its query and update boundary with LZ78 phrase tries: emit dictionary index and next symbol, Segment-tree stabbing indexes: list intervals active at one point, Condensation DAGs: compress directed cycles before path queries, then complete the structure audit and decision quiz.
