0Pricing
DSA Interview Prep · درس

مسافة التحرير (Levenshtein)

اشتق علاقة تكرار مسافة التحرير لعمليات الإدراج والحذف والاستبدال، واملأ جدول DP لأزواج من السلاسل ذات الأطوال المختلفة

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

مسألة مسافة التحرير

تطرح مسافة التحرير (مسافة Levenshtein، LeetCode 72) السؤال الآتي: ما الحد الأدنى لعدد عمليات الإدراج أو الحذف أو الاستبدال اللازمة لتحويل سلسلة إلى أخرى؟ على سبيل المثال، لتحويل 'horse' إلى 'ros': استبدل 'h'→'r' (horse→rorse)، واحذف 'r' (rorse→rose)، واحذف 'e' (rose→ros) — أي 3 عمليات. وتُعد مسافة التحرير أساسية في المدققات الإملائية ومحاذاة DNA والمطابقة التقريبية.

# Allowed operations:
# Insert: 'abc' → 'abXc' (insert X)
# Delete: 'abc' → 'ac' (delete b)
# Replace: 'abc' → 'aXc' (replace b with X)

# horse → ros: 3 operations
# 1. horse → rorse (replace h with r)
# 2. rorse → rose  (delete r at index 1)
# 3. rose  → ros   (delete e)
print('Edit distance horse→ros: 3')
print('Edit distance intention→execution: 5')

حالة DP والعلاقة العودية

عرّف dp[i][j] بأنها الحد الأدنى لمسافة التحرير بين word1[:i] وword2[:j]. إذا كان word1[i-1] == word2[j-1]، فلا حاجة إلى أي عملية: dp[i][j] = dp[i-1][j-1]. وإلا، فخذ الحد الأدنى للعمليات الثلاث: الإدراج dp[i][j-1] + 1، والحذف dp[i-1][j] + 1، والاستبدال dp[i-1][j-1] + 1. حالات الأساس: dp[i][0] = i (حذف كل أحرف word1) وdp[0][j] = j (إدراج كل أحرف word2).

def edit_distance(word1, word2):
    m, n = len(word1), len(word2)
    dp = [[0]*(n+1) for _ in range(m+1)]
    # Base cases
    for i in range(m+1): dp[i][0] = i  # delete all of word1
    for j in range(n+1): dp[0][j] = j  # insert all of word2
    for i in range(1, m+1):
        for j in range(1, n+1):
            if word1[i-1] == word2[j-1]:
                dp[i][j] = dp[i-1][j-1]  # no cost
            else:
                dp[i][j] = 1 + min(
                    dp[i][j-1],    # insert
                    dp[i-1][j],    # delete
                    dp[i-1][j-1]   # replace
                )
    return dp[m][n]

print(edit_distance('horse', 'ros'))          # 3
print(edit_distance('intention', 'execution')) # 5

فهم العمليات الثلاث

تتوافق العمليات الثلاث مباشرة مع الانتقالات في جدول DP: الاستبدال dp[i-1][j-1]+1 — طابقنا الحرفين، لكننا دفعنا تكلفة واحدة. الحذف من word1 dp[i-1][j]+1 — أزل حرفًا من word1 (انتقل إلى الأعلى في الجدول). الإدراج في word1 dp[i][j-1]+1 — أدرج حرفًا لمطابقة word2 (انتقل إلى اليسار). ويعطي الحد الأدنى للقيم الثلاث مسار التحرير الأمثل.

# Visualise the DP table for 'cat' → 'cut'
# dp[i][j] = min edits for word1[:i] vs word2[:j]

word1, word2 = 'cat', 'cut'
m, n = len(word1), len(word2)
dp = [[0]*(n+1) for _ in range(m+1)]
for i in range(m+1): dp[i][0] = i
for j in range(n+1): dp[0][j] = j
for i in range(1, m+1):
    for j in range(1, n+1):
        if word1[i-1]==word2[j-1]: dp[i][j]=dp[i-1][j-1]
        else: dp[i][j]=1+min(dp[i][j-1],dp[i-1][j],dp[i-1][j-1])
print('  ', ' '.join(' '+word2))
for i, row in enumerate(dp):
    print((' ' if i==0 else word1[i-1]), row)

تحسين استخدام الذاكرة إلى O(n)

تحتاج مسافة التحرير فقط إلى الصف الحالي والصف السابق. استخدم مصفوفة أحادية الأبعاد بحجم n+1، وتتبع قيمة diagonal (dp[i-1][j-1]) بشكل منفصل قبل تحديث كل خلية. عالج الخلايا من اليسار إلى اليمين: temp = dp[j] (القيمة القديمة = dp[i-1][j])، ثم حدّث dp[j] باستخدام dp[j] (الحذف)، وdp[j-1] (الإدراج)، وdiagonal (الاستبدال).

def edit_distance_1d(word1, word2):
    m, n = len(word1), len(word2)
    dp = list(range(n + 1))  # initial row: 0,1,2,...,n
    for i in range(1, m + 1):
        diag = dp[0]       # dp[i-1][0]
        dp[0] = i          # dp[i][0] = i
        for j in range(1, n + 1):
            temp = dp[j]   # dp[i-1][j] before overwrite
            if word1[i-1] == word2[j-1]:
                dp[j] = diag
            else:
                dp[j] = 1 + min(dp[j],     # delete
                                dp[j-1],   # insert
                                diag)      # replace
            diag = temp
    return dp[n]

print(edit_distance_1d('horse', 'ros'))          # 3
print(edit_distance_1d('intention', 'execution')) # 5

إعادة بناء عمليات التحرير

لإعادة بناء تسلسل عمليات التحرير الفعلي، أجرِ التتبع العكسي عبر جدول DP بدءًا من (m, n). في كل خلية: إذا كان word1[i-1] == word2[j-1]، فانتقل قطريًا (من دون إجراء أي عملية). وإلا، فحدّد أيًّا من الجيران الثلاثة أعطى القيمة الصغرى، وسجّل العملية المقابلة. ينتج عن ذلك نص التعديلات بترتيب عكسي؛ فاعكسه للحصول على الإجابة النهائية.

def edit_ops(word1, word2):
    m, n = len(word1), len(word2)
    dp = [[0]*(n+1) for _ in range(m+1)]
    for i in range(m+1): dp[i][0]=i
    for j in range(n+1): dp[0][j]=j
    for i in range(1,m+1):
        for j in range(1,n+1):
            if word1[i-1]==word2[j-1]: dp[i][j]=dp[i-1][j-1]
            else: dp[i][j]=1+min(dp[i][j-1],dp[i-1][j],dp[i-1][j-1])
    ops, i, j = [], m, n
    while i>0 or j>0:
        if i>0 and j>0 and word1[i-1]==word2[j-1]:
            i-=1; j-=1
        elif j>0 and (i==0 or dp[i][j-1]<=dp[i-1][j] and dp[i][j-1]<=dp[i-1][j-1]):
            ops.append(f'Insert {word2[j-1]} at pos {i}'); j-=1
        elif i>0 and (j==0 or dp[i-1][j]<=dp[i][j-1] and dp[i-1][j]<=dp[i-1][j-1]):
            ops.append(f'Delete {word1[i-1]} at pos {i-1}'); i-=1
        else:
            ops.append(f'Replace {word1[i-1]} with {word2[j-1]}'); i-=1; j-=1
    return list(reversed(ops))

for op in edit_ops('horse', 'ros'): print(op)

التحقق من مسافة تحرير واحدة

مسألة أبسط في المقابلات: هل تفصل بين سلسلتين عملية تحرير واحدة تمامًا؟ يمكن حل ذلك بزمن O(n) من دون DP. حرّك مؤشري السلسلتين معًا. عند حدوث عدم تطابق، جرّب العمليات الثلاث (تخطّي حرف في s1، أو تخطّي حرف في s2، أو تخطّيهما معًا) وتحقّق مما إذا كانت الأجزاء المتبقية متطابقة. إذا حدث عدم تطابق ثانٍ، فأعد False. يتجنب هذا الأسلوب الجشع استخدام DP الكامل بزمن O(mn) عندما تحتاج فقط إلى معرفة ما إذا كانت المسافة ≤ 1.

def is_one_edit_distance(s, t):
    m, n = len(s), len(t)
    if abs(m - n) > 1: return False
    if m > n: return is_one_edit_distance(t, s)  # ensure m <= n
    for i in range(m):
        if s[i] != t[i]:
            if m == n:
                return s[i+1:] == t[i+1:]   # replace
            else:
                return s[i:] == t[i+1:]     # insert into s (delete from t)
    return m + 1 == n  # all matched, lengths differ by 1

print(is_one_edit_distance('ab', 'acb'))   # True (insert c)
print(is_one_edit_distance('ab', 'ab'))    # False (zero edits)
print(is_one_edit_distance('ab', 'abc'))   # True (append c)
print(is_one_edit_distance('ab', 'xyz'))   # False

مقارنة مسافة التحرير بـ LCS

تقدم مسافة التحرير (باستخدام العمليات الثلاث) وLCS منظورين متكاملين لتشابه السلاسل. تحسب مسافة التحرير الاختلاف، بينما يحسب LCS التشابه. عند السماح بالإدراج والحذف فقط (من دون استبدال)، تكون مسافة التحرير = m + n - 2×LCS. وعند السماح بالاستبدال، يختلف DP قليلًا: يساهم القطر بـ dp[i-1][j-1] عند التطابق (مجانًا)، أو بـ dp[i-1][j-1]+1 عند الاستبدال. تعمل الخوارزميتان بزمن O(mn).

def lcs_len(s1, s2):
    m, n = len(s1), len(s2)
    dp = [[0]*(n+1) for _ in range(m+1)]
    for i in range(1,m+1):
        for j in range(1,n+1):
            if s1[i-1]==s2[j-1]: dp[i][j]=dp[i-1][j-1]+1
            else: dp[i][j]=max(dp[i-1][j],dp[i][j-1])
    return dp[m][n]

def edit_insert_delete_only(s1, s2):
    return len(s1) + len(s2) - 2 * lcs_len(s1, s2)

print(edit_insert_delete_only('sea', 'eat'))  # 2
print(edit_distance('sea', 'eat'))            # 2 (same here: replace not needed)

المطابقة التقريبية للسلاسل

تُستخدم مسافة التحرير في المطابقة التقريبية في تطبيقات واقعية. يقترح المدقق الإملائي تصحيحات تقع على مسافة تحرير 1 أو 2 من الكلمة المكتوبة. يكمن التحدي عند التوسع في تجنب إجراء O(mn × dict_size) مقارنة. تشمل الحلول BK-trees (شجرة قياسية لمسافة التحرير)، وفهرسة n-gram، وخوارزميات مطابقة تقريبية للسلاسل مثل Bitap. يساعدك فهم DP الأساسي على تحليل كفاءة هذه الأدوات ذات المستوى الأعلى.

def spell_suggest(typed, dictionary, max_dist=2):
    '''Return words in dictionary within max_dist edits of typed.'''
    suggestions = []
    for word in dictionary:
        if abs(len(typed) - len(word)) <= max_dist:
            if edit_distance(typed, word) <= max_dist:
                suggestions.append(word)
    return suggestions

def edit_distance(w1, w2):
    dp = list(range(len(w2)+1))
    for i,c1 in enumerate(w1,1):
        prev = i
        for j,c2 in enumerate(w2,1):
            temp = dp[j]
            dp[j] = prev if c1==c2 else 1+min(dp[j],prev,dp[j-1])
            prev = temp
    return dp[len(w2)]

dictionary = ['horse', 'worse', 'house', 'morse', 'nurse']
print(spell_suggest('harse', dictionary))  # horse, worse, house, morse

مسافة التحرير الموزونة

في بعض التطبيقات، تكون للعمليات المختلفة تكاليف مختلفة. فعلى سبيل المثال، قد تكون كلفة تبديل حرفين متجاورين (وهو خطأ شائع في الكتابة) أقل من كلفة استبدال كامل. وتضيف Damerau-Levenshtein distance عملية التبديل بوصفها عملية رابعة. وتمتد خوارزمية DP لتتحقق أيضًا من dp[i-2][j-2]+1 عندما يكون word1[i-1]==word2[j-2] وword1[i-2]==word2[j-1]. ويمثل هذا أخطاء الكتابة على لوحة المفاتيح بدقة أكبر.

def damerau_levenshtein(s, t):
    m, n = len(s), len(t)
    dp = [[0]*(n+1) for _ in range(m+1)]
    for i in range(m+1): dp[i][0]=i
    for j in range(n+1): dp[0][j]=j
    for i in range(1,m+1):
        for j in range(1,n+1):
            cost = 0 if s[i-1]==t[j-1] else 1
            dp[i][j] = min(
                dp[i-1][j]+1,     # delete
                dp[i][j-1]+1,     # insert
                dp[i-1][j-1]+cost # replace
            )
            # Transposition
            if i>1 and j>1 and s[i-1]==t[j-2] and s[i-2]==t[j-1]:
                dp[i][j] = min(dp[i][j], dp[i-2][j-2]+1)
    return dp[m][n]

print(damerau_levenshtein('CA', 'ABC'))   # 2
print(damerau_levenshtein('ab', 'ba'))    # 1 (transposition)

محاذاة تسلسلات DNA

تستخدم المعلوماتية الحيوية متغيرات من مسافة التحرير في محاذاة تسلسلات DNA. وتُعد خوارزمية Needleman-Wunsch خوارزمية DP للمحاذاة العامة، وترتبط ارتباطًا وثيقًا بـ LCS ومسافة التحرير، إذ يمنح التطابق +1، ويمنح عدم التطابق -1، وتفرض الفجوة (الإدراج أو الحذف) عقوبة. أما متغير Smith-Waterman فينفذ محاذاة محلية (للعثور على السلسلة الفرعية الأفضل تطابقًا). وكلتاهما خوارزميتا DP بزمن O(mn) وتستخدمان البنية نفسها لملء الجدول.

def needleman_wunsch(seq1, seq2, match=1, mismatch=-1, gap=-1):
    m, n = len(seq1), len(seq2)
    dp = [[0]*(n+1) for _ in range(m+1)]
    for i in range(m+1): dp[i][0] = i * gap
    for j in range(n+1): dp[0][j] = j * gap
    for i in range(1,m+1):
        for j in range(1,n+1):
            score = match if seq1[i-1]==seq2[j-1] else mismatch
            dp[i][j] = max(
                dp[i-1][j-1] + score,  # align
                dp[i-1][j] + gap,      # gap in seq2
                dp[i][j-1] + gap       # gap in seq1
            )
    return dp[m][n]

print(needleman_wunsch('GATTACA', 'GCATGCU'))  # alignment score

منهجية التعامل مع مسافة التحرير في المقابلات

عند سؤالك عن مسافة التحرير في مقابلة: (1) تأكد من العمليات المسموح بها (الإدراج والحذف والاستبدال). (2) حدّد حالة DP بوضوح. (3) اكتب الحالات الثلاث والعلاقة العودية صراحةً. (4) اذكر حالات الأساس: dp[i][0]=i وdp[0][j]=j. (5) اذكر تحسين المساحة إلى O(n). (6) إذا سمح الوقت، تتبّع مثالًا صغيرًا مثل 'cat'→'cut' (استبدال واحد) للتحقق. ويُعد الزمن O(mn) والمساحة O(mn) → O(n) حدَّي التعقيد القياسيين.

# Clean interview solution
def min_distance(word1, word2):
    m, n = len(word1), len(word2)
    # O(n) space with rolling row
    dp = list(range(n + 1))
    for i in range(1, m + 1):
        diag = dp[0]   # dp[i-1][0]
        dp[0] = i
        for j in range(1, n + 1):
            temp = dp[j]
            if word1[i-1] == word2[j-1]:
                dp[j] = diag
            else:
                dp[j] = 1 + min(dp[j], dp[j-1], diag)
            diag = temp
    return dp[n]

# Time: O(mn), Space: O(n)
print(min_distance('horse', 'ros'))          # 3
print(min_distance('intention', 'execution')) # 5
print(min_distance('', 'abc'))               # 3
print(min_distance('abc', ''))               # 3

اختبار سريع

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

مراجعة الدرس

تعلمت في هذا الدرس أن: مسافة التحرير dp[i][j] = min(dp[i][j-1]+1, dp[i-1][j]+1, dp[i-1][j-1]+cost) مع cost=0 عند التطابق وإلا 1، وأن حالات الأساس dp[i][0]=i وdp[0][j]=j تمثل التحويل من سلسلة فارغة أو إليها، وأن تحسين المساحة إلى O(n) يستخدم مصفوفة أحادية الأبعاد متدحرجة مع متغير قطري. بعد ذلك سنطبق حيلة المصفوفة المتدحرجة نفسها لتقليل جداول DP ثنائية الأبعاد من مساحة O(mn) إلى O(min(m,n)).

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

هل درس «مسافة التحرير (Levenshtein)» مجاني؟

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

ماذا ستتعلم في «مسافة التحرير (Levenshtein)»؟

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

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

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

كم من الوقت يستغرق درس «مسافة التحرير (Levenshtein)»؟

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

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

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

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

  1. المسارات الفريدة وأقل مجموع مسار في الشبكات
  2. أطول تتابع مشترك
  3. مسافة التحرير (Levenshtein)
  4. تحسين المساحة في DP ثنائي الأبعاد
← العودة إلى DSA Interview Prep