0Pricing
Coding Interview Prep · درس

ترميز السلاسل النصية وعكسها ومتلازمات التناظر

نفّذ عكس الكلمات في مكانها وترميز طول التتابع واكتشاف متلازمات التناظر، بما في ذلك تقنية التوسّع حول المركز

ترميز السلاسل النصية وعكسها ومتلازمات التناظر درس مجاني في Coding Interview Prep على CoddyKit. هذا هو الدرس 4 من أصل 4. يمكنك قراءة الدرس كاملاً أدناه مجاناً — ثم تمرن عليه مباشرة في المتصفح باستخدام محرر أكواد مدمج ومدرس ذكاء اصطناعي متاح 24/7. هذا الدرس جزء من مسار التعلم في Coding Interview Prep، وتقدمك يتزامن عبر الويب وتطبيق CoddyKit. تتضمن دورة Coding Interview Prep 4 دروس في المجموع.

عكس سلسلة نصية في مكانها

سلاسل Python النصية غير قابلة للتغيير، لذا يعني العكس «في مكانها» تحويلها إلى قائمة محارف، ثم تبديل المحارف باستخدام مؤشرين وضمّها. في التبديل الكلاسيكي باستخدام مؤشرين، ضع left عند الفهرس 0 وright عند الفهرس الأخير؛ بدّل المحارف وحرّك المؤشرين نحو الداخل حتى يتجاوز أحدهما الآخر. يستغرق ذلك O(n) من الوقت وO(n) من المساحة لقائمة المحارف، وهي مساحة لا يمكن الاستغناء عنها لأن السلاسل النصية غير قابلة للتغيير.

def reverse_string(s):
    chars = list(s)
    left, right = 0, len(chars) - 1
    while left < right:
        chars[left], chars[right] = chars[right], chars[left]
        left  += 1
        right -= 1
    return ''.join(chars)

print(reverse_string('hello'))   # 'olleh'
print(reverse_string('Hannah'))  # 'hannaH'

# Pythonic shortcut (creates new string):
print('hello'[::-1])  # 'olleh'

عكس الكلمات في جملة

اعكس ترتيب الكلمات مع إزالة المسافات الزائدة. الحل Python الواضح هو: استخدم split (فهو يتعامل مع المسافات المتعددة)، ثم اعكس القائمة، ثم استخدم join. وللعكس في مكانه على مصفوفة محارف، اعكس المصفوفة كاملة أولًا، ثم اعكس كل كلمة على حدة. يعمل هذا الأسلوب المكوّن من مرورين في O(n) من الوقت وO(n) من المساحة، وهي مساحة لا يمكن تجنّبها مع سلاسل Python النصية لأنها غير قابلة للتغيير.

def reverse_words(s):
    words = s.split()       # split and strip whitespace
    words.reverse()         # in-place reverse
    return ' '.join(words)  # single space between words

print(reverse_words('  hello   world  '))  # 'world hello'
print(reverse_words('a good example'))     # 'example good a'

# One-liner:
print(' '.join('  hello   world  '.split()[::-1]))

اكتشاف المتناظر: بسيط

تكون السلسلة النصية متناظرة إذا كانت مساوية لمعكوسها. أسرع طريقة للتحقق في Python هي: s == s[::-1]. أما في حالة السلاسل المتناظرة التي لا تحتوي إلا على محارف أبجدية رقمية ولا تميّز بين حالة الأحرف، وهي الصيغة الأكثر شيوعًا في المقابلات، فطبّع السلسلة أولًا: صفِّ المحارف غير الأبجدية الرقمية وحوّل الأحرف إلى أحرف صغيرة، ثم قارن. كلا الأسلوبين يعمل في O(n).

def is_palindrome(s):
    # Filter and normalise
    cleaned = ''.join(c.lower() for c in s if c.isalnum())
    return cleaned == cleaned[::-1]

print(is_palindrome('A man, a plan, a canal: Panama'))  # True
print(is_palindrome('race a car'))                       # False
print(is_palindrome('Was it a car or a cat I saw?'))     # True

اكتشاف المتناظر: مؤشّران

للحصول على مساحة إضافية مقدارها O(1)، تحقّق من تناظر السلسلة باستخدام مؤشرين بدلًا من إنشاء شريحة. ضع left عند 0 وright في النهاية. تجاوز المحارف غير الأبجدية الرقمية، وقارن المحارف المتبقية مع تجاهل حالة الأحرف، وأعد False عند عدم التطابق. هذا الأسلوب أطول، لكنه يتجنّب إنشاء السلسلة المنقّاة بالكامل، وهو أمر مهم عندما تكون الذاكرة محدودة.

def is_palindrome_twoptr(s):
    left, right = 0, len(s) - 1
    while left < right:
        while left < right and not s[left].isalnum():
            left += 1
        while left < right and not s[right].isalnum():
            right -= 1
        if s[left].lower() != s[right].lower():
            return False
        left += 1; right -= 1
    return True

print(is_palindrome_twoptr('A man, a plan, a canal: Panama'))  # True

التوسّع حول المركز لإيجاد أطول متناظر

تجد تقنية التوسّع حول المركز أطول سلسلة فرعية متناظرة في O(n²) من الوقت مع O(1) من المساحة الإضافية. لكل محرف، بحثًا عن المتناظرات ذات الطول الفردي، ولكل فجوة بين محرفين، بحثًا عن المتناظرات ذات الطول الزوجي، توسّع نحو الخارج ما دامت المحارف متطابقة. احتفظ بأفضل زوج (start, end) تم العثور عليه. يوجد 2n-1 مركزًا، ويستغرق كل توسّع O(n) في أسوأ الحالات.

def longest_palindrome(s):
    best_start = best_end = 0

    def expand(left, right):
        while left >= 0 and right < len(s) and s[left] == s[right]:
            left -= 1; right += 1
        return left + 1, right - 1  # last valid bounds

    for i in range(len(s)):
        l, r = expand(i, i)      # odd-length
        if r - l > best_end - best_start:
            best_start, best_end = l, r
        l, r = expand(i, i + 1)  # even-length
        if r - l > best_end - best_start:
            best_start, best_end = l, r

    return s[best_start:best_end+1]

print(longest_palindrome('babad'))    # 'bab' or 'aba'
print(longest_palindrome('cbbd'))     # 'bb'

لمحة عن خوارزمية Manacher

تجد خوارزمية Manacher أطول سلسلة فرعية متناظرة في O(n) من الوقت، مستفيدةً من أن متناظرًا يقع داخل متناظر أكبر يمكن تهيئته انطلاقًا من موضعه المنعكس. نادرًا ما يُطلب تنفيذها في المقابلات، لكن من المفيد معرفة وجودها. يقبل معظم القائمين بالمقابلات أسلوب التوسّع حول المركز ذي التعقيد O(n²) باعتباره «أمثل بما يكفي»؛ لذا اذكر خوارزمية Manacher كحل نظري بتعقيد O(n) إذا طُلب منك التوسّع في الإجابة.

# Manacher's: O(n) longest palindromic substring
def manacher(s):
    # Transform s into '#a#b#a#' to handle even/odd uniformly
    t = '#' + '#'.join(s) + '#'
    n = len(t)
    P = [0] * n  # P[i] = palindrome radius at i
    center = right = 0
    for i in range(n):
        mirror = 2 * center - i
        if i < right:
            P[i] = min(right - i, P[mirror])
        while (i + P[i] + 1 < n and i - P[i] - 1 >= 0
               and t[i+P[i]+1] == t[i-P[i]-1]):
            P[i] += 1
        if i + P[i] > right:
            center, right = i, i + P[i]
    max_len = max(P)
    center_idx = P.index(max_len)
    start = (center_idx - max_len) // 2
    return s[start:start+max_len]

print(manacher('babad'))   # 'bab'

ترميز طول التتابع

يعمل ترميز طول التتابع (RLE) على ضغط المحارف المتكررة المتجاورة: تصبح 'aaabbc' على شكل 'a3b2c1'. لتنفيذه، امسح السلسلة باستخدام مؤشّر سريع للعثور على نهاية كل تتابع، واكتب المحرف وعدده في قائمة ناتج، ثم اضمم العناصر. قد يكون الإدخال أقصر من الناتج المرمّز في حالة التتابعات القصيرة؛ لذا تحقّق دائمًا من أن النسخة المرمّزة أقصر قبل إعادتها.

def encode_rle(s):
    if not s: return ''
    parts = []
    i = 0
    while i < len(s):
        char = s[i]
        j = i
        while j < len(s) and s[j] == char:
            j += 1
        count = j - i
        parts.append(char + (str(count) if count > 1 else ''))
        i = j
    encoded = ''.join(parts)
    return encoded if len(encoded) < len(s) else s

print(encode_rle('aaabbc'))    # 'a3b2c'
print(encode_rle('abc'))       # 'abc'  (no compression gain)

فك ترميز السلاسل المرمّزة بترميز طول التتابع

يقرأ فك ترميز RLE المحارف وتسلسلات الأرقام التي تليها، ثم يوسّع كل تتابع. يقدّم القائمون بالمقابلات أحيانًا صيغة LeetCode التي تستخدم k[encoded_string] لتكرار السلاسل الفرعية، مثل: 3[ab] → ababab. تتطلب هذه الصيغة المتداخلة مكدّسًا للتعامل مع مستويات التداخل المتعددة.

def decode_rle(s):
    result = []
    i = 0
    while i < len(s):
        char = s[i]; i += 1
        num_str = ''
        while i < len(s) and s[i].isdigit():
            num_str += s[i]; i += 1
        count = int(num_str) if num_str else 1
        result.append(char * count)
    return ''.join(result)

print(decode_rle('a3b2c'))    # 'aaabbc'
print(decode_rle('a2b3c1'))   # 'aabbbc'

# Nested bracket decode (LeetCode 394)
def decode_bracket(s):
    stack = []
    for c in s:
        if c != ']':
            stack.append(c)
        else:
            chars = []
            while stack[-1] != '[':
                chars.append(stack.pop())
            stack.pop()  # remove '['
            k = int(stack.pop())
            stack.append(''.join(reversed(chars)) * k)
    return ''.join(stack)
print(decode_bracket('3[ab]'))  # 'ababab'

المتناظر الصالح II: السماح بحذف واحد

بالنظر إلى سلسلة نصية، أعد True إذا كان بإمكانك جعلها متناظرة بحذف محرف واحد على الأكثر. استخدم مؤشرين؛ وعند أول عدم تطابق، تحقّق مما إذا كانت s[left+1:right+1] أو s[left:right] متناظرة، أي جرّب تجاوز كل محرف من المحرفين غير المتطابقين. إذا كان أحد الجانبين متناظرًا، فأعد True. ينجح هذا الأسلوب الجشع لأن تجاوز المحرف غير المتطابق هو الإجراء المفيد الوحيد.

def valid_palindrome(s):
    def is_pal(l, r):
        while l < r:
            if s[l] != s[r]: return False
            l += 1; r -= 1
        return True

    left, right = 0, len(s) - 1
    while left < right:
        if s[left] != s[right]:
            # Try skipping either character
            return is_pal(left+1, right) or is_pal(left, right-1)
        left += 1; right -= 1
    return True

print(valid_palindrome('aba'))    # True
print(valid_palindrome('abca'))   # True  (delete 'c')
print(valid_palindrome('abc'))    # False

تقسيم السلسلة إلى متناظرات I

قسّم السلسلة إلى جميع السلاسل الفرعية التي تكون متناظرة. استخدم التراجع: في كل خطوة، جرّب جميع البادئات للسلسلة المتبقية؛ فإذا كانت البادئة متناظرة، فاستدعِ التراجع على الجزء المتبقي. احسب مسبقًا جدولًا منطقيًا ثنائي الأبعاد is_pal[i][j] باستخدام البرمجة الديناميكية على الفواصل لجعل عمليات التحقق من التناظر في O(1)، وبذلك ينخفض التعقيد الإجمالي للتراجع من O(n² × 2^n) إلى O(n × 2^n)، وهو مقبول لأن توليد جميع التقسيمات أُسّي بطبيعته.

def partition(s):
    n = len(s)
    dp = [[False]*n for _ in range(n)]
    for i in range(n):
        dp[i][i] = True
    for length in range(2, n+1):
        for i in range(n-length+1):
            j = i + length - 1
            if s[i] == s[j]:
                dp[i][j] = length == 2 or dp[i+1][j-1]

    result = []
    def backtrack(start, path):
        if start == n: result.append(path[:]); return
        for end in range(start, n):
            if dp[start][end]:
                path.append(s[start:end+1])
                backtrack(end+1, path)
                path.pop()
    backtrack(0, [])
    return result

print(partition('aab'))  # [['a','a','b'],['aa','b']]

أقصر متناظر: تجزئة السلسلة

أوجد أقصر متناظر يمكن الحصول عليه بإضافة محارف إلى مقدمة سلسلة نصية. الفكرة الأساسية هي العثور على أطول بادئة متناظرة في s، ثم إضافة معكوس اللاحقة المتبقية إلى المقدمة. وللعثور بكفاءة على أطول بادئة متناظرة، استخدم دالة الفشل الخاصة بـ KMP على السلسلة s + '#' + reverse(s). تعطي القيمة الأخيرة لدالة الفشل طول أطول بادئة متناظرة.

def shortest_palindrome(s):
    rev = s[::-1]
    combined = s + '#' + rev  # '#' prevents overlap
    n = len(combined)
    kmp = [0] * n
    j = 0
    for i in range(1, n):
        while j > 0 and combined[i] != combined[j]:
            j = kmp[j-1]
        if combined[i] == combined[j]:
            j += 1
        kmp[i] = j
    # kmp[-1] = length of longest palindromic prefix
    to_add = rev[:len(s) - kmp[-1]]
    return to_add + s

print(shortest_palindrome('aacecaaa'))  # 'aaacecaaa'
print(shortest_palindrome('abcd'))      # 'dcbabcd'

تحقّق سريع

اختبر مدى فهمك لمفاهيم Data Structures & Algorithms — Coding Interview Prep من هذا الدرس.

مراجعة الدرس

تعلّمت في هذا الدرس أن: اكتشاف التناظر باستخدام مؤشرين يستغرق O(n) من الوقت وO(1) من المساحة، لذا احرص دائمًا على تفضيل عمليات التحقق المعتمدة على الفهارس على إنشاء نسخة معكوسة عندما تكون المساحة مهمة، وأن التوسّع حول المركز يجد أطول سلسلة فرعية متناظرة في O(n²) من خلال اعتبار كل موضع من المواضع البالغ عددها 2n-1 مركزًا محتملًا للتناظر، وأن ترميز طول التتابع يضغط التتابعات المتجاورة في O(n)، بينما يتطلب فك الترميز مكدّسًا لصيغة الأقواس المتداخلة. بعد ذلك سنستكشف فرز الفقاعات وفرز الإدراج.

الأسئلة الشائعة

هل درس «ترميز السلاسل النصية وعكسها ومتلازمات التناظر» مجاني؟

نعم — نص درس «ترميز السلاسل النصية وعكسها ومتلازمات التناظر» كامل متاح مجاناً هنا على الويب. لتمرينه بشكل تفاعلي (محرر أكواد مدمج ومدرس ذكاء اصطناعي متاح 24/7) وفتح باقي دورة Coding Interview Prep، انتقل إلى CoddyKit PRO. تتضمن دورة Coding Interview Prep 4 دروس في المجموع.

ماذا ستتعلم في «ترميز السلاسل النصية وعكسها ومتلازمات التناظر»؟

نفّذ عكس الكلمات في مكانها وترميز طول التتابع واكتشاف متلازمات التناظر، بما في ذلك تقنية التوسّع حول المركز تتمرن على Coding Interview Prep مع أكواد عملية تشغلها مباشرة في المتصفح، ومدرس ذكاء اصطناعي متاح 24/7 يجيب على أسئلتك أثناء عملك.

هل أحتاج إلى خبرة سابقة لأبدأ Coding Interview Prep؟

لا تُشترط خبرة سابقة. Coding Interview Prep على CoddyKit منظم للمبتدئين حتى المتقدمين، لذا يمكنك البدء من هنا أو من البداية والتقدم بسرعتك الخاصة. هذا هو الدرس 4 من أصل 4.

كم من الوقت يستغرق درس «ترميز السلاسل النصية وعكسها ومتلازمات التناظر»؟

معظم دروس CoddyKit تستغرق حوالي 5–10 دقائق. كل منها موجز وتفاعلي، لذا تحرز تقدماً مستمراً وتستأنف من حيث توقفت عبر الويب والتطبيق.

هل يمكنني كتابة وتشغيل أكواد في درس Coding Interview Prep هذا؟

نعم. كل درس في Coding Interview Prep يتضمن محرر أكواد مدمج، لذا تكتب وتشغل أكواداً حقيقية مباشرة في متصفحك وتحصل على تعليقات فورية من الذكاء الاصطناعي — بدون إعداد محلي.

جميع الدروس في هذه الدورة

  1. واجهة Python البرمجية للسلاسل النصية في المقابلات
  2. النافذة المنزلقة للسلاسل الجزئية
  3. التباديل الخرسانية وخرائط تكرار المحارف
  4. ترميز السلاسل النصية وعكسها ومتلازمات التناظر
← العودة إلى Coding Interview Prep