Count the Minimum Insertions, Deletions, and Substitutions to Transform One String Into Another
Edit Distance (Levenshtein)
Edit distance measures how different two strings are by counting the minimum number of single-character insertions, deletions, and substitutions needed to transform one into the other.
What you'll learn
- Write the Levenshtein recurrence for insert, delete, and substitute operations
- Fill the O(mn) DP table and read off the edit distance
- Apply the row-rolling trick to reduce space from O(mn) to O(n)
Edit distance (Levenshtein distance) between two strings s and t is the minimum number of single-character edits — insertions, deletions, or substitutions — needed to transform s into t. It powers spell checkers, fuzzy search, and DNA alignment.
State and Recurrence
State: dp[i][j] = edit distance between s[0..i-1] and t[0..j-1].
Recurrence:
- If
s[i-1] === t[j-1]:dp[i][j] = dp[i-1][j-1](no edit needed) - Otherwise:
dp[i][j] = 1 + min(dp[i-1][j]— delete from s (or insert into t)dp[i][j-1]— insert into s (or delete from t)dp[i-1][j-1]— substitute s[i-1] with t[j-1])
Base cases:
dp[0][j] = j— transforming empty string tot[0..j-1]requires j insertionsdp[i][0] = i— transformings[0..i-1]to empty string requires i deletions
function editDistance(s, t) {
const m = s.length, n = t.length;
const dp = Array.from({ length: m + 1 }, (_, i) =>
Array.from({ length: n + 1 }, (_, j) => (i === 0 ? j : j === 0 ? i : 0))
);
for (let i = 1; i <= m; i++) {
for (let j = 1; j <= n; j++) {
if (s[i - 1] === t[j - 1]) {
dp[i][j] = dp[i - 1][j - 1];
} else {
dp[i][j] = 1 + Math.min(
dp[i - 1][j], // delete
dp[i][j - 1], // insert
dp[i - 1][j - 1] // substitute
);
}
}
}
return dp[m][n];
}
console.log(editDistance('kitten', 'sitting')); // 3
// kitten → sitten (substitute k→s)
// sitten → sittin (substitute e→i)
// sittin → sitting (insert g) Space-Optimized Version
Row i depends only on row i-1, so a rolling pair of arrays reduces space from O(mn) to O(n):
function editDistanceOpt(s, t) {
const m = s.length, n = t.length;
let prev = Array.from({ length: n + 1 }, (_, j) => j);
for (let i = 1; i <= m; i++) {
const curr = new Array(n + 1);
curr[0] = i;
for (let j = 1; j <= n; j++) {
if (s[i - 1] === t[j - 1]) {
curr[j] = prev[j - 1];
} else {
curr[j] = 1 + Math.min(prev[j], curr[j - 1], prev[j - 1]);
}
}
prev = curr;
}
return prev[n];
}
console.log(editDistanceOpt('sunday', 'saturday')); // 3 Reading the Table
Each cell represents a subproblem. The three neighbors of dp[i][j] are:
| Neighbor | Operation |
|---|---|
| dp[i-1][j-1] | Match or substitute character i with character j |
| dp[i-1][j] | Delete character i from s |
| dp[i][j-1] | Insert character j into s |
Backtracking from dp[m][n] to dp[0][0] reveals the sequence of
operations, similar to LCS reconstruction.
Complexity
Time O(mn), space O(mn) with full table, O(n) with rolling rows.
Up Next
Coin change applies 1-D DP to find the fewest coins for a target amount and shows why greedy fails for arbitrary coin systems.
Coin Change →