0Pricing
DSA Interview Prep · บทเรียน

ระยะห่างการแก้ไข (Levenshtein)

หาความสัมพันธ์เวียนเกิดของระยะห่างการแก้ไขสำหรับการแทรก ลบ และแทนที่ แล้วเติมตาราง DP สำหรับสตริงที่มีความยาวต่างกัน

ระยะห่างการแก้ไข (Levenshtein) เป็นบทเรียน DSA Interview Prep ฟรีบน CoddyKit นี่คือบทเรียนที่ 3 จากทั้งหมด 4 บทเรียน คุณสามารถอ่านบทเรียนทั้งหมดด้านล่างฟรี — จากนั้นลองปฏิบัติด้วยตัวคุณเองในเบราว์เซอร์พร้อมตัวแก้ไขโค้ดในตัวและติวเตอร์ AI ตลอด 24/7 บทเรียนนี้เป็นส่วนหนึ่งของเส้นทางการเรียน DSA Interview Prep และความก้าวหน้าของคุณจะซิงค์ข้ามเว็บและแอป CoddyKit คอร์ส DSA Interview Prep มีบทเรียนทั้งหมด 4 บทเรียน

ปัญหาระยะห่างการแก้ไข

ระยะห่างการแก้ไข (ระยะห่างเลเวนชไตน์, 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)

ระยะห่างการแก้ไขต้องใช้เพียงแถวปัจจุบันและแถวก่อนหน้า ใช้อาร์เรย์ 1 มิติขนาด 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 × ขนาดพจนานุกรม) แนวทางแก้ไขได้แก่ต้นไม้ BK (ต้นไม้มิติสำหรับระยะห่างการแก้ไข) การทำดัชนี n-gram และอัลกอริทึมจับคู่สตริงโดยประมาณ เช่น บิแทป การเข้าใจ 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

ระยะห่างการแก้ไขแบบมีน้ำหนัก

ในแอปพลิเคชันบางประเภท การดำเนินการแต่ละแบบมีค่าใช้จ่ายต่างกัน ตัวอย่างเช่น การสลับอักขระที่อยู่ติดกัน (ข้อผิดพลาดในการพิมพ์ที่พบบ่อย) อาจมีค่าใช้จ่ายน้อยกว่าการแทนที่ทั้งตัว ระยะห่างดาเมอเรา–เลเวนชไตน์เพิ่มการสลับเป็นการดำเนินการแบบที่สี่ 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 อัลกอริทึมนีดเดิลแมน–วุนช์เป็น DP สำหรับการจัดเรียงแบบทั่วโลกที่มีความเกี่ยวข้องอย่างใกล้ชิดกับ LCS และระยะห่างการแก้ไข โดยการตรงกันให้ค่า +1 การไม่ตรงกันให้ค่า -1 และช่องว่าง (การแทรก/ลบ) ให้ค่าปรับ อัลกอริทึมสมิธ–วอเตอร์แมนเป็นรูปแบบสำหรับการจัดเรียงเฉพาะที่ (ค้นหาสตริงย่อยที่ตรงกันดีที่สุด) ทั้งสองเป็นอัลกอริทึม 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' (แทนที่ 1 ครั้ง) เพื่อยืนยันความถูกต้อง ขอบเขตความซับซ้อนมาตรฐานคือเวลา 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

ตรวจสอบความเข้าใจ

ทดสอบความเข้าใจแนวคิดโครงสร้างข้อมูลและอัลกอริทึม — การเตรียมตัวสัมภาษณ์การเขียนโปรแกรมจากบทเรียนนี้

สรุปบทเรียน

ในบทเรียนนี้คุณได้เรียนรู้ว่า: ระยะห่างการแก้ไข 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) ใช้อาร์เรย์ 1 มิติแบบเลื่อนร่วมกับตัวแปรแนวทแยง บทถัดไปเราจะใช้เทคนิคอาร์เรย์เลื่อนแบบเดียวกันเพื่อลดตาราง DP 2 มิติจากพื้นที่ O(mn) เหลือ O(min(m,n))

คำถามที่พบบ่อย

บทเรียน “ระยะห่างการแก้ไข (Levenshtein)” ฟรีหรือไม่

ใช่ — ข้อความเต็มของ “ระยะห่างการแก้ไข (Levenshtein)” ฟรีให้อ่านที่นี่บนเว็บ เพื่อปฏิบัติแบบโต้ตอบ (ตัวแก้ไขโค้ดในตัวและติวเตอร์ AI ตลอด 24/7) และปลดล็อคส่วนที่เหลือของคอร์ส DSA Interview Prep ให้อัปเกรดเป็น CoddyKit PRO คอร์ส DSA Interview Prep มีบทเรียนทั้งหมด 4 บทเรียน

คุณจะเรียนรู้อะไรในบทเรียน “ระยะห่างการแก้ไข (Levenshtein)”

หาความสัมพันธ์เวียนเกิดของระยะห่างการแก้ไขสำหรับการแทรก ลบ และแทนที่ แล้วเติมตาราง DP สำหรับสตริงที่มีความยาวต่างกัน คุณปฏิบัติ DSA Interview Prep ด้วยโค้ดที่ใช้งานได้จริงที่คุณเรียกใช้โดยตรงในเบราว์เซอร์ และติวเตอร์ AI ตลอด 24/7 ตอบคำถามของคุณขณะที่คุณไปผ่านบทเรียน

คุณต้องมีประสบการณ์ก่อนที่จะเริ่มเรียน DSA Interview Prep หรือไม่

ไม่จำเป็นต้องมีประสบการณ์มาก่อน DSA Interview Prep บน CoddyKit ออกแบบมาสำหรับผู้เริ่มต้นไปจนถึงผู้เรียนขั้นสูง คุณสามารถเริ่มต้นที่นี่หรือเริ่มจากตัวแรกและเรียนด้วยความเร็วของคุณเอง นี่คือบทเรียนที่ 3 จากทั้งหมด 4 บทเรียน

บทเรียน “ระยะห่างการแก้ไข (Levenshtein)” ใช้เวลานานแค่ไหน

บทเรียน CoddyKit ส่วนใหญ่ใช้เวลาประมาณ 5–10 นาที แต่ละบทเรียนจึงสั้นและเป็นแบบโต้ตอบ คุณสามารถก้าวหน้าอย่างต่อเนื่องและกลับมาเรียนต่อจากตรงที่เพิ่งหยุดบนเว็บและแอปได้เลย

ฉันเขียนและรันโค้ดในบทเรียน DSA Interview Prep นี้ได้ไหม

ได้ บทเรียน DSA Interview Prep ทุกบทมีตัวแก้ไขโค้ดในตัว คุณจึงเขียนและรันโค้ดจริงได้เลยในเบราว์เซอร์ และได้รับข้อเสนอแนะจาก AI ในทันที — ไม่ต้องติดตั้งในเครื่องของคุณ

บทเรียนทั้งหมดในหลักสูตรนี้

  1. เส้นทางไม่ซ้ำและผลรวมเส้นทางต่ำสุดบนกริด
  2. ลำดับร่วมที่ยาวที่สุด
  3. ระยะห่างการแก้ไข (Levenshtein)
  4. การเพิ่มประสิทธิภาพพื้นที่สำหรับ DP สองมิติ
← กลับไปที่ DSA Interview Prep