Edit distance (Levenshtein)

A table where dp[i][j] is the cost of turning the first i characters of one string into the first j of the other. Each cell is either the diagonal unchanged (characters match) or one more than the cheapest of its three neighbours. O(m·n) time and space, reducible to O(min(m, n)).

Overview

The problem

Find the minimum number of single-character edits — insert, delete or substitute — to turn one string into another. This is the Levenshtein distance.

Word 1Word 2DistanceEdits
"horse""ros"3Substitute h→r, delete r, delete e
"intention""execution"5—
"abc""abc"0None
"""abc"3Three inserts

The brute force tries every sequence of edits, which is exponential. The standard solution is dynamic programming in O(nm).

StringsCoding problemHard

Step through it

What to watch

  • The edges are free to fill: i deletions to reach the empty string.
  • A match copies the diagonal — no cost added at all.
  • Each neighbour corresponds to one specific edit.

Say this out loud

"Classic DP. dp[i][j] is the cost for the two prefixes. If the characters match it's the diagonal; otherwise it's 1 plus the min of diagonal, left and up - substitute, insert, delete. O(m·n), and you only need two rows so space can be O(min(m,n))."

Edit distance (Levenshtein)

What is the minimum number of insertions, deletions and substitutions to turn one string into another?

Run it

The full table printed for a small pair so the shape is visible, the two-row version checked against it, and the zeroed-base-case bug producing a confidently wrong answer.

1Python
Output
2Python
Output
3Python
Output

The recurrence

Let dp[i][j] be the distance between the first i characters of word1 and the first j of word2.

if word1[i−1] == word2[j−1]:  dp[i][j] = dp[i−1][j−1]

else:  dp[i][j] = 1 + min(dp[i−1][j], dp[i][j−1], dp[i−1][j−1])

The three options in the min are the three edits, and knowing which is which is what makes the recurrence memorable:

TermEditMeaning
dp[i-1][j]DeleteDrop word1[i-1]
dp[i][j-1]InsertAdd word2[j-1]
dp[i-1][j-1]SubstituteReplace one with the other

If the characters already match, no edit is needed and the cost carries over diagonally.

Base cases: dp[i][0] = i (delete everything) and dp[0][j] = j (insert everything). Getting those wrong is the most common source of off-by-one errors.

4Python
Output

O(nm) time and O(nm) space.

The table, filled in

"horse" to "ros":

 ∅ros
∅0123
h1123
o2212
r3222
s4332
e5443

The answer is the bottom-right cell: 3.

Reading the table is instructive. The diagonal 1 at (o, o) is where a match let the cost carry over. Filling in a small table by hand during an interview is worth doing — it catches base-case and index errors immediately, and it demonstrates the recurrence rather than asserting it.

What each neighbour means

The three options are not arbitrary; each is one edit:

Diagonal (dp[i-1][j-1]) — substitute one character for the other. Free when they already match.

Up (dp[i-1][j]) — delete a character from the first string.

Left (dp[i][j-1]) — insert a character into the first string.

Being able to say which is which is what separates understanding the recurrence from having memorised it.

The base cases

Row 0 and column 0 are the edges: turning a prefix of length i into the empty string costs i deletions, and building a prefix of length j from nothing costs j insertions. Filling them with zeros instead is the most common mistake, and it produces answers that are too small.

Cutting the space

Each cell depends only on the row above and the cell to its left, so the whole table is never needed at once — two rows suffice, and iterating over the shorter string makes it O(min(m, n)).

The trade is that you can no longer walk the table backwards to recover the actual sequence of edits. If the question asks which edits, keep the full table; if it asks only for the count, the two-row version is strictly better.

Reducing space to O(m)

Each row depends only on the row above it, so the full table is unnecessary:

5Python
Output

O(min(n, m)) space if you also swap the arguments so the shorter string drives the row width — worth mentioning as the final refinement.

The three lookups map cleanly: prev[j] is above (delete), curr[j-1] is left (insert), prev[j-1] is diagonal (substitute). Keeping that mapping straight is what makes the compressed version writable without re-deriving it.

Note that this loses the ability to reconstruct the edits, which needs the full table — walk backwards from the bottom-right, choosing whichever predecessor produced the value.

The variants

VariantChange
Longest common subsequenceOnly insert and delete; no substitution
Hamming distanceSubstitutions only; strings must be equal length
Damerau-LevenshteinAdds transposition of adjacent characters
Weighted edit distanceDifferent costs per operation
One edit away?Early exit once the distance exceeds 1
Levenshtein with a bound kOnly fill a band of width 2k+1 — O(kn)

The banded version is the practically important one. If you only care whether the distance is at most k, cells far from the diagonal cannot contribute, so only a band needs filling — O(kn) instead of O(nm). That is what makes fuzzy search over large dictionaries feasible.

One edit away has a neat O(n) solution that avoids DP entirely: if the lengths differ by more than 1 the answer is no; otherwise scan once, and on the first mismatch either skip one character in the longer string or skip both, then require the remainder to match exactly.

Where it is used

  • Spell checking and autocorrect. Suggest dictionary words within a small edit distance of the typed word.
  • Fuzzy search. Elasticsearch's fuzzy queries are bounded Levenshtein.
  • diff and version control. Line-level longest common subsequence, which is the same DP family.
  • Bioinformatics. Needleman-Wunsch is edit distance with substitution costs from a scoring matrix; Smith-Waterman is the local-alignment variant.
  • Record linkage and deduplication. Matching names and addresses that differ by typos.
  • OCR post-processing, correcting recognition errors against a lexicon.

For large-scale fuzzy matching, a BK-tree or a Levenshtein automaton indexes a dictionary so that all words within distance k are retrieved without computing the distance to every entry — which is what a spell checker actually does.

What it is testing

Do you recognise it as dynamic programming? Overlapping subproblems and optimal substructure, with a two-dimensional state.

Can you define the state precisely? "The distance between the first i and the first j characters" is the sentence that makes the recurrence derivable.

Do you get the base cases right? dp[i][0] = i and dp[0][j] = j, which are the deletions and insertions needed against an empty string.

Do you know which term is which edit? Being able to say "up is delete, left is insert, diagonal is substitute" demonstrates understanding rather than recall.

Do you find the space optimisation? One row instead of the full table, and it follows from noticing the dependency pattern.

Recap in one screen

  • dp[i][j] is the distance between the first i and first j characters.
  • Matching characters carry the diagonal value; otherwise take 1 plus the minimum of up (delete), left (insert) and diagonal (substitute).
  • Base cases are dp[i][0] = i and dp[0][j] = j.
  • O(nm) time; space reduces to one row, at the cost of not being able to reconstruct the edits.
  • A banded version gives O(kn) when only distances up to k matter — which is what fuzzy search uses.

How the code works

The full table printed for a small pair so the shape is visible, the two-row version checked against it, and the zeroed-base-case bug producing a confidently wrong answer.

How the code works

  1. dp[i][0] = iThe base case people zero out by accident. Turning a prefix into the empty string costs one deletion per character, and starting from zero makes every answer too small.
  2. dp[i][j] = dp[i - 1][j - 1]A match costs nothing at all — it copies the diagonal rather than adding to it. Adding 1 here is the other common slip.
  3. min(diagonal, up, left)Substitute, delete, insert, in that order. Being able to name which neighbour is which edit is what shows you understand the recurrence rather than remember it.
  4. previous = currentThe two-row version. Each cell needs only the row above and the cell to its left, so the full table is never required — unless you want to reconstruct the edits.

Change one thing

  • Swap the argument order. The distance is symmetric, and watching the table transpose is a good check that you have the axes right.
  • Add a fourth move for transposition (swapping adjacent characters). That turns it into Damerau-Levenshtein, which is what spell checkers actually use.

Where this runs

Real CPython, compiled to WebAssembly and running on your own machine — nothing is uploaded. The first run takes a few seconds while the interpreter downloads; after that it is immediate. Need more room, or want to paste your own attempt? Use the Python compiler.

Check yourself

0 of 3

Answer without scrolling back up.

  1. In the DP table, the cell above dp[i][j] corresponds to which edit?

  2. Filling row 0 and column 0 with zeros instead of 0..n gives:

  3. The space can be reduced to O(min(m, n)) because:

Cheat sheet

Edit distance (Levenshtein)

A table where dp[i][j] is the cost of turning the first i characters of one string into the first j of the other. Each cell is either the diagonal unchanged (characters match) or one more than the cheapest of its three neighbours. O(m·n) time and space, reducible to O(min(m, n)).

INTERVIEW · vizlearn.in/interview/edit-distance.html

About the author

Ashish Jangra builds and maintains VizLearn. Every module here is written and the visualisation behind it hand-built, so the numbers in a readout come from the same code that draws the picture. Corrections are genuinely welcome and get priority over everything else — if a page states something wrong, or an animation misrepresents what the algorithm does, get in touch.