چگونگی کار الگوریتم ها
Chapter 6 Strings

فصل 6: رشته‌ها در الگوریتم‌ها

رشته‌ها یک نوع داده اساسی در علوم کامپیوتر هستند که توالی‌های کاراکتر را نمایش می‌دهند. مطالعه الگوریتم‌ها و ساختارهای داده برای پردازش رشته‌ها بخش مهمی از هر برنامه درسی علوم کامپیوتر است. در این فصل، چندین الگوریتم مهم پردازش رشته را بررسی می‌کنیم، از جمله مرتب‌سازی رشته‌ها، تری‌ها، جستجوی زیررشته، عبارات منظم و فشرده‌سازی داده. این الگوریتم‌ها کاربردهای عملی متعددی دارند، از ویرایش متن و بازیابی اسناد تا بیوانفورماتیک و پردازش زبان طبیعی.

مرتب‌سازی رشته‌ها

مرتب‌سازی یک عملیات اساسی در علوم کامپیوتر است، و مرتب‌سازی رشته‌ها یک وظیفه رایج در بسیاری از برنامه‌ها است. در حالی که الگوریتم‌های مرتب‌سازی کلی مانند کویک‌سورت و ادغام‌سورت می‌توانند برای مرتب‌سازی رشته‌ها استفاده شوند، الگوریتم‌های کارآمدتری وجود دارند که از ویژگی‌های خاص رشته‌ها بهره می‌برند.

شمارش کلید-شاخص‌دار

شمارش کلید-شاخص‌دار یک الگوریتم ساده و کارآمد برای مرتب‌سازی رشته‌هایی است که از مجموعه کوچکی از کاراکترهای متمایز تشکیل شده‌اند. ایده اصلی این است که تعداد دفعات ظهور هر کاراکتر را شمارش کرده و سپس از این شمارش‌ها برای تعیین موقعیت هر رشته در ترتیب مرتب‌شده استفاده کنیم.

اینجا مثالی از شمارش کلید-شاخص‌دار را مشاهده می‌کنید:

ورودی:  ["dab", "cab", "fad", "bad", "dad", "ebb", "ace"]
خروجی: ["ace", "bad", "cab", "dab", "dad", "ebb", "fad"]

الگوریتم به شرح زیر عمل می‌کند:

  1. فراوانی هر کاراکتر در رشته‌ها را شمارش کنید.
  2. شاخص شروع هر کاراکتر در آرایه مرتب‌شده را محاسبه کنید.
  3. رشته‌ها را به آرایه مرتب‌شده کپی کنید، با استفاده از شمارش‌ها برای تعیین موقعیت‌ها.

شمارش کلید-شاخص‌دار در زمان O(n + R) اجرا می‌شود، که در آن n تعداد کل کاراکترهای رشته‌ها و R اندازه الفبا (تعداد کاراکترهای متمایز) است. این آن را به یک الگوریتم بسیار کارآمد برای مرتب‌سازی رشته‌ها با اندازه الفبای کوچک تبدیل می‌کند.

مرتب‌سازی رادیکس LSD

مرتب‌سازی رادیکس کم‌ترین-رقم-مهم‌ترین (LSD) یک گسترش از شمارش کلید-شاخص‌داراینجا ترجمه فارسی فایل مارک‌داون است:

کانت که می‌تواند رشته‌های با طول برابر را بر روی یک الفبای بزرگ مرتب کند. ایده اصلی این است که رشته‌ها را از آخرین کاراکتر به سمت اولین کاراکتر مرتب کنیم.

اینجا مثالی از مرتب‌سازی LSD رادیکس داریم:

ورودی:  ["4PGC938", "2IYE230", "3CIO720", "1ICK750", "1OHV845", "4JZY524", "1ICK750", "3CIO720", "1OHV845", "1OHV845", "2RLA629", "2RLA629", "3ATW723"]
خروجی: ["1ICK750", "1ICK750", "1OHV845", "1OHV845", "1OHV845", "2IYE230", "2RLA629", "2RLA629", "3ATW723", "3CIO720", "3CIO720", "4JZY524", "4PGC938"]

الگوریتم به شرح زیر است:

  1. رشته‌ها را بر اساس آخرین کاراکتر با استفاده از شمارش کلید مرتب کنید.
  2. مرحله 1 را برای هر کاراکتر به سمت اول تکرار کنید.

مرتب‌سازی LSD رادیکس در زمان O(w * n) اجرا می‌شود، که در آن w طول رشته‌ها و n تعداد رشته‌ها است. این آن را به یک گزینه کارآمد برای مرتب‌سازی رشته‌های با طول ثابت تبدیل می‌کند.

مرتب‌سازی رادیکس MSD

مرتب‌سازی رادیکس MSD (Most-significant-digit-first) یک نسخه بازگشتی از مرتب‌سازی رادیکس است که می‌تواند رشته‌های با طول متفاوت را مرتب کند. مانند مرتب‌سازی سریع، مرتب‌سازی رادیکس MSD آرایه را به زیرآرایه‌هایی که می‌توانند مستقل مرتب شوند، تقسیم می‌کند.

اینجا مثالی از مرتب‌سازی رادیکس MSD داریم:

ورودی:  ["she", "sells", "seashells", "by", "the", "sea", "shore", "the", "shells", "she", "sells", "are", "surely", "seashells"]
خروجی: ["are", "by", "sea", "seashells", "seashells", "sells", "sells", "she", "she", "shells", "shore", "surely", "the", "the"]

الگوریتم به شرح زیر است:

  1. آرایه را بر اساس اولین کاراکتر هر رشته به زیرآرایه‌ها تقسیم کنید.
  2. هر زیرآرایه را به طور بازگشتی مرتب کنید.

مرتب‌سازی رادیکس MSD در بدترین حالت در زمان O(n * w) اجرا می‌شود، اما در عمل اغلب سریع‌تر از مرتب‌سازی رادیکس LSD برای رشته‌های با طول متفاوت است.

تری‌ها

تری (به انگلیسی: trie) (تلفظ: "تری") یک ساختار داده درختی‌مانند برای ذخیره رشته‌ها است که امکان مطابقت پیشوند و جستجوی رشته را فراهم می‌کند. هر گره در یک تری یک کاراکتر را نمایش می‌دهد و مسیر از ریشه تا یک گره، پیشوندی از آن رشته را نشان می‌دهد.اینجا ترجمه فارسی فایل مارک‌داون داده شده است:

این یک مثال از یک تری است که رشته‌های "sea"، "sells"، "she"، "shells" و "shore" را ذخیره می‌کند:

     (ریشه)
    /  |  \
   s   h   o
  / \     / \
 e   h   r   t
 |   |   |   |
 a   e   e   h
     |       |
     l       e
     |       |
     l       r
     |
     s

تری‌ها پشتیبانی از عملیات زیر را دارند:

  • افزودن یک رشته به تری.
  • جستجوی یک رشته در تری.
  • حذف یک رشته از تری.
  • پیدا کردن همه رشته‌ها با یک پیشوند داده شده.

پیچیدگی زمانی این عملیات O(w) است، که w طول رشته‌ای است که افزوده، جستجو یا حذف می‌شود. این موضوع تری‌ها را به ساختار داده‌ای بسیار کارآمد برای کارهای مرتبط با رشته تبدیل می‌کند.

جستجوی زیررشته

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

جستجوی زور خام

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

اینجا یک مثال از جستجوی زور خام آورده شده است:

متن:    "abacadabrabracabracadabrabrabracad"
الگو: "abracadabra"
خروجی:  [13]

الگوریتم زور خام در بدترین حالت زمان اجرای O(n * m) دارد، که n طول متن و m طول الگو است. در حالی که پیاده‌سازی آن ساده است، اما ممکن است برای متن‌ها و الگوهای بزرگ ناکارآمد باشد.

الگوریتم کنوت-موریس-پرات

الگوریتم کنوت-موریس-پرات (KMP) یک الگوریتم کارآمد برای جستجوی زیررشته است که از یک "تابع شکست" پیش‌محاسبه‌شده برای اجتناب از مقایسه‌های غیرضروری استفاده می‌کند.

تابع شکست به ما می‌گوید طول بلندترین پیشوند مناسب الگو که همچنین پسوندی از زیررشته‌ای از الگو که تاکنون مطابقت داده‌ایم نیز هست. این به ما اجازه می‌دهد که هنگام پیدا کردن عدم تطابق، به جای برگشت به عقب، به جلو بپریم.

اینجا یک مثال از الگوریتم KMP آورده شده است:

متن:    "abacadabrabrاینجا ترجمه فارسی فایل مارک‌داون است:

الگوی: "abracadabra"
خروجی: [13]

الگوریتم KMP دارای زمان اجرای O(n + m) است، که آن را بسیار کارآمدتر از رویکرد زور-خام برای متن‌ها و الگوهای بزرگ می‌کند.

الگوریتم بویر-مور

الگوریتم بویر-مور یک الگوریتم جستجوی زیررشته کارآمد دیگر است که با پیش‌پردازش رشته الگو کار می‌کند. برخلاف KMP که مقایسه‌ها را از ابتدای الگو شروع می‌کند، بویر-مور از انتها شروع و به عقب حرکت می‌کند.

ایده کلیدی پشت بویر-مور استفاده از دو تابع از پیش محاسبه‌شده است: تابع "پسوند خوب" و تابع "کاراکتر بد". این توابع به ما اجازه می‌دهند هنگام یافتن عدم تطابق، مانند KMP، در متن جلو برویم.

اینجا مثالی از الگوریتم بویر-مور است:

متن: "abacadabrabracabracadabrabrabracad" الگو: "abracadabra" خروجی: [13]


بویر-مور در بهترین حالت دارای زمان اجرای O(n/m) و در بدترین حالت دارای زمان اجرای O(n * m) است، اما در عمل، اغلب سریع‌ترین الگوریتم جستجوی زیررشته برای الفبای بزرگ است.

## عبارات منظم

عبارات منظم ابزار قدرتمندی برای توصیف الگوها در رشته‌ها هستند. آنها یک نوتاسیون مختصر و انعطاف‌پذیر برای تطبیق، جستجو و دستکاری متن ارائه می‌دهند.

یک عبارت منظم یک توالی از کاراکترها است که یک الگوی جستجو را تعریف می‌کند. ساده‌ترین شکل یک عبارت منظم یک رشته لیترال مانند "hello" است که دقیقاً رشته "hello" را تطبیق می‌دهد. با این حال، عبارات منظم همچنین شامل کاراکترها و ساختارهای ویژه هستند که امکان الگوهای پیچیده‌تر را فراهم می‌کنند:

- `.` (نقطه) هر کاراکتر تک را تطبیق می‌دهد.
- `*` (ستاره) صفر یا بیشتر تکرار کاراکتر یا گروه قبلی را تطبیق می‌دهد.
- `+` (پلاس) یک یا بیشتر تکرار کاراکتر یا گروه قبلی را تطبیق می‌دهد.
- `?` (علامت سوال) صفر یا یک تکرار کاراکتر یا گروه قبلی را تطبیق می‌دهد.
- `^` (کاشت) ابتدای خط را تطبیق می‌دهد.
- `$` (دلار) انتهای خط را تطبیق می‌دهد.
- `[]` (کروشه‌ها) یک کلاس کاراکتر را تعریف می‌کنند که هر کاراکتر تک را تطبیق می‌دهد.اینجا ترجمه فارسی فایل مارک‌داون است:

اینجا چند مثال از عبارات منظم و الگوهایی که آن‌ها مطابقت می‌کنند وجود دارد:

- `a.b` هر رشته سه‌کاراکتری که با "a" شروع و با "b" پایان می‌یابد را مطابقت می‌دهد، مانند "acb"، "a5b" یا "a b".
- `a*b` هر رشته‌ای که شامل صفر یا بیشتر کاراکتر "a" و سپس یک "b" است را مطابقت می‌دهد، مانند "b"، "ab"، "aab" یا "aaab".
- `a+b` هر رشته‌ای که شامل یک یا بیشتر کاراکتر "a" و سپس یک "b" است را مطابقت می‌دهد، مانند "ab"، "aab" یا "aaab"، اما نه "b".
- `a?b` یا "ab" یا "b" را مطابقت می‌دهد.
- `^a` هر رشته‌ای که با "a" شروع شود را مطابقت می‌دهد.
- `a$` هر رشته‌ای که با "a" پایان یابد را مطابقت می‌دهد.
- `[aeiou]` هر کاراکتر مصوت را مطابقت می‌دهد.

عبارات منظم توسط بسیاری از زبان‌های برنامه‌نویسی و ابزارها، از جمله جاوا، پایتون و ابزارهای یونیکس مانند grep و sed پشتیبانی می‌شوند. آن‌ها به طور گسترده برای وظایفی مانند اعتبارسنجی داده، پردازش متن و تحلیل لاگ استفاده می‌شوند.

## فشرده‌سازی داده

فشرده‌سازی داده فرآیند رمزگذاری اطلاعات با استفاده از تعداد کمتری بیت نسبت به نمایش اصلی است. این برای کاهش نیازهای ذخیره‌سازی و زمان انتقال مفید است. دو نوع اصلی فشرده‌سازی وجود دارد: بدون تلفات و با تلفات. فشرده‌سازی بدون تلفات امکان بازسازی کامل داده اصلی از داده فشرده‌شده را فراهم می‌کند، در حالی که فشرده‌سازی با تلفات امکان از دست دادن برخی اطلاعات را در ازای نسبت فشرده‌سازی بیشتر فراهم می‌کند.

### رمزگذاری طول دنباله

رمزگذاری طول دنباله (RLE) یک تکنیک ساده فشرده‌سازی بدون تلفات است که دنباله‌های تکراری از نمادهای یکسان (دنباله‌ها) را با یک نمونه تک از نماد و شمارش تعداد دفعات تکرار آن جایگزین می‌کند.

اینجا مثالی از RLE است:

ورودی: "AAAABBBCCCDDDEEE" خروجی: "4A3B3C3D3E"


RLE برای داده‌هایی با دنباله‌های طولانی زیاد، مانند تصاویر گرافیکی ساده، موثر است. با این حال، می‌تواند اندازه داده را افزایش دهد اگر دنباله‌های کم یا وجود نداشته باشند.

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

کدگذاری هافمن یک الگوریتم فشرده‌سازی بدون تلفات است که کدهای متغیر طول را به نمادها بر اساس فراوانی آن‌ها اختصاص می‌دهد.فایل مارک‌داون را به فارسی ترجمه می‌کنم، به جز کد که فقط توضیحات آن را ترجمه می‌کنم:

کدگذاری هافمن (Huffman Coding)
کدگذاری هافمن یک روش کدگذاری بهینه است که برای فشرده‌سازی داده‌ها استفاده می‌شود. این روش بر اساس فراوانی نمادها در داده‌های ورودی عمل می‌کند. نمادهای با فراوانی بیشتر به کدهای کوتاه‌تر و نمادهای با فراوانی کمتر به کدهای طولانی‌تر اختصاص داده می‌شوند.

الگوریتم با ساخت یک درخت دودویی به نام درخت هافمن از پایین به بالا کار می‌کند. هر گره برگ نماینده یک نماد و فراوانی آن است، در حالی که هر گره داخلی نماینده مجموع فراوانی فرزندان آن است. درخت با ادغام مکرر دو گره با کمترین فراوانی ساخته می‌شود تا فقط یک گره باقی بماند.

پس از ساخت درخت، کد هر نماد با مسیر از ریشه به گره برگ مربوطه تعیین می‌شود، به طوری که شاخه چپ نماینده "0" و شاخه راست نماینده "1" است.

اینجا مثالی از کدگذاری هافمن آورده شده است:

ورودی: "AAAABBBCCCDDDEEE" خروجی: "000010011001011100101"

درخت هافمن: (15) /
(7) (8) / \ /
(4) (3) (3) (5) /\ /\ /\ /
A B C D E


در این مثال، "A" به کد "00"، "B" به کد "01"، "C" به کد "10"، "D" به کد "110" و "E" به کد "111" اختصاص داده شده است. خروجی فشرده با جایگزینی هر نماد در ورودی با کد مربوطه به دست می‌آید.

کدگذاری هافمن یک کد پیشوندی بهینه است، به این معنی که هیچ کدی پیشوند کد دیگری نیست. این امر باعث می‌شود که فشرده‌سازی داده‌ها به طور یکتا قابل رمزگشایی باشد. کدگذاری هافمن در قالب‌های فایل و ابزارهای فشرده‌سازی مختلف مانند JPEG، MP3 و ZIP استفاده می‌شود.

### فشرده‌سازی LZW

فشرده‌سازی لمپل-زیو-ولچ (LZW) یک الگوریتم فشرده‌سازی مبتنی بر دیکشنری است که در حین فشرده‌سازی ورودی، دیکشنری (یا کتابچه کدها) از رشته‌ها را می‌سازد. LZW در ابزارهای فشرده‌سازی فایل گسترده استفاده می‌شود و در قالب تصویر GIF نیز به کار رفته است.

ایده اصلی LZW جایگزینی رشته‌های کاراکترها با کدهای تک‌رقمی است. این الگوریتم ورودی را کاراکتر به کاراکتر می‌خواند و با جایگزینی هر رشته ثابت‌طول با یک کد متغیر‌طول، آن را به شکل فشرده‌تری کدگذاری می‌کند. هرچه رشته طولانی‌تر باشد، فضای بیشتری با کدگذاری آن به عنوان یک عدد تک‌رقمی ذخیره می‌شود.

اینجا مثالی از ...اینجا ترجمه فارسی فایل مارک‌داون است:

1. فرهنگ لغت را با تمام رشته‌های تک‌کاراکتری آغاز کنید.
2. طولانی‌ترین رشته W را در فرهنگ لغت که با ورودی فعلی مطابقت دارد، پیدا کنید.
3. شاخص فرهنگ لغت برای W را به خروجی ارسال کنید و W را از ورودی حذف کنید.
4. W را به همراه نماد بعدی در ورودی به فرهنگ لغت اضافه کنید.
5. به مرحله 2 بروید.

حالا یک مثال را در نظر بگیریم. فرض کنید می‌خواهیم رشته "ABABABABA" را با استفاده از LZW فشرده کنیم.

1. فرهنگ لغت را با "A" و "B" آغاز کنید.
2. طولانی‌ترین مطابقت "A" است. شاخص آن (0) را ارسال کنید و آن را از ورودی حذف کنید. فرهنگ لغت اکنون شامل "A"، "B" و "AB" است.
3. طولانی‌ترین مطابقت "B" است. شاخص آن (1) را ارسال کنید و آن را از ورودی حذف کنید. فرهنگ لغت اکنون شامل "A"، "B"، "AB" و "BA" است.
4. طولانی‌ترین مطابقت "AB" است. شاخص آن (2) را ارسال کنید و آن را از ورودی حذف کنید. فرهنگ لغت اکنون شامل "A"، "B"، "AB"، "BA" و "ABA" است.
5. طولانی‌ترین مطابقت "ABA" است. شاخص آن (4) را ارسال کنید و آن را از ورودی حذف کنید. فرهنگ لغت اکنون شامل "A"، "B"، "AB"، "BA"، "ABA" و "ABAB" است.
6. طولانی‌ترین مطابقت "BA" است. شاخص آن (3) را ارسال کنید. ورودی اکنون خالی است.

بنابراین، نمایش فشرده‌شده "ABABABABA" توالی شاخص‌های [1] است که برای نمایش آن به تعداد کمتری بیت نیاز است.

فرآیند بازگشایی مشابه است، اما به صورت معکوس:

1. فرهنگ لغت را با تمام رشته‌های تک‌کاراکتری آغاز کنید.
2. یک کد X را از ورودی بخوانید.
3. رشته مربوط به X را از فرهنگ لغت خروجی دهید.
4. اگر کد قبلی وجود داشته باشد، رشته قبلی را به همراه اولین کاراکتر رشته مربوط به X به فرهنگ لغت اضافه کنید.
5. به مرحله 2 بروید.

فشرده‌سازی LZW ساده و سریع است و برای بسیاری از کاربردها گزینه خوبی است. با این حال، محدودیت‌هایی نیز دارد. اندازه فرهنگ لغت می‌تواند بسیار بزرگ شود و مقدار قابل توجهی از حافظه را مصرف کند. همچنین، فرهنگ لغت پس از هر بلوک ورودی بازنشانی می‌شود که می‌تواند نسبت فشرده‌سازی را برای فایل‌های کوچک کاهش دهد.

با وجود این محدودیت‌هااینجا ترجمه فارسی فایل مارک‌داون است. برای کد، فقط نظرات را ترجمه کنید، نه کد را.

## نتیجه‌گیری

در این فصل، ما چندین الگوریتم مهم پردازش رشته‌ای را بررسی کردیم، از جمله مرتب‌سازی رشته‌ها، تریه‌ها، جستجوی زیررشته، عبارات منظم و فشرده‌سازی داده‌ها. این الگوریتم‌ها پایه و اساس بسیاری از برنامه‌های کاربردی واقعی هستند و ابزارهای ضروری برای هر برنامه‌نویسی که با داده‌های متنی کار می‌کند.

ما با بحث در مورد مرتب‌سازی رشته‌ها شروع کردیم، که الگوریتم‌های مرتب‌سازی بهینه‌شده هستند که از ویژگی‌های خاص رشته‌ها استفاده می‌کنند. شمارش کلید‌دار، مرتب‌سازی رادیکس LSD و مرتب‌سازی رادیکس MSD روش‌های کارآمدی برای مرتب‌سازی رشته‌ها بر اساس کاراکترهای فردی آن‌ها ارائه می‌دهند.

سپس، ما تریه‌ها را بررسی کردیم، که ساختار داده‌ای درخت‌مانند برای ذخیره و بازیابی رشته‌ها است. تریه‌ها امکان تطبیق پیشوند سریع را فراهم می‌کنند و به طور معمول در برنامه‌هایی مانند تکمیل خودکار و جداول مسیریابی IP استفاده می‌شوند.

الگوریتم‌های جستجوی زیررشته، مانند الگوریتم‌های Knuth-Morris-Pratt و Boyer-Moore، به ما امکان می‌دهند تا به طور کارآمد الگوهایی را در رشته‌های بزرگ‌تر جستجو کنیم. این الگوریتم‌ها کاربردهای متعددی در ویرایش متن، زیست‌شناسی محاسباتی و بازیابی اطلاعات دارند.

عبارات منظم راهی قدرتمند و انعطاف‌پذیر برای توصیف الگوهای رشته‌ای ارائه می‌دهند. ما نحوه پایه‌ای عبارات منظم و چگونگی استفاده از آن‌ها برای تطبیق الگو و دستکاری رشته در زبان‌های برنامه‌نویسی و ابزارهای مختلف را بررسی کردیم.

در نهایت، ما الگوریتم‌های فشرده‌سازی داده را کاوش کردیم، که اندازه داده را با بهره‌برداری از افزونگی و الگوها در ورودی کاهش می‌دهند. ما کدگذاری طول تکرار، کدگذاری هافمن و فشرده‌سازی Lempel-Ziv-Welch را پوشش دادیم، که هر کدام قوت‌ها و کاربردهای خاص خود را دارند.

درک این الگوریتم‌ها و ساختارهای داده‌ای پردازش رشته‌ای برای هر کسی که با داده‌های متنی کار می‌کند، حیاتی است. همانطور که حجم داده‌های غیرساختاریافته ادامه می‌یابد، توانایی دستکاری، جستجو و فشرده‌سازی کارآمد آن‌ها اهمیت بیشتری پیدا می‌کند.Here is the Persian translation of the provided text, with the code comments translated:

فشار رشته‌ها تنها ارزشمندتر خواهند شد. با تسلط بر تکنیک‌های پوشش داده شده در این فصل، شما به خوبی مجهز خواهید بود تا با طیف گسترده‌ای از چالش‌های پردازش رشته در پروژه‌ها و برنامه‌های خود مقابله کنید.

```python
# این تابع یک رشته را به حروف بزرگ تبدیل می‌کند
def to_uppercase(text):
    return text.upper()

# این تابع یک رشته را به حروف کوچک تبدیل می‌کند
def to_lowercase(text):
    return text.lower()

# این تابع طول یک رشته را برمی‌گرداند
def get_length(text):
    return len(text)

# این تابع یک رشته را معکوس می‌کند
def reverse_string(text):
    return text[::-1]