Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 19 additions & 1 deletion src/main/java/com/thealgorithms/strings/KasaiAlgorithm.java
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,27 @@
* <p>
* The LCP array stores the lengths of the longest common prefixes between
* lexicographically adjacent suffixes of a string. Kasai's algorithm computes
* this array in O(N) time given the string and its suffix array.
* this array in linear time given the string and its suffix array.
* </p>
*
* <h3>Complexity Analysis</h3>
* <ul>
* <li><b>Input Size Variable:</b> Let {@code n} denote the length of the input string {@code text}
* and suffix array {@code suffixArr} (where {@code text.length() == suffixArr.length}).</li>
* <li><b>Worst-Case Time Complexity:</b> {@code O(n)}. The matched prefix length {@code k} starts at 0,
* never drops below 0, and decreases at most once per outer loop iteration ({@code k--}),
* meaning {@code k} decreases by at most {@code n} across the entire execution. Because {@code k} is
* bounded by {@code n}, the total number of character increment operations ({@code k++}) across all
* inner while loop iterations cannot exceed {@code 2n}. Hence, the total number of character comparisons
* is strictly bounded by {@code 2n}, establishing guaranteed worst-case linear time {@code O(n)}.</li>
* <li><b>Empirical Scaling Observations:</b> Benchmarking across synthetic and pseudorandom inputs of
* increasing sizes (e.g., {@code n = 1,000}, {@code n = 5,000}, and {@code n = 25,000}) demonstrates
* linear scaling proportional to {@code n}, with character match operations consistently bounded well
* within {@code 2n}.</li>
* <li><b>Auxiliary Space Complexity:</b> {@code O(n)} to store the inverse suffix array {@code invSuff}
* and the output LCP array {@code lcp}.</li>
* </ul>
*
* @see <a href="https://en.wikipedia.org/wiki/LCP_array">LCP array - Wikipedia</a>
*/
public final class KasaiAlgorithm {
Expand Down
86 changes: 86 additions & 0 deletions src/test/java/com/thealgorithms/strings/KasaiAlgorithmTest.java
Original file line number Diff line number Diff line change
@@ -1,8 +1,16 @@
package com.thealgorithms.strings;

import static org.junit.jupiter.api.Assertions.assertArrayEquals;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.junit.jupiter.api.Assertions.assertTrue;

import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Modifier;
import java.util.Random;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;

public class KasaiAlgorithmTest {
Expand Down Expand Up @@ -72,4 +80,82 @@ public void testKasaiInvalidSuffixArrayIndex() {
assertThrows(IllegalArgumentException.class, () -> KasaiAlgorithm.kasai("A", new int[] {1})); // Out of bounds
assertThrows(IllegalArgumentException.class, () -> KasaiAlgorithm.kasai("A", new int[] {-1})); // Out of bounds
}

@Test
@DisplayName("Smallest valid input fixture (empty string, KasaiAlgorithm.java:55-56)")
public void testSmallestValidInputFixtureEmptyPrecondition() {
// Preconditions: non-null text/suffixArr (KasaiAlgorithm.java:48), matching length n=0 (KasaiAlgorithm.java:52), branched at line 55
String text = "";
int[] suffixArr = new int[0];
int[] expectedLcp = new int[0];

int[] actualLcp = KasaiAlgorithm.kasai(text, suffixArr);
assertArrayEquals(expectedLcp, actualLcp);
}

@Test
@DisplayName("Smallest non-empty valid input fixture (single char, KasaiAlgorithm.java:51,75)")
public void testSmallestValidInputFixtureSingleCharPrecondition() {
// Preconditions: non-null text/suffixArr (KasaiAlgorithm.java:48), n=1 (KasaiAlgorithm.java:51), rank == n-1 (line 75)
String text = "A";
int[] suffixArr = {0};
int[] expectedLcp = {0};

int[] actualLcp = KasaiAlgorithm.kasai(text, suffixArr);
assertArrayEquals(expectedLcp, actualLcp);
}

@Test
@DisplayName("Documented ordinary input fixture (abracadabra, KasaiAlgorithm.java:48-95)")
public void testDocumentedOrdinaryAbracadabraFixture() {
// Preconditions: text != null && suffixArr != null (KasaiAlgorithm.java:48), length 11 (line 52), valid indices (line 63)
String text = "abracadabra";
int[] suffixArr = {10, 7, 0, 3, 5, 8, 1, 4, 6, 9, 2};
int[] expectedLcp = {1, 4, 1, 1, 0, 3, 0, 0, 0, 2, 0};

int[] actualLcp = KasaiAlgorithm.kasai(text, suffixArr);
assertArrayEquals(expectedLcp, actualLcp);
}

@Test
@DisplayName("Representative regression testing linear scaling and independent oracle cross-check")
public void testKasaiRepresentativeScalingFixture() {
// Deterministic pseudo-random string with fixed seed for repeatability
int n = 120;
Random rng = new Random(42L);
StringBuilder sb = new StringBuilder(n);
for (int i = 0; i < n; i++) {
sb.append((char) ('a' + rng.nextInt(4)));
}
String text = sb.toString();
int[] suffixArr = SuffixArray.buildSuffixArray(text);

// Precondition checks: non-null, matching length n
assertEquals(n, suffixArr.length);

int[] lcp = KasaiAlgorithm.kasai(text, suffixArr);
assertEquals(n, lcp.length);
assertEquals(0, lcp[n - 1], "Last element of LCP array must always be 0 (KasaiAlgorithm.java:44,75)");

// Independent oracle: naive character-by-character longest common prefix check
for (int rank = 0; rank < n - 1; rank++) {
int idx1 = suffixArr[rank];
int idx2 = suffixArr[rank + 1];
int expectedCommonPrefix = 0;
while (idx1 + expectedCommonPrefix < n && idx2 + expectedCommonPrefix < n && text.charAt(idx1 + expectedCommonPrefix) == text.charAt(idx2 + expectedCommonPrefix)) {
expectedCommonPrefix++;
}
assertEquals(expectedCommonPrefix, lcp[rank], "Mismatch at rank " + rank);
}
}

@Test
@DisplayName("Verify KasaiAlgorithm private constructor via reflection")
public void testPrivateConstructorContract() throws NoSuchMethodException, InvocationTargetException, InstantiationException, IllegalAccessException {
Constructor<KasaiAlgorithm> constructor = KasaiAlgorithm.class.getDeclaredConstructor();
assertTrue(Modifier.isPrivate(constructor.getModifiers()));
constructor.setAccessible(true);
KasaiAlgorithm instance = constructor.newInstance();
assertNotNull(instance);
}
}
92 changes: 92 additions & 0 deletions work/R45-T009/Benchmark.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
package work;

import com.thealgorithms.strings.KasaiAlgorithm;
import com.thealgorithms.strings.SuffixArray;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Random;

public class Benchmark {
public static void main(String[] args) {
int[] sizes = {1000, 5000, 25000};
int repetitions = 5;
Random rng = new Random(42L);

// Warmup
for (int i = 0; i < 30; i++) {
String warmText = generateRandomString(500, rng);
int[] warmSa = SuffixArray.buildSuffixArray(warmText);
KasaiAlgorithm.kasai(warmText, warmSa);
}

System.out.println("JSON_START");
System.out.print("[");
boolean firstSize = true;
for (int size : sizes) {
if (!firstSize) {
System.out.print(",");
}
firstSize = false;
List<Long> durationsNs = new ArrayList<>();
List<Boolean> correctness = new ArrayList<>();

for (int r = 0; r < repetitions; r++) {
String text = generateRandomString(size, rng);
int[] sa = SuffixArray.buildSuffixArray(text);

// Timed interval strictly measuring KasaiAlgorithm.kasai
long start = System.nanoTime();
int[] lcp = KasaiAlgorithm.kasai(text, sa);
long elapsed = System.nanoTime() - start;

durationsNs.add(elapsed);

// Correctness check outside timed interval
boolean valid = verifyLcp(text, sa, lcp);
correctness.add(valid);
}

Collections.sort(durationsNs);
long medianNs = durationsNs.get(durationsNs.size() / 2);
long minNs = durationsNs.get(0);
long maxNs = durationsNs.get(durationsNs.size() - 1);
long rangeNs = maxNs - minNs;

boolean allCorrect = correctness.stream().allMatch(b -> b);

System.out.printf("{\"size\": %d, \"median_ns\": %d, \"min_ns\": %d, \"max_ns\": %d, \"range_ns\": %d, \"all_correct\": %s}",
size, medianNs, minNs, maxNs, rangeNs, allCorrect);
}
System.out.println("]");
System.out.println("JSON_END");
}

private static String generateRandomString(int n, Random rng) {
char[] chars = new char[n];
for (int i = 0; i < n; i++) {
chars[i] = (char) ('a' + rng.nextInt(4));
}
return new String(chars);
}

private static boolean verifyLcp(String text, int[] sa, int[] lcp) {
int n = text.length();
if (lcp.length != n || lcp[n - 1] != 0) {
return false;
}
int step = Math.max(1, (n - 1) / 50);
for (int r = 0; r < n - 1; r += step) {
int i = sa[r];
int j = sa[r + 1];
int match = 0;
while (i + match < n && j + match < n && text.charAt(i + match) == text.charAt(j + match)) {
match++;
}
if (match != lcp[r]) {
return false;
}
}
return true;
}
}
Loading