Edit Distance
Table of Contents + β
Edit Distance is the question behind spell-check and autocorrect. When your phone changes βwrodβ to βwordβ, something is measuring how far apart two words are. This question asks you to find that distance. It looks scary at first. But it becomes calm and clear once you see it as a small grid you fill in one cell at a time.
π― The Problem
You get two words. Change the first word into the second word using the fewest edits.
The rules:
- You can insert a letter.
- You can delete a letter.
- You can replace one letter with another.
- Each edit counts as one step. Find the smallest number of steps.
For "horse" and "ros", the answer is 3. You replace h with r. Then you delete o. Then you delete e. Three edits and "horse" becomes "ros".
Input: word1 = "horse", word2 = "ros"Output: 3
Explanation: horse -> rorse (replace 'h' with 'r') rorse -> rose (delete 'r') rose -> ros (delete 'e')This smallest number of edits has a name. It is called the Levenshtein distance. That is just a plain label for how many single edits turn one string into the other.
Here is a picture of the question. We line up one word along the top and the other word down the side. Every path from the start to the end is one way of editing.
π’ Approach 1: Plain Recursion (Brute Force)
The idea in one line: look at the last letters, and on a mismatch try insert, delete, and replace.
The idea:
- Think from the end of both words.
- If the last letters are the same, they cost nothing. Keep them and solve the smaller pair.
- If the last letters differ, you must do one edit.
How it works:
- On a mismatch, try inserting, deleting, and replacing.
- Each choice leaves a slightly smaller pair of words.
- Solve each smaller pair the same way. Take the cheapest of the three plus one.
Why it is weak:
- The same small pair of words gets solved again and again from different paths.
- Each step branches into three more.
- The blow-up is roughly O(3^n). Unusable for long words.
Here is the plain recursion code:
def min_distance(word1, word2): def dfs(i, j): if i == len(word1): return len(word2) - j if j == len(word2): return len(word1) - i if word1[i] == word2[j]: return dfs(i + 1, j + 1) return 1 + min(dfs(i + 1, j), dfs(i, j + 1), dfs(i + 1, j + 1))
return dfs(0, 0)π§ Approach 2: Add Memoization (Better)
The idea in one line: remember the answer for each leftover pair so you solve it once.
The idea:
- The waste comes from solving the same small pair many times.
- The key is βhow much of word1 is leftβ and βhow much of word2 is leftβ.
How it works:
- The first time you solve a pair, store the answer.
- The next time the same pair shows up, read it straight from the table.
Why it is fast:
- Every distinct pair is solved only once.
- There are about m times n pairs, so the time drops to O(m Γ n). This is memoization.
Here is the memoized recursion:
from functools import lru_cache
def min_distance(word1, word2): @lru_cache(None) def dfs(i, j): if i == len(word1): return len(word2) - j if j == len(word2): return len(word1) - i if word1[i] == word2[j]: return dfs(i + 1, j + 1) return 1 + min(dfs(i + 1, j), dfs(i, j + 1), dfs(i + 1, j + 1))
return dfs(0, 0)π Approach 3: Bottom-Up 2D DP Table (Best)
The idea in one line: fill a grid from the empty-string corner, one cell at a time, no recursion.
The idea:
- Make a grid
dpwith one extra row and one extra column. dp[i][j]is the edit distance between the firstiletters of word1 and the firstjletters of word2.
The base cases:
- Turning a word into an empty string means deleting every letter. So
dp[i][0] = i. - Building a word from nothing means inserting every letter. So
dp[0][j] = j.
How each inner cell fills:
- If letter
iof word1 matches letterjof word2, no edit is needed. Copy the diagonaldp[i-1][j-1]. - If they differ, take one plus the smallest of three neighbors.
- The cell above
dp[i-1][j]is a delete. The leftdp[i][j-1]is an insert. The diagonaldp[i-1][j-1]is a replace.
Why it is best:
- Each cell is filled once, so the time is O(m Γ n).
- It uses simple loops with no recursion stack. This is tabulation.
Here is the filled table for "horse" and "ros". The bottom-right cell holds the final answer, which is 3.
Steps to Solve
- Let
mbe the length of word1 andnbe the length of word2. - Make a grid
dpwithm + 1rows andn + 1columns. - Fill the first column so
dp[i][0] = i, because that many deletes empties the word. - Fill the first row so
dp[0][j] = j, because that many inserts builds the word. - For each cell, if the two letters match, copy the diagonal
dp[i-1][j-1]. - If they do not match, take
1 + min(dp[i-1][j], dp[i][j-1], dp[i-1][j-1]). - The answer sits in the bottom-right cell
dp[m][n].
This Python version builds the table with a list of lists and uses the built-in min.
def edit_distance(w1, w2): m, n = len(w1), len(w2) dp = [[0] * (n + 1) for _ in range(m + 1)] # (m+1) x (n+1) grid
for i in range(m + 1): dp[i][0] = i # delete every letter of word1 for j in range(n + 1): dp[0][j] = j # insert every letter of word2
for i in range(1, m + 1): for j in range(1, n + 1): if w1[i - 1] == w2[j - 1]: # letters match, no edit dp[i][j] = dp[i - 1][j - 1] else: # cheapest of three + 1 dp[i][j] = 1 + min( dp[i - 1][j], # delete dp[i][j - 1], # insert dp[i - 1][j - 1], # replace ) return dp[m][n]
word1 = "horse"word2 = "ros"print(edit_distance(word1, word2))The output of the above code will be:
3Let us walk through the Python version line by line. Code first, then the why.
m, n = len(w1), len(w2)dp = [[0] * (n + 1) for _ in range(m + 1)]We grab both lengths. Then we build a grid with one extra row and one extra column. That extra row and column hold the empty-string cases. Without them the matching rule below would fall off the edge of the table.
for i in range(m + 1): dp[i][0] = ifor j in range(n + 1): dp[0][j] = jThese two loops fill the borders. Column zero means word2 is empty. To reach an empty word from i letters you delete i times. Row zero means word1 is empty. To build j letters from nothing you insert j times. These known values give the rest of the table something to build on.
if w1[i - 1] == w2[j - 1]: dp[i][j] = dp[i - 1][j - 1]We use i - 1 and j - 1 because the table is shifted by one. Cell dp[i][j] is about the first i and first j letters, so the actual letters sit at index i - 1 and j - 1. When they match, this position adds no cost. We just copy the diagonal, which is the answer for both prefixes without these last two letters.
dp[i][j] = 1 + min( dp[i - 1][j], dp[i][j - 1], dp[i - 1][j - 1],)When the letters differ we must spend one edit. The cell above is the delete path. The cell to the left is the insert path. The diagonal is the replace path. We take the smallest of those three already-solved answers and add one for the edit we do now. That single min is the whole heart of the solution.
return dp[m][n]The bottom-right cell looked at all of word1 against all of word2. So it holds the final smallest edit count, which is 3 here.
β±οΈ Time and Space Complexity
Plain recursion branches three ways at every step, so it is exponential and unusable for long words. Memoization solves each distinct pair once, which brings it down to O(m Γ n). The bottom-up table does the same amount of work with simple loops and no recursion stack. The table itself needs O(m Γ n) memory. You can shrink that to O(n) by keeping only the previous row, but the full table is easier to explain in an interview.
| Approach | Time Complexity | Space Complexity |
|---|---|---|
| Plain recursion | O(3^n) | O(m + n) |
| Top-down memoization | O(m Γ n) | O(m Γ n) |
| Bottom-up 2D table | O(m Γ n) | O(m Γ n) |
Tip
In an interview, draw the small grid for two short words and fill a few cells by hand. Showing the table makes the insert, delete, and replace rule obvious. The interviewer wants to see that you understand each cell, not just that you memorized the formula.
π§© Key Takeaways
- β Edit Distance counts the fewest inserts, deletes, and replaces to turn one word into another.
- β
The cell
dp[i][j]is the distance between the firstiand firstjletters. - β When letters match, copy the diagonal. When they differ, take one plus the smallest of three neighbors.
- β The first row and first column are the empty-string base cases.
- β Recursion is exponential, but the 2D table runs in O(m Γ n) time.
Check Your Knowledge
Test what you learned. Pick an answer for each question, then click Check.
- 1
Which three operations are allowed in the Edit Distance problem?
Why: Each edit is one insert, one delete, or one replace, and each counts as a single step.
- 2
What does the cell dp[i][j] represent?
Why: dp[i][j] holds the smallest edit distance between the two prefixes of length i and j.
- 3
When the current letters match, what does the table do?
Why: Matching letters need no edit, so the cell just copies the diagonal answer for the smaller prefixes.
- 4
What is the time complexity of the bottom-up table solution?
Why: Filling each of the m Γ n cells once gives O(m Γ n) time, far better than the exponential recursion.