AlgoVerse

کدگذاری هافمن

برای هر نماد کدی دودویی می‌سازد که نمادهای پرتکرار کد کوتاه‌تری بگیرند: هر بار دو درخت سبک‌تر را زیر یک ریشه جمع می‌کند.

با کد ثابت، هر نماد از الفبای k تایی ⌈log₂ k⌉ بیت می‌گیرد. اگر بعضی نمادها خیلی پرتکرارتر باشند، بهتر است کد کوتاه‌تری بگیرند. کد باید پیشوندی باشد (هیچ کدی پیشوند کد دیگر نباشد) تا بشود بدون جداکننده خواندش؛ هر کد پیشوندی یک درخت دودویی است.

ادامه‌ی توضیح ↓
ورودی:
text="aaaaaaaaaaaa…"خروجی PDF

فراوانی‌های کتاب CLRS (a:45، b:13، c:12، d:16، e:9، f:5) در متنی به طول ۱۰۰؛ کد کل ۲۲۴ بیت می‌شود، در برابر ۳۰۰ بیت با کد ثابت ۳ بیتی.

  1. import heapq
  2. class Node:
  3. def __init__(self, key, symbol=None, left=None, right=None):
  4. self.key = key # what the picture shows: "a:5" for a leaf, the total count inside
  5. self.symbol = symbol
  6. self.left = left
  7. self.right = right
  8. def huffman(text):
  9. freq = {}
  10. for ch in text:
  11. freq[ch] = freq.get(ch, 0) + 1
  12. # A min-heap of (count, order, tree); the order breaks ties, so every run builds the same tree.
  13. heap = []
  14. for ch in sorted(freq):
  15. name = "␣" if ch == " " else ch
  16. heap.append((freq[ch], len(heap), Node(f"{name}:{freq[ch]}", ch)))
  17. heapq.heapify(heap)
  18. order = len(heap)
  19. root = None
  20. while len(heap) > 1:
  21. f1, _, a = heapq.heappop(heap)
  22. f2, _, b = heapq.heappop(heap)
  23. root = Node(str(f1 + f2), None, a, b)
  24. heapq.heappush(heap, (f1 + f2, order, root))
  25. order = order + 1
  26. root = heap[0][2] if heap else None
  27. codes = {}
  28. if root is not None:
  29. assign(root, "", codes)
  30. rows = [("␣" if ch == " " else ch, freq[ch], codes[ch]) for ch in sorted(freq)]
  31. bits = sum(freq[ch] * len(codes[ch]) for ch in freq)
  32. return bits
  33. def assign(node, prefix, codes):
  34. if node.left is None:
  35. code = prefix if prefix else "0" # a lone symbol still needs one bit
  36. codes[node.symbol] = code
  37. return
  38. assign(node.left, prefix + "0", codes)
  39. assign(node.right, prefix + "1", codes)

ناوردا: دو نماد کم‌تکرارتر در یک کد بهینه همیشه می‌توانند برادر و در پایین‌ترین سطح باشند؛ پس جمع کردن آن دو اول، به جواب بهینه می‌رسد.

شروع اجرا

∅

bits—

جدول هنوز خالی است.

درخت فراخوانی‌های بازگشتی

نارنجی پر = فراخوانی در حال اجرا · حاشیه‌ی نارنجی = هنوز روی پشته · خاکستری = تمام شده

متغیرهای محلی

text'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabbbbbbbbbbbbbccccccccccccddddddddddddddddeeeeeeeeefffff'

نارنجی = در همین گام عوض شد

Call stack

  1. huffman()خط 12
گام ۱ از ۳۶۹
خط سیاه = merge · خط نارنجی = گام کلیدیcounted: 0/1 · merge: 0/5 · code: 0/6 · total_bits: 0/1
    توضیح

    ایده

    با کد ثابت، هر نماد از الفبای k تایی ⌈log₂ k⌉ بیت می‌گیرد. اگر بعضی نمادها خیلی پرتکرارتر باشند، بهتر است کد کوتاه‌تری بگیرند. کد باید پیشوندی باشد (هیچ کدی پیشوند کد دیگر نباشد) تا بشود بدون جداکننده خواندش؛ هر کد پیشوندی یک درخت دودویی است.

    الگوریتم حریصانه

    1. هر نماد یک درخت تک‌گره با وزن فراوانی‌اش است.
    2. هر بار دو درخت سبک‌تر را بردار و زیر یک ریشه‌ی تازه (با وزن جمعشان) قرار بده.
    3. وقتی یک درخت ماند، مسیر هر برگ (چپ = ۰، راست = ۱) کد آن است.

    چرا بهینه است؟

    دو نماد کم‌تکرارتر را همیشه می‌شود در عمیق‌ترین سطح و کنار هم گذاشت بی‌آنکه کد بدتر شود؛ پس جمع کردنشان اول امن است و بقیه همان مسئله با یک نماد کمتر است.

    در trace به چه چیزی نگاه کنی

    • تصویر آخرین درخت ساخته‌شده را نشان می‌دهد که مدام بزرگ‌تر می‌شود؛ جدول در پایان نماد، فراوانی و کد را دارد.