Levenshtein Distance
Algorithm Customization
String Comparison
Programming
Software Development

Make Levenstein's Distance algorithm fit my needs

Master System Design with Codemia

Enhance your system design skills with over 120 practice problems, detailed solutions, and hands-on exercises.

Introduction

Classic Levenshtein distance is a good default for measuring string similarity, but real applications often need more than the textbook version. You may want case-insensitive matching, different costs for insertions and substitutions, early cutoffs, token-based comparison, or support for transposed characters. Making the algorithm fit your needs usually means changing the cost model or the preprocessing, not throwing the algorithm away.

Start with the Standard Dynamic Programming Version

The classic algorithm computes the minimum number of insertions, deletions, and substitutions needed to transform one string into another.

python
1def levenshtein(a: str, b: str) -> int:
2    dp = [[0] * (len(b) + 1) for _ in range(len(a) + 1)]
3
4    for i in range(len(a) + 1):
5        dp[i][0] = i
6    for j in range(len(b) + 1):
7        dp[0][j] = j
8
9    for i in range(1, len(a) + 1):
10        for j in range(1, len(b) + 1):
11            cost = 0 if a[i - 1] == b[j - 1] else 1
12            dp[i][j] = min(
13                dp[i - 1][j] + 1,
14                dp[i][j - 1] + 1,
15                dp[i - 1][j - 1] + cost,
16            )
17    return dp[-1][-1]
18
19print(levenshtein("kitten", "sitting"))  # 3

That gives you a correct baseline before you start customizing.

Customize the Cost Model

One of the simplest and most useful changes is weighted cost. For example, maybe substitution errors are cheaper than insertions because you are comparing human typing mistakes.

python
1def weighted_levenshtein(a: str, b: str,
2                         insert_cost=1,
3                         delete_cost=1,
4                         substitute_cost=1) -> int:
5    dp = [[0] * (len(b) + 1) for _ in range(len(a) + 1)]
6
7    for i in range(len(a) + 1):
8        dp[i][0] = i * delete_cost
9    for j in range(len(b) + 1):
10        dp[0][j] = j * insert_cost
11
12    for i in range(1, len(a) + 1):
13        for j in range(1, len(b) + 1):
14            cost = 0 if a[i - 1] == b[j - 1] else substitute_cost
15            dp[i][j] = min(
16                dp[i - 1][j] + delete_cost,
17                dp[i][j - 1] + insert_cost,
18                dp[i - 1][j - 1] + cost,
19            )
20    return dp[-1][-1]
21
22print(weighted_levenshtein("color", "colour", insert_cost=1, substitute_cost=2))

This is often enough to make the algorithm much more useful for domain-specific matching.

Preprocess the Strings First

Many matching tasks are improved more by preprocessing than by changing the algorithm itself. Common preprocessing steps include:

  • lowercasing
  • trimming whitespace
  • removing punctuation
  • Unicode normalization
  • token normalization such as expanding abbreviations

For example:

python
1import re
2
3def normalize(text: str) -> str:
4    text = text.lower().strip()
5    text = re.sub(r"[^a-z0-9 ]+", "", text)
6    return text
7
8print(levenshtein(normalize("New-York"), normalize("new york")))

Without preprocessing, many distances reflect formatting noise rather than real semantic difference.

Consider Variants Such as Damerau-Levenshtein

If transposed letters are common, such as teh versus the, standard Levenshtein can be too harsh because it counts the transposition as two edits. In that case, a Damerau-Levenshtein variant is often a better fit.

You can also add a threshold and stop early when the distance already exceeds the maximum acceptable value. That is useful in search, autocomplete, and deduplication pipelines where you do not need the exact number for obviously dissimilar strings.

Common Pitfalls

The biggest mistake is modifying the cost model before defining what "similar enough" means in the real application. The right customization depends on the error patterns you actually care about.

Another issue is using raw Levenshtein distance on strings of very different lengths and then comparing scores directly. A distance of 2 means something very different for strings of length 4 and strings of length 40. In those cases, a normalized score may be more useful.

Developers also sometimes try to use character-level Levenshtein where token-level or domain-aware matching would be more appropriate. Product names, addresses, and identifiers often need custom preprocessing or token rules before edit distance becomes meaningful.

Finally, do not ignore performance. The standard dynamic programming table is O(mn) in time and memory, so large-scale fuzzy matching may require cutoffs, indexing, or approximate search techniques beyond the raw algorithm.

Summary

  • Standard Levenshtein distance is a good baseline, but many applications need customization.
  • Weighted edit costs let you reflect domain-specific error patterns.
  • Preprocessing often improves matching quality more than algorithm changes alone.
  • Variants such as Damerau-Levenshtein are useful when transpositions matter.
  • Define your matching goal first, then customize the distance function to support that goal.

Course illustration
Course illustration

All Rights Reserved.