Skip to content

Commit ca0a498

Browse files
williamfisetclaude
andauthored
Refactor EditDistance iterative and recursive, add tests (williamfiset#1279)
EditDistanceIterative: - Add file-level header, Javadoc, null checks, cross-reference to recursive - Simplify loop by extracting base cases into explicit initialization - Replace INF sentinel with clean base case logic - Add labeled examples in main EditDistanceRecursive: - Add file-level header, Javadoc, cross-reference to iterative - Make fields private - Replace custom min() varargs with Math.min - Add inline comments explaining base cases and transitions - Remove duplicate example in main Tests: - Add 14 tests covering null input, empty strings, insertions, deletions, substitutions, insert+delete vs substitute tradeoff, single char ops, and cross-validation between iterative and recursive solvers Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
1 parent fb7b762 commit ca0a498

4 files changed

Lines changed: 268 additions & 114 deletions

File tree

Lines changed: 67 additions & 53 deletions
Original file line numberDiff line numberDiff line change
@@ -1,74 +1,88 @@
1+
package com.williamfiset.algorithms.dp;
2+
13
/**
2-
* An implementation of the edit distance algorithm
4+
* Edit Distance (Levenshtein Distance) — Iterative Bottom-Up DP
5+
*
6+
* Computes the minimum cost to transform string `a` into string `b` using
7+
* three operations, each with a configurable cost:
8+
*
9+
* - Insert a character into `a` (cost: insertionCost)
10+
* - Delete a character from `a` (cost: deletionCost)
11+
* - Substitute a character in `a` (cost: substitutionCost, 0 if chars match)
12+
*
13+
* The DP table dp[i][j] represents the cost of converting the first i
14+
* characters of `a` into the first j characters of `b`. Each cell is
15+
* computed from three neighbors: diagonal (substitute/match), above (delete),
16+
* and left (insert).
17+
*
18+
* See also: EditDistanceRecursive for a top-down memoized approach.
319
*
4-
* <p>Time Complexity: O(nm)
20+
* Tested against: https://leetcode.com/problems/edit-distance
21+
*
22+
* Time: O(n*m) where n = a.length(), m = b.length()
23+
* Space: O(n*m)
524
*
625
* @author Micah Stairs
726
*/
8-
package com.williamfiset.algorithms.dp;
9-
1027
public class EditDistanceIterative {
1128

12-
// Computes the cost to convert a string 'a' into a string 'b' using dynamic
13-
// programming given the insertionCost, deletionCost and substitutionCost, O(nm)
29+
/**
30+
* Computes the minimum cost to convert string `a` into string `b`.
31+
*
32+
* @param a the source string
33+
* @param b the target string
34+
* @param insertionCost cost of inserting one character
35+
* @param deletionCost cost of deleting one character
36+
* @param substitutionCost cost of substituting one character (0 cost if chars already match)
37+
* @return the minimum edit distance
38+
*
39+
* Time: O(n*m)
40+
* Space: O(n*m)
41+
*/
1442
public static int editDistance(
1543
String a, String b, int insertionCost, int deletionCost, int substitutionCost) {
16-
17-
final int AL = a.length(), BL = b.length();
18-
int[][] dp = new int[AL + 1][BL + 1];
19-
20-
for (int i = 0; i <= AL; i++) {
21-
for (int j = (i == 0 ? 1 : 0); j <= BL; j++) {
22-
23-
int min = Integer.MAX_VALUE;
24-
25-
// Substitution
26-
if (i > 0 && j > 0)
27-
min = dp[i - 1][j - 1] + (a.charAt(i - 1) == b.charAt(j - 1) ? 0 : substitutionCost);
28-
29-
// Deletion
30-
if (i > 0) min = Math.min(min, dp[i - 1][j] + deletionCost);
31-
32-
// Insertion
33-
if (j > 0) min = Math.min(min, dp[i][j - 1] + insertionCost);
34-
35-
dp[i][j] = min;
44+
if (a == null || b == null) throw new IllegalArgumentException("Input strings must not be null");
45+
46+
final int n = a.length(), m = b.length();
47+
int[][] dp = new int[n + 1][m + 1];
48+
49+
// Base cases: transforming a prefix of `a` into empty string (deletions only)
50+
for (int i = 1; i <= n; i++)
51+
dp[i][0] = i * deletionCost;
52+
53+
// Base cases: transforming empty string into a prefix of `b` (insertions only)
54+
for (int j = 1; j <= m; j++)
55+
dp[0][j] = j * insertionCost;
56+
57+
// Fill the DP table
58+
for (int i = 1; i <= n; i++) {
59+
for (int j = 1; j <= m; j++) {
60+
// If characters match, no substitution cost; otherwise pay substitutionCost
61+
int substitute = dp[i - 1][j - 1]
62+
+ (a.charAt(i - 1) == b.charAt(j - 1) ? 0 : substitutionCost);
63+
int delete = dp[i - 1][j] + deletionCost;
64+
int insert = dp[i][j - 1] + insertionCost;
65+
dp[i][j] = Math.min(substitute, Math.min(delete, insert));
3666
}
3767
}
3868

39-
return dp[AL][BL];
69+
return dp[n][m];
4070
}
4171

4272
public static void main(String[] args) {
73+
// Identical strings — cost is 0
74+
System.out.println(editDistance("abcdefg", "abcdefg", 10, 10, 10)); // 0
4375

44-
String a = "abcdefg";
45-
String b = "abcdefg";
46-
47-
// The strings are the same so the cost is zero
48-
System.out.println(EditDistanceIterative.editDistance(a, b, 10, 10, 10));
49-
50-
a = "aaa";
51-
b = "aaabbb";
52-
53-
// 10*3 = 30 because of three insertions
54-
System.out.println(EditDistanceIterative.editDistance(a, b, 10, 2, 3));
55-
56-
a = "1023";
57-
b = "10101010";
58-
59-
// Outputs 2*2 + 4*5 = 24 for 2 substitutions and 4 insertions
60-
System.out.println(EditDistanceIterative.editDistance(a, b, 5, 7, 2));
61-
62-
a = "923456789";
63-
b = "12345";
76+
// 3 insertions at cost 10 each = 30
77+
System.out.println(editDistance("aaa", "aaabbb", 10, 2, 3)); // 30
6478

65-
// Outputs 4*4 + 1 = 16 because we need to delete 4
66-
// characters and perform one substitution
67-
System.out.println(EditDistanceIterative.editDistance(a, b, 2, 4, 1));
79+
// 2 substitutions (cost 2) + 4 insertions (cost 5) = 24
80+
System.out.println(editDistance("1023", "10101010", 5, 7, 2)); // 24
6881

69-
a = "aaaaa";
70-
b = "aabaa";
82+
// 1 substitution (cost 1) + 4 deletions (cost 4) = 17
83+
System.out.println(editDistance("923456789", "12345", 2, 4, 1)); // 17
7184

72-
System.out.println(EditDistanceIterative.editDistance(a, b, 2, 3, 10));
85+
// Insert 'b' then delete 'a' is cheaper than substituting 'a'->'b'
86+
System.out.println(editDistance("aaaaa", "aabaa", 2, 3, 10)); // 5
7387
}
7488
}
Lines changed: 80 additions & 61 deletions
Original file line numberDiff line numberDiff line change
@@ -1,21 +1,47 @@
1+
package com.williamfiset.algorithms.dp;
2+
13
/**
2-
* A solution to the edit distance problem
4+
* Edit Distance (Levenshtein Distance) — Top-Down Recursive with Memoization
5+
*
6+
* Computes the minimum cost to transform string `a` into string `b` using
7+
* three operations, each with a configurable cost:
8+
*
9+
* - Insert a character into `a` (cost: insertionCost)
10+
* - Delete a character from `a` (cost: deletionCost)
11+
* - Substitute a character in `a` (cost: substitutionCost, 0 if chars match)
12+
*
13+
* The recursive function f(i, j) returns the cost of converting a[i..] into
14+
* b[j..]. At each step it considers three choices — substitute/match, delete,
15+
* insert — and memoizes results in a 2D table.
16+
*
17+
* Compared to EditDistanceIterative, the recursive approach only visits
18+
* reachable states, which can be faster when many states are unreachable.
19+
*
20+
* Tested against: https://leetcode.com/problems/edit-distance
321
*
4-
* <p>Tested against: https://leetcode.com/problems/edit-distance
22+
* Time: O(n*m) where n = a.length(), m = b.length()
23+
* Space: O(n*m)
524
*
625
* @author William Fiset, william.alexandre.fiset@gmail.com
726
*/
8-
package com.williamfiset.algorithms.dp;
9-
1027
public class EditDistanceRecursive {
1128

12-
final char[] a, b;
13-
final int insertionCost, deletionCost, substitutionCost;
29+
private final char[] a, b;
30+
private final int insertionCost, deletionCost, substitutionCost;
1431

32+
/**
33+
* Creates an edit distance solver for the given strings and operation costs.
34+
*
35+
* @param a the source string
36+
* @param b the target string
37+
* @param insertionCost cost of inserting one character
38+
* @param deletionCost cost of deleting one character
39+
* @param substitutionCost cost of substituting one character (0 cost if chars already match)
40+
*/
1541
public EditDistanceRecursive(
1642
String a, String b, int insertionCost, int deletionCost, int substitutionCost) {
1743
if (a == null || b == null) {
18-
throw new IllegalArgumentException("Input string must not be null");
44+
throw new IllegalArgumentException("Input strings must not be null");
1945
}
2046
this.a = a.toCharArray();
2147
this.b = b.toCharArray();
@@ -24,70 +50,63 @@ public EditDistanceRecursive(
2450
this.substitutionCost = substitutionCost;
2551
}
2652

27-
private static int min(int... values) {
28-
int m = Integer.MAX_VALUE;
29-
for (int v : values) {
30-
if (v < m) {
31-
m = v;
32-
}
33-
}
34-
return m;
35-
}
36-
37-
// Returns the Levenshtein distance to transform string `a` into string `b`.
53+
/**
54+
* Computes and returns the minimum edit distance from `a` to `b`.
55+
*
56+
* Time: O(n*m)
57+
* Space: O(n*m)
58+
*/
3859
public int editDistance() {
3960
Integer[][] dp = new Integer[a.length + 1][b.length + 1];
4061
return f(dp, 0, 0);
4162
}
4263

64+
/**
65+
* Recursive helper: returns the min cost to convert a[i..] into b[j..].
66+
*/
4367
private int f(Integer[][] dp, int i, int j) {
44-
if (i == a.length && j == b.length) {
45-
return 0;
46-
}
47-
if (i == a.length) {
48-
return (b.length - j) * insertionCost;
49-
}
50-
if (j == b.length) {
51-
return (a.length - i) * deletionCost;
52-
}
53-
if (dp[i][j] != null) {
54-
return dp[i][j];
55-
}
56-
int substituteOrSkip = f(dp, i + 1, j + 1) + (a[i] == b[j] ? 0 : substitutionCost);
68+
// Both strings fully consumed — nothing left to do
69+
if (i == a.length && j == b.length) return 0;
70+
71+
// Remaining characters in `b` must be inserted
72+
if (i == a.length) return (b.length - j) * insertionCost;
73+
74+
// Remaining characters in `a` must be deleted
75+
if (j == b.length) return (a.length - i) * deletionCost;
76+
77+
if (dp[i][j] != null) return dp[i][j];
78+
79+
// Match (free) or substitute, then advance both pointers
80+
int substitute = f(dp, i + 1, j + 1) + (a[i] == b[j] ? 0 : substitutionCost);
81+
82+
// Delete a[i], advance i only
5783
int delete = f(dp, i + 1, j) + deletionCost;
84+
85+
// Insert b[j] into a, advance j only
5886
int insert = f(dp, i, j + 1) + insertionCost;
59-
return dp[i][j] = min(substituteOrSkip, delete, insert);
87+
88+
return dp[i][j] = Math.min(substitute, Math.min(delete, insert));
6089
}
6190

6291
public static void main(String[] args) {
63-
String a = "923456789";
64-
String b = "12345";
65-
EditDistanceRecursive solver = new EditDistanceRecursive(a, b, 100, 4, 2);
66-
System.out.println(solver.editDistance());
67-
68-
a = "12345";
69-
b = "923456789";
70-
solver = new EditDistanceRecursive(a, b, 100, 4, 2);
71-
System.out.println(solver.editDistance());
72-
73-
a = "aaa";
74-
b = "aaabbb";
75-
solver = new EditDistanceRecursive(a, b, 10, 2, 3);
76-
System.out.println(solver.editDistance());
77-
78-
a = "1023";
79-
b = "10101010";
80-
solver = new EditDistanceRecursive(a, b, 5, 7, 2);
81-
System.out.println(solver.editDistance());
82-
83-
a = "923456789";
84-
b = "12345";
85-
EditDistanceRecursive solver2 = new EditDistanceRecursive(a, b, 100, 4, 2);
86-
System.out.println(solver2.editDistance());
87-
88-
a = "aaaaa";
89-
b = "aabaa";
90-
solver = new EditDistanceRecursive(a, b, 2, 3, 10);
91-
System.out.println(solver.editDistance());
92+
// 1 substitution (cost 2) + 4 deletions (cost 4) = 18
93+
System.out.println(
94+
new EditDistanceRecursive("923456789", "12345", 100, 4, 2).editDistance()); // 18
95+
96+
// Reverse direction: 1 substitution (cost 2) + 4 insertions (cost 100) = 402
97+
System.out.println(
98+
new EditDistanceRecursive("12345", "923456789", 100, 4, 2).editDistance()); // 402
99+
100+
// 3 insertions at cost 10 each = 30
101+
System.out.println(
102+
new EditDistanceRecursive("aaa", "aaabbb", 10, 2, 3).editDistance()); // 30
103+
104+
// 2 substitutions (cost 2) + 4 insertions (cost 5) = 24
105+
System.out.println(
106+
new EditDistanceRecursive("1023", "10101010", 5, 7, 2).editDistance()); // 24
107+
108+
// Insert 'b' then delete 'a' is cheaper than substituting 'a'->'b'
109+
System.out.println(
110+
new EditDistanceRecursive("aaaaa", "aabaa", 2, 3, 10).editDistance()); // 5
92111
}
93112
}

src/test/java/com/williamfiset/algorithms/dp/BUILD

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,5 +50,16 @@ java_test(
5050
deps = TEST_DEPS,
5151
)
5252

53+
# bazel test //src/test/java/com/williamfiset/algorithms/dp:EditDistanceIterativeTest
54+
java_test(
55+
name = "EditDistanceIterativeTest",
56+
srcs = ["EditDistanceIterativeTest.java"],
57+
main_class = "org.junit.platform.console.ConsoleLauncher",
58+
use_testrunner = False,
59+
args = ["--select-class=com.williamfiset.algorithms.dp.EditDistanceIterativeTest"],
60+
runtime_deps = JUNIT5_RUNTIME_DEPS,
61+
deps = TEST_DEPS,
62+
)
63+
5364
# Run all tests
5465
# bazel test //src/test/java/com/williamfiset/algorithms/dp:all

0 commit comments

Comments
 (0)