کدگذاری هافمن
برای هر نماد کدی دودویی میسازد که نمادهای پرتکرار کد کوتاهتری بگیرند: هر بار دو درخت سبکتر را زیر یک ریشه جمع میکند.
با کد ثابت، هر نماد از الفبای k تایی ⌈log₂ k⌉ بیت میگیرد. اگر بعضی نمادها خیلی پرتکرارتر باشند، بهتر است کد کوتاهتری بگیرند. کد باید پیشوندی باشد (هیچ کدی پیشوند کد دیگر نباشد) تا بشود بدون جداکننده خواندش؛ هر کد پیشوندی یک درخت دودویی است.
- زمان:
O(n + k log k) for k symbols - حافظه:
O(k) - CLRS، بخش ۱۵.۳
- گریمالدی، فصل ۱۲
- جانسونباو، فصل ۹
فراوانیهای کتاب CLRS (a:45، b:13، c:12، d:16، e:9، f:5) در متنی به طول ۱۰۰؛ کد کل ۲۲۴ بیت میشود، در برابر ۳۰۰ بیت با کد ثابت ۳ بیتی.
import heapqclass Node:def __init__(self, key, symbol=None, left=None, right=None):self.key = key # what the picture shows: "a:5" for a leaf, the total count insideself.symbol = symbolself.left = leftself.right = rightdef huffman(text):freq = {}for ch in text:freq[ch] = freq.get(ch, 0) + 1# A min-heap of (count, order, tree); the order breaks ties, so every run builds the same tree.heap = []for ch in sorted(freq):name = "␣" if ch == " " else chheap.append((freq[ch], len(heap), Node(f"{name}:{freq[ch]}", ch)))heapq.heapify(heap)order = len(heap)root = Nonewhile len(heap) > 1:f1, _, a = heapq.heappop(heap)f2, _, b = heapq.heappop(heap)root = Node(str(f1 + f2), None, a, b)heapq.heappush(heap, (f1 + f2, order, root))order = order + 1root = heap[0][2] if heap else Nonecodes = {}if root is not None:assign(root, "", codes)rows = [("␣" if ch == " " else ch, freq[ch], codes[ch]) for ch in sorted(freq)]bits = sum(freq[ch] * len(codes[ch]) for ch in freq)return bitsdef assign(node, prefix, codes):if node.left is None:code = prefix if prefix else "0" # a lone symbol still needs one bitcodes[node.symbol] = codereturnassign(node.left, prefix + "0", codes)assign(node.right, prefix + "1", codes)
شروع اجرا
∅
جدول هنوز خالی است.
درخت فراخوانیهای بازگشتی
نارنجی پر = فراخوانی در حال اجرا · حاشیهی نارنجی = هنوز روی پشته · خاکستری = تمام شده
متغیرهای محلی
نارنجی = در همین گام عوض شد
Call stack
- huffman()خط 12
توضیح
ایده
با کد ثابت، هر نماد از الفبای k تایی ⌈log₂ k⌉ بیت میگیرد. اگر بعضی نمادها خیلی پرتکرارتر باشند، بهتر است کد کوتاهتری بگیرند. کد باید پیشوندی باشد (هیچ کدی پیشوند کد دیگر نباشد) تا بشود بدون جداکننده خواندش؛ هر کد پیشوندی یک درخت دودویی است.
الگوریتم حریصانه
- هر نماد یک درخت تکگره با وزن فراوانیاش است.
- هر بار دو درخت سبکتر را بردار و زیر یک ریشهی تازه (با وزن جمعشان) قرار بده.
- وقتی یک درخت ماند، مسیر هر برگ (چپ = ۰، راست = ۱) کد آن است.
چرا بهینه است؟
دو نماد کمتکرارتر را همیشه میشود در عمیقترین سطح و کنار هم گذاشت بیآنکه کد بدتر شود؛ پس جمع کردنشان اول امن است و بقیه همان مسئله با یک نماد کمتر است.
در trace به چه چیزی نگاه کنی
- تصویر آخرین درخت ساختهشده را نشان میدهد که مدام بزرگتر میشود؛ جدول در پایان نماد، فراوانی و کد را دارد.