کدگذاری/فشردهسازی - روش هافمن
مقدمه و معرفی الگوریتم
برای دیدن مسئلههای بیشتر در زمینهی الگوریتمهای رمزنگاری به مطالب زیر رجوع کنید:
-
-
- کدگذاری و فشردهسازی متون - روش 827
فرض کنید قصد داریم متن I am a C++ programmer from Iran را فشرده کنیم. با استفاده از الگوریتم کدگذاری هافمن، در قدم اول جدول فراوانی حروف در متن مورد نظر را تشکیل میدهیم. تعداد تکرار هر یک از حروف متن در جدول زیر آمده است. دقت کنید که برای خوانایی بیشتر، بهجای حرف فاصله (Space)، علامت ¬ نوشته شده است:
حرف | I | ¬ | a | m | C | + | p | r | o | g | e | f | n |
فراوانی | 2 | 6 | 4 | 4 | 1 | 2 | 1 | 5 | 2 | 1 | 1 | 1 | 1 |
الگوریتم کدگذاری هافمن سعی دارد بهجای هر یک از حروف از یک کد باینری کوتاه استفاده کند. کوتاهترین کدها (کدهایی با طول کمتر) به حروف پر تکرار اختصاص داده میشوند تا حجم متن کدشدهی نهایی، در نتیجهی تکرارها به حداقل برسد. برای جلوگیری از ابهام و دوگانگی هنگام تفسیر متن کدگذاری شده، کدها نباید پیشوند یکدیگر باشند، مسئلهای که یکی از چالشهای اصلی دیوید هافمن برای طراحی الگوریتمی کارامد بهشمار میرفت و در نهایت با معرفی ساختار درختی برای نگهداری کدها، این ویژگی اساسی را تثبیت کرد.
برای آشنایی با دادهساختار درخت دودویی و دریافت جزئیات بیشتر به معرفی درخت دودویی، پیادهسازی درخت دودویی با آرایهی خطی و پیادهسازی درخت دودویی با حافظهی پویا رجوع کنید.
به بیان دقیقتر، هیچ ترکیبی از کدها، نباید کد دیگری از همان مجموعه را تولید کند. در واقع صرف پیشوند بودن، فقط عملیات تفسیر متن کدشده را دچار مشکل میکند اما این ترکیب کدهاست که ابهام و بنبست را بهوجود میآورد. برای درک بهتر موضوع به یک مثال توجه کنید:
فرض کنید برای کد کردن 3 حرف A، B و C به ترتیب از کدهای 0، 1 و 01 استفاده میکنیم و میخواهیم پیامی را به فرد دیگری بفرستیم. فرستنده و گیرندهی پیام، جدول نگاشت کدها را در اختیار دارند. پیام عبارتاستاز CC، و طبق جدول، هریک از حروف C به کد 01 نگاشت شده و پیام رمز حاوی متن 0101 ارسال میشود. در سمت گیرنده برای بازیابی متن اصلی ابهام بهوجود میآید چون پیام دریافتی را میتوان به هریک از متون مجاز زیر ترجمه کرد:
0101 | ABAB |
0101 | ABC |
0101 | CAB |
01 01 | CC |
مشکل این سیستم کدگذاری ایناست که ترکیب کدهای 0 و 1 کد 01 را بهوجود میآورد و همانطور که در جدول بالا دیدیم، کد 01 بیش از یک تفسیر خواهد داشت. به هرحال در حالت کلی اگر ترکیب کد C1 و C2 کد C3 را تولید کند، حداقل بهاین معنی است که کد C1 پیشوند کد C3 است (و به همین ترتیب کد C2 پسوند آن است) و اگر ساختاری داشته باشیم که اجازه ندهد کدها بهصورت پیشوند همدیگر تعریف شوند، این ابهام رفع میشود.
برخلاف الگوریتم کدگذاری شانون-فانو درخت کدهای هافمن از پایین به بالا، یعنی از برگها ساخته میشود. همهی حروف در برگهای درخت هستند و گرههای میانی (حتی ریشه) هیچیک از حروف را شامل نمیشوند. درخت ساخته شده، یک درخت دودویی است و لذا هر گره حداکثر 2 فرزند دارد. برای شهود بهتر، فرزندان را بصورت فرزند چپ و فرزند راست در نظر گرفته و طبق قرارداد، شاخهی مربوط به فرزند چپ را با برچسب 0 و شاخهی مربوط به فرزند سمت راست را با برچسب 1 نشانهگذاری میکنیم. با شروع از ریشه، دنبالهی برچسبهای مسیر منتهی به هر یک از حروف (برگها)، کد مربوط به آن حرف است.
ساختار درخت دودویی، ویژگیهای منحصربهفردی را به ارمغان میآورد:
- باتوجه به اینکه مسیر از ریشه به برگ در درخت دودویی یکتاست، لذا هر حرف با یک کد یکتا متناظر است.
- چون حروف فقط در برگها ذخیره شدهاند، لذا در بین مسیر، هیچ حرفی وجود ندارد
و هیچیک از کدها در بین مسیر و پیش از رسیدن به برگ کامل نمیشود لذا تضمین کنندهی این است که کدها نمیتواند پیشوند یکدیگر باشند.
برگردیم به مثال اصلی و بهعنوان قدم بعدی میخواهیم درخت دوددویی کدها را بسازیم. گفتیم که درخت هافمن از پایین به بالا ساخته میشود لذا اولین برگهای ساخته شده که پایینتر از سایرین قرار میگیرند، احتمالا دورترین برگها به ریشه خواهند بود و بههمین خاطر دارای مسیر طولانی و کدهای بزرگتری هستند. کدهای بزرگتر به حروف کمتکرار اختصاص دارند، پس جدول فراوانی حروف را بر اساس تعداد تکرار به صورت صعودی (از کوچک به بزرگ) مرتب کرده و از ابتدای لیست شروع به برداشتن حروف و تشکیل درخت هافمن میکنیم. جدول فراوانی مرتبشده برای مثال دادهشده بهصورت زیر است:
حرف | C | p | g | e | f | n | I | + | o | a | m | r | ¬ |
فراوانی | 1 | 1 | 1 | 1 | 1 | 1 | 2 | 2 | 2 | 4 | 4 | 5 | 6 |
الگوریتم تشکیل درخت دودویی هافمن عبارت است از:
1- تا زمانیکه بیش از یک عنصر در جدول فراوانی وجود دارد، مراحل زیر را تکرار کن
1.1- جدول را بهصورت صعودی براساس فراوانی مرتب کن
1.2- دو عنصر ابتدایی n1 و n2 را از جدول حذف کن
و بهعنوان فرزندان چپ و راست یک درخت دودویی جدید با ریشهی
n قرار بده
1.3- فراوانی عنصر جدید
n برابر است با مجموع فراوانیهای
n1 و n2
1.4- عنصر جدید (n) را به جدول فراوانی اضافه کن
الگوریتم را برای جدول فراوانی بالا بهکار میگیریم. در مرحلهی اول دو عنصر ابتدایی لیست یعنی C و p از جدول حذف میشوند و درختی بهصورت زیر تشکیل میشود:
برای قرار دادن یک عنصر در فرزند چپ یا راست، لازم است تابعی بهعنوان قرارداد داشته باشیم. در اینجا مقدار فراوانی کوچکتر یا مساوی را بهعنوان فرزند چپ در نظر میگیریم. برای مقادیر فراوانی برابر، عنصر اول در چپ و عنصر بعدی در راست قرار میگیرد. حال این عنصر جدید را که Cp نام دارد و دارای فراوانی 2 است را به جدول فراوانی اضافه میکنیم. وضعیت جدول پس از این تغییرات و مرتبسازی دوباره به قرار زیر است:
حرف | g | e | f | n | I | + | o | Cp | a | m | r | ¬ |
فراوانی | 1 | 1 | 1 | 1 | 2 | 2 | 2 | 2 | 4 | 4 | 5 | 6 |
دو عنصر بعدی، g و e هستند. مانند قبل، آنها را از جدول حذف کرده و والد آنها را به جدول اضافه میکنیم. در زیر، گره جدید و ساختار جدول پس از حذف آنها را ملاحظه میکنید:
|
نوبت f و n است که از جدول حذف شوند. مشابه عناصر قبلی درخت جدیدی تشکیل داده و ریشهی آن را به جدول اضافه میکنیم. پس از مرتبسازی خواهیم داشت:
|
حروف I و + نیز به همان ترتیب حذف شده و گره جدید I+ با فراوانی 4 به جدول اضافه میشود:
|
دقت داشته باشید که هر یک از درختهای دودویی جدید، در کنار حروف به جدول فراوانی اضافه میشوند و هیچ تفاوتی بین آنها وجود ندارد. درواقع هر یک از حروف، درخت دودیی با یک گره (فقط ریشه) است. این مطلب مهم در گام بعدی باید بیشتر مورد توجه قرار بگیرد، جاییکه در آن، حرف o و ریشهی درخت دودویی Cp باهم ترکیب شده و درخت جدیدی با ریشهی oCp تولید میکنند:
حرف o فرزند چپ و کل درخت دودویی Cp بهعنوان فرزند راست قرار میگیرد. جدول فراوانی پس از این عملیات و مرتبسازی به شکل زیر خواهد بود:
حرف | ge | fn | a | m | I+ | oCp | r | ¬ |
فراوانی | 2 | 2 | 4 | 4 | 4 | 4 | 5 | 6 |
حال درختهای ge و fn را برمیداریم. از آنجاییکه پس از مرتبسازی، عنصر ge قبل از همه ظاهر شده، لذا بهعنوان فرزند چپ قرار گرفته و درخت دودویی جدید با ریشهی gefn بهشکل زیر ساخته شده و در جدول قرار داده میشود:
حرف | a | m | I+ | oCp | gefn | r | ¬ |
فراوانی | 4 | 4 | 4 | 4 | 4 | 5 | 6 |
حروف a و m هم به سادگی باهم ترکیب شده و درخت دودویی am را به شکل زیر تشکیل میدهند:
|
اجرای الگوریتم را تا رسیدن به تنها یک عنصر ادامه میدهیم. نتیجهی ترکیب گرههای I+ و oCp به قرار زیر است:
حرف | gefn | r | ¬ | am | I+oCp |
فراوانی | 4 | 5 | 6 | 8 | 8 |
و حال نوبت گرههای gefn و r است:
حرف | ¬ | am | I+oCp | gefnr |
فراوانی | 6 | 8 | 8 | 9 |
پس از ترکیب گرههای ¬ و am داریم:
حرف | I+oCp | gefnr | ¬am |
فراوانی | 8 | 9 | 14 |
با ترکیب I+oCp و gefnr داریم:
حرف | ¬am | I+oCpgefnr |
فراوانی | 14 | 17 |
در نهایت اینکه دو گره باقیمانده ¬am و I+oCpgefnr را باهم ترکیب میکنیم تا به ریشهی ¬amI+oCpgefnr که ترکیبی از همهی حروف را شامل میشود برسیم. درخت نهایی عبارت است:
پس از تشکیل درخت، گرههای میانی (گرههای زرد رنگ در تصویر بالا) صرفا برای شمارش گامها بوده و محتوای آنها هیچ استفادهای ندارند. برای شهود بهتر، ضمن حذف محتوای آنها، یالهایی که به سمت چپ هستند و قرار بود با برچسب 0 نشانهگذاری شوند را بهرنگ آبی، و یالهایی که به سمت راست هستند و قرار بود با برچسب 1 نشانهگذاری شوند را بهرنگ قرمز نشان میدهیم:
با شروع از ریشه، برچسبهای مسیر منتهی به هر یک از حروف موجود در برگها را یادداشت کرده و بهعنوان کد آن حرف در نظر میگیریم:
حرف | کد متناظر | حرف | کد متناظر | حرف | کد متناظر | ||
¬ | 00 | a | 010 | m | 011 | ||
r | 111 | I | 1000 | + | 1001 | ||
o | 1010 | C | 10110 | p | 10111 | ||
g | 11000 | e | 11001 | f | 11010 | ||
n | 11011 |