From 8e1f12447c23b0ac9179e76e4adef6b75ecc10e7 Mon Sep 17 00:00:00 2001 From: lmj798 <2757400745@qq.com> Date: Wed, 11 Mar 2026 21:31:15 +0800 Subject: [PATCH 1/6] test: enhance GenericHashMapUsingArrayTest with comprehensive edge case coverage (#7300) * test: enhance GenericHashMapUsingArrayTest with additional edge case coverage * Removed unused assertion 'assertNotEquals' from imports. * Simplify import statements in GenericHashMapUsingArrayTest * Refactor assertions to use Assertions class * Refactor null key test to use variable --- .../hashing/GenericHashMapUsingArrayTest.java | 190 +++++++++++++++--- 1 file changed, 164 insertions(+), 26 deletions(-) diff --git a/src/test/java/com/thealgorithms/datastructures/hashmap/hashing/GenericHashMapUsingArrayTest.java b/src/test/java/com/thealgorithms/datastructures/hashmap/hashing/GenericHashMapUsingArrayTest.java index 5d1733a3e97c..6b6e670a258b 100644 --- a/src/test/java/com/thealgorithms/datastructures/hashmap/hashing/GenericHashMapUsingArrayTest.java +++ b/src/test/java/com/thealgorithms/datastructures/hashmap/hashing/GenericHashMapUsingArrayTest.java @@ -1,11 +1,9 @@ package com.thealgorithms.datastructures.hashmap.hashing; -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertFalse; -import static org.junit.jupiter.api.Assertions.assertNotNull; -import static org.junit.jupiter.api.Assertions.assertTrue; - +import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; class GenericHashMapUsingArrayTest { @@ -16,10 +14,10 @@ void testGenericHashmapWhichUsesArrayAndBothKeyAndValueAreStrings() { map.put("Nepal", "Kathmandu"); map.put("India", "New Delhi"); map.put("Australia", "Sydney"); - assertNotNull(map); - assertEquals(4, map.size()); - assertEquals("Kathmandu", map.get("Nepal")); - assertEquals("Sydney", map.get("Australia")); + Assertions.assertNotNull(map); + Assertions.assertEquals(4, map.size()); + Assertions.assertEquals("Kathmandu", map.get("Nepal")); + Assertions.assertEquals("Sydney", map.get("Australia")); } @Test @@ -29,12 +27,12 @@ void testGenericHashmapWhichUsesArrayAndKeyIsStringValueIsInteger() { map.put("Nepal", 25); map.put("India", 101); map.put("Australia", 99); - assertNotNull(map); - assertEquals(4, map.size()); - assertEquals(25, map.get("Nepal")); - assertEquals(99, map.get("Australia")); + Assertions.assertNotNull(map); + Assertions.assertEquals(4, map.size()); + Assertions.assertEquals(25, map.get("Nepal")); + Assertions.assertEquals(99, map.get("Australia")); map.remove("Nepal"); - assertFalse(map.containsKey("Nepal")); + Assertions.assertFalse(map.containsKey("Nepal")); } @Test @@ -44,11 +42,11 @@ void testGenericHashmapWhichUsesArrayAndKeyIsIntegerValueIsString() { map.put(34, "Kathmandu"); map.put(46, "New Delhi"); map.put(89, "Sydney"); - assertNotNull(map); - assertEquals(4, map.size()); - assertEquals("Sydney", map.get(89)); - assertEquals("Washington DC", map.get(101)); - assertTrue(map.containsKey(46)); + Assertions.assertNotNull(map); + Assertions.assertEquals(4, map.size()); + Assertions.assertEquals("Sydney", map.get(89)); + Assertions.assertEquals("Washington DC", map.get(101)); + Assertions.assertTrue(map.containsKey(46)); } @Test @@ -56,7 +54,7 @@ void testRemoveNonExistentKey() { GenericHashMapUsingArray map = new GenericHashMapUsingArray<>(); map.put("USA", "Washington DC"); map.remove("Nepal"); // Attempting to remove a non-existent key - assertEquals(1, map.size()); // Size should remain the same + Assertions.assertEquals(1, map.size()); // Size should remain the same } @Test @@ -65,8 +63,8 @@ void testRehashing() { for (int i = 0; i < 20; i++) { map.put("Key" + i, "Value" + i); } - assertEquals(20, map.size()); // Ensure all items were added - assertEquals("Value5", map.get("Key5")); // Check retrieval after rehash + Assertions.assertEquals(20, map.size()); // Ensure all items were added + Assertions.assertEquals("Value5", map.get("Key5")); // Check retrieval after rehash } @Test @@ -74,7 +72,7 @@ void testUpdateValueForExistingKey() { GenericHashMapUsingArray map = new GenericHashMapUsingArray<>(); map.put("USA", "Washington DC"); map.put("USA", "New Washington DC"); // Updating value for existing key - assertEquals("New Washington DC", map.get("USA")); + Assertions.assertEquals("New Washington DC", map.get("USA")); } @Test @@ -83,14 +81,154 @@ void testToStringMethod() { map.put("USA", "Washington DC"); map.put("Nepal", "Kathmandu"); String expected = "{USA : Washington DC, Nepal : Kathmandu}"; - assertEquals(expected, map.toString()); + Assertions.assertEquals(expected, map.toString()); } @Test void testContainsKey() { GenericHashMapUsingArray map = new GenericHashMapUsingArray<>(); map.put("USA", "Washington DC"); - assertTrue(map.containsKey("USA")); - assertFalse(map.containsKey("Nepal")); + Assertions.assertTrue(map.containsKey("USA")); + Assertions.assertFalse(map.containsKey("Nepal")); + } + + // ======= Added tests from the new version ======= + + @Test + void shouldThrowNullPointerExceptionForNullKey() { + GenericHashMapUsingArray map = new GenericHashMapUsingArray<>(); + String nullKey = null; // Use variable to avoid static analysis false positive + Assertions.assertThrows(NullPointerException.class, () -> map.put(nullKey, "value")); + } + + @Test + void shouldStoreNullValueForKey() { + GenericHashMapUsingArray map = new GenericHashMapUsingArray<>(); + map.put("keyWithNullValue", null); + Assertions.assertEquals(1, map.size()); + Assertions.assertNull(map.get("keyWithNullValue")); + // Note: containsKey returns false for null values due to implementation + Assertions.assertFalse(map.containsKey("keyWithNullValue")); + } + + @Test + void shouldHandleCollisionWhenKeysHashToSameBucket() { + GenericHashMapUsingArray map = new GenericHashMapUsingArray<>(); + Integer key1 = 1; + Integer key2 = 17; + map.put(key1, 100); + map.put(key2, 200); + Assertions.assertEquals(2, map.size()); + Assertions.assertEquals(100, map.get(key1)); + Assertions.assertEquals(200, map.get(key2)); + Assertions.assertTrue(map.containsKey(key1)); + Assertions.assertTrue(map.containsKey(key2)); + } + + @Test + void shouldHandleEmptyStringAsKey() { + GenericHashMapUsingArray map = new GenericHashMapUsingArray<>(); + map.put("", "valueForEmptyKey"); + Assertions.assertEquals(1, map.size()); + Assertions.assertEquals("valueForEmptyKey", map.get("")); + Assertions.assertTrue(map.containsKey("")); + } + + @Test + void shouldHandleEmptyStringAsValue() { + GenericHashMapUsingArray map = new GenericHashMapUsingArray<>(); + map.put("keyForEmptyValue", ""); + Assertions.assertEquals(1, map.size()); + Assertions.assertEquals("", map.get("keyForEmptyValue")); + Assertions.assertTrue(map.containsKey("keyForEmptyValue")); + } + + @Test + void shouldHandleNegativeIntegerKeys() { + GenericHashMapUsingArray map = new GenericHashMapUsingArray<>(); + map.put(-1, 100); + map.put(-100, 200); + Assertions.assertEquals(2, map.size()); + Assertions.assertEquals(100, map.get(-1)); + Assertions.assertEquals(200, map.get(-100)); + Assertions.assertTrue(map.containsKey(-1)); + Assertions.assertTrue(map.containsKey(-100)); + } + + @Test + void shouldHandleZeroAsKey() { + GenericHashMapUsingArray map = new GenericHashMapUsingArray<>(); + map.put(0, 100); + Assertions.assertEquals(1, map.size()); + Assertions.assertEquals(100, map.get(0)); + Assertions.assertTrue(map.containsKey(0)); + } + + @Test + void shouldHandleStringWithSpecialCharacters() { + GenericHashMapUsingArray map = new GenericHashMapUsingArray<>(); + map.put("key!@#$%^&*()", "value<>?/\\|"); + Assertions.assertEquals(1, map.size()); + Assertions.assertEquals("value<>?/\\|", map.get("key!@#$%^&*()")); + Assertions.assertTrue(map.containsKey("key!@#$%^&*()")); + } + + @Test + void shouldHandleLongStrings() { + GenericHashMapUsingArray map = new GenericHashMapUsingArray<>(); + StringBuilder longKey = new StringBuilder(); + StringBuilder longValue = new StringBuilder(); + for (int i = 0; i < 1000; i++) { + longKey.append("a"); + longValue.append("b"); + } + String key = longKey.toString(); + String value = longValue.toString(); + map.put(key, value); + Assertions.assertEquals(1, map.size()); + Assertions.assertEquals(value, map.get(key)); + Assertions.assertTrue(map.containsKey(key)); + } + + @ParameterizedTest + @ValueSource(strings = {"a", "ab", "abc", "test", "longerString"}) + void shouldHandleKeysOfDifferentLengths(String key) { + GenericHashMapUsingArray map = new GenericHashMapUsingArray<>(); + map.put(key, "value"); + Assertions.assertEquals(1, map.size()); + Assertions.assertEquals("value", map.get(key)); + Assertions.assertTrue(map.containsKey(key)); + } + + @Test + void shouldHandleUpdateOnExistingKeyInCollisionBucket() { + GenericHashMapUsingArray map = new GenericHashMapUsingArray<>(); + Integer key1 = 1; + Integer key2 = 17; + map.put(key1, 100); + map.put(key2, 200); + Assertions.assertEquals(2, map.size()); + map.put(key2, 999); + Assertions.assertEquals(2, map.size()); + Assertions.assertEquals(100, map.get(key1)); + Assertions.assertEquals(999, map.get(key2)); + Assertions.assertTrue(map.containsKey(key1)); + Assertions.assertTrue(map.containsKey(key2)); + } + + @Test + void shouldHandleExactlyLoadFactorBoundary() { + GenericHashMapUsingArray map = new GenericHashMapUsingArray<>(); + // Fill exactly to load factor (12 items with capacity 16 and 0.75 load factor) + for (int i = 0; i < 12; i++) { + map.put(i, i * 10); + } + Assertions.assertEquals(12, map.size()); + // Act - This should trigger rehash on 13th item + map.put(12, 120); + // Assert - Rehash should have happened + Assertions.assertEquals(13, map.size()); + Assertions.assertEquals(120, map.get(12)); + Assertions.assertTrue(map.containsKey(12)); } } From 5e06b1592638ac0826258341398f92537717eac3 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 11 Mar 2026 23:12:37 +0100 Subject: [PATCH 2/6] chore(deps-dev): bump org.mockito:mockito-core from 5.22.0 to 5.23.0 (#7305) Bumps [org.mockito:mockito-core](https://github.com/mockito/mockito) from 5.22.0 to 5.23.0. - [Release notes](https://github.com/mockito/mockito/releases) - [Commits](https://github.com/mockito/mockito/compare/v5.22.0...v5.23.0) --- updated-dependencies: - dependency-name: org.mockito:mockito-core dependency-version: 5.23.0 dependency-type: direct:development update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index b2192fb9a64a..dab7447430e5 100644 --- a/pom.xml +++ b/pom.xml @@ -42,7 +42,7 @@ org.mockito mockito-core - 5.22.0 + 5.23.0 test From 8bbd090e0cc7cf0dc9b2e3e74f9a33ca8dc297e6 Mon Sep 17 00:00:00 2001 From: kvadrik <41710943+kvadrik@users.noreply.github.com> Date: Sat, 14 Mar 2026 23:09:19 +0200 Subject: [PATCH 3/6] Overlapping condition changed (#7314) Overlapping happens not when centers of circular bodies are in the same point, but when the distance between them is smaller than the sum of their radii. --- src/main/java/com/thealgorithms/physics/ElasticCollision2D.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/com/thealgorithms/physics/ElasticCollision2D.java b/src/main/java/com/thealgorithms/physics/ElasticCollision2D.java index 399c3f1e041f..d096e0a8d7cd 100644 --- a/src/main/java/com/thealgorithms/physics/ElasticCollision2D.java +++ b/src/main/java/com/thealgorithms/physics/ElasticCollision2D.java @@ -41,7 +41,7 @@ public static void resolveCollision(Body a, Body b) { double dy = b.y - a.y; double dist = Math.hypot(dx, dy); - if (dist == 0) { + if (dist < a.radius + b.radius) { return; // overlapping } From 24c2beae463b7b1f8c2616110911449589af482b Mon Sep 17 00:00:00 2001 From: Maryam Hazrati <117775713+Maryamh12@users.noreply.github.com> Date: Sat, 14 Mar 2026 21:15:50 +0000 Subject: [PATCH 4/6] Improve space complexity to O(1) in WordSearch. (#7308) * Modifying space complexity to O(1). * Fix formatting using clang-format. * Fix checkstyle violations. * Fix checkstyle violations and code formatting. * Remove unused fields reported by SpotBugs. * Remove unused fields and comments. * Remove unused field reported by SpotBugs. * Fix PMD collapsible if statement. * Fix indentation to satisfy clang-format. --------- Co-authored-by: Deniz Altunkapan --- .../backtracking/WordSearch.java | 66 +++++++------------ 1 file changed, 25 insertions(+), 41 deletions(-) diff --git a/src/main/java/com/thealgorithms/backtracking/WordSearch.java b/src/main/java/com/thealgorithms/backtracking/WordSearch.java index 174ca90ccaab..452f17b6ace6 100644 --- a/src/main/java/com/thealgorithms/backtracking/WordSearch.java +++ b/src/main/java/com/thealgorithms/backtracking/WordSearch.java @@ -35,22 +35,6 @@ * - Stack space for the recursive DFS function, where L is the maximum depth of recursion (length of the word). */ public class WordSearch { - private final int[] dx = {0, 0, 1, -1}; - private final int[] dy = {1, -1, 0, 0}; - private boolean[][] visited; - private char[][] board; - private String word; - - /** - * Checks if the given (x, y) coordinates are valid positions in the board. - * - * @param x The row index. - * @param y The column index. - * @return True if the coordinates are within the bounds of the board; false otherwise. - */ - private boolean isValid(int x, int y) { - return x >= 0 && x < board.length && y >= 0 && y < board[0].length; - } /** * Performs Depth First Search (DFS) from the cell (x, y) @@ -58,28 +42,27 @@ private boolean isValid(int x, int y) { * * @param x The current row index. * @param y The current column index. - * @param nextIdx The index of the next character in the word to be matched. + * @param idx The index of the next character in the word to be matched. * @return True if a valid path is found to match the remaining characters of the word; false otherwise. */ - private boolean doDFS(int x, int y, int nextIdx) { - visited[x][y] = true; - if (nextIdx == word.length()) { + + private boolean dfs(char[][] board, int x, int y, String word, int idx) { + if (idx == word.length()) { return true; } - for (int i = 0; i < 4; ++i) { - int xi = x + dx[i]; - int yi = y + dy[i]; - if (isValid(xi, yi) && board[xi][yi] == word.charAt(nextIdx) && !visited[xi][yi]) { - boolean exists = doDFS(xi, yi, nextIdx + 1); - if (exists) { - return true; - } - } + if (x < 0 || y < 0 || x >= board.length || y >= board[0].length || board[x][y] != word.charAt(idx)) { + return false; } - visited[x][y] = false; // Backtrack - return false; + char temp = board[x][y]; + board[x][y] = '#'; + + boolean found = dfs(board, x + 1, y, word, idx + 1) || dfs(board, x - 1, y, word, idx + 1) || dfs(board, x, y + 1, word, idx + 1) || dfs(board, x, y - 1, word, idx + 1); + + board[x][y] = temp; + + return found; } /** @@ -90,20 +73,21 @@ private boolean doDFS(int x, int y, int nextIdx) { * @param word The target word to search for in the board. * @return True if the word exists in the board; false otherwise. */ + public boolean exist(char[][] board, String word) { - this.board = board; - this.word = word; - for (int i = 0; i < board.length; ++i) { - for (int j = 0; j < board[0].length; ++j) { - if (board[i][j] == word.charAt(0)) { - visited = new boolean[board.length][board[0].length]; - boolean exists = doDFS(i, j, 1); - if (exists) { - return true; - } + + int m = board.length; + int n = board[0].length; + + // DFS search + for (int i = 0; i < m; i++) { + for (int j = 0; j < n; j++) { + if (board[i][j] == word.charAt(0) && dfs(board, i, j, word, 0)) { + return true; } } } + return false; } } From 7d57c5720670dc0fd6e37d7bdc41ad053d925f5b Mon Sep 17 00:00:00 2001 From: kvadrik <41710943+kvadrik@users.noreply.github.com> Date: Sun, 15 Mar 2026 19:44:31 +0200 Subject: [PATCH 5/6] Added quadratic mean (#7315) * Added quadratic mean Added a quadratic mean of the given numbers, sqrt ((n1^2+n2^2+...+nk^2)/k). * Added tests for quadratic mean * Corrected quadratic mean * Added comment to quadratic mean * Corrected quadratic mean tests * Replaced sqrt by pow * Error fixed * Extra whitespace removed * Extra whitespace removed * Removed extra white space * Removed extra white space --- .../java/com/thealgorithms/maths/Means.java | 22 ++++++++ .../com/thealgorithms/maths/MeansTest.java | 53 ++++++++++++++++++- 2 files changed, 74 insertions(+), 1 deletion(-) diff --git a/src/main/java/com/thealgorithms/maths/Means.java b/src/main/java/com/thealgorithms/maths/Means.java index 5445a3caebc7..d77eb1d3f661 100644 --- a/src/main/java/com/thealgorithms/maths/Means.java +++ b/src/main/java/com/thealgorithms/maths/Means.java @@ -107,6 +107,28 @@ public static Double harmonic(final Iterable numbers) { return size / sumOfReciprocals; } + /** + * Computes the quadratic mean (root mean square) of the given numbers. + *

+ * The quadratic mean is calculated as: √[(x₁^2 × x₂^2 × ... × xₙ^2)/n] + *

+ *

+ * Example: For numbers [1, 7], the quadratic mean is √[(1^2+7^2)/2] = √25 = 5.0 + *

+ * + * @param numbers the input numbers (must not be empty) + * @return the quadratic mean of the input numbers + * @throws IllegalArgumentException if the input is empty + * @see Quadratic + * Mean + */ + public static Double quadratic(final Iterable numbers) { + checkIfNotEmpty(numbers); + double sumOfSquares = StreamSupport.stream(numbers.spliterator(), false).reduce(0d, (x, y) -> x + y * y); + int size = IterableUtils.size(numbers); + return Math.pow(sumOfSquares / size, 0.5); + } + /** * Validates that the input iterable is not empty. * diff --git a/src/test/java/com/thealgorithms/maths/MeansTest.java b/src/test/java/com/thealgorithms/maths/MeansTest.java index deee0a931910..853fdbea3963 100644 --- a/src/test/java/com/thealgorithms/maths/MeansTest.java +++ b/src/test/java/com/thealgorithms/maths/MeansTest.java @@ -172,6 +172,53 @@ void testHarmonicMeanWithLinkedList() { assertEquals(expected, Means.harmonic(numbers), EPSILON); } + // ========== Quadratic Mean Tests ========== + + @Test + void testQuadraticMeanThrowsExceptionForEmptyList() { + List numbers = new ArrayList<>(); + IllegalArgumentException exception = assertThrows(IllegalArgumentException.class, () -> Means.quadratic(numbers)); + assertTrue(exception.getMessage().contains("Empty list")); + } + + @Test + void testQuadraticMeanSingleNumber() { + LinkedHashSet numbers = new LinkedHashSet<>(Arrays.asList(2.5)); + assertEquals(2.5, Means.quadratic(numbers), EPSILON); + } + + @Test + void testQuadraticMeanTwoNumbers() { + List numbers = Arrays.asList(1.0, 7.0); + assertEquals(5.0, Means.quadratic(numbers), EPSILON); + } + + @Test + void testQuadraticMeanMultipleNumbers() { + Vector numbers = new Vector<>(Arrays.asList(1.0, 2.5, 3.0, 7.5, 10.0)); + double expected = Math.sqrt(34.5); + assertEquals(expected, Means.quadratic(numbers), EPSILON); + } + + @Test + void testQuadraticMeanThreeNumbers() { + List numbers = Arrays.asList(3.0, 6.0, 9.0); + double expected = Math.sqrt(42.0); + assertEquals(expected, Means.quadratic(numbers), EPSILON); + } + + @Test + void testQuadraticMeanIdenticalNumbers() { + List numbers = Arrays.asList(5.0, 5.0, 5.0); + assertEquals(5.0, Means.quadratic(numbers), EPSILON); + } + + @Test + void testQuadraticMeanWithLinkedList() { + LinkedList numbers = new LinkedList<>(Arrays.asList(1.0, 5.0, 11.0)); + assertEquals(7.0, Means.quadratic(numbers), EPSILON); + } + // ========== Additional Edge Case Tests ========== @Test @@ -198,21 +245,25 @@ void testAllMeansConsistencyForIdenticalValues() { double arithmetic = Means.arithmetic(numbers); double geometric = Means.geometric(numbers); double harmonic = Means.harmonic(numbers); + double quadratic = Means.quadratic(numbers); assertEquals(7.5, arithmetic, EPSILON); assertEquals(7.5, geometric, EPSILON); assertEquals(7.5, harmonic, EPSILON); + assertEquals(7.5, quadratic, EPSILON); } @Test void testMeansRelationship() { - // For positive numbers, harmonic mean ≤ geometric mean ≤ arithmetic mean + // For positive numbers, harmonic mean ≤ geometric mean ≤ arithmetic mean ≤ quadratic mean List numbers = Arrays.asList(2.0, 4.0, 8.0); double arithmetic = Means.arithmetic(numbers); double geometric = Means.geometric(numbers); double harmonic = Means.harmonic(numbers); + double quadratic = Means.quadratic(numbers); assertTrue(harmonic <= geometric, "Harmonic mean should be ≤ geometric mean"); assertTrue(geometric <= arithmetic, "Geometric mean should be ≤ arithmetic mean"); + assertTrue(arithmetic <= quadratic, "Arithmetic mean should be ≤ quadratic mean"); } } From af1d9d166522e3904ce60f2303d1ad9d8b462d62 Mon Sep 17 00:00:00 2001 From: Keykyrios Date: Thu, 19 Mar 2026 00:55:39 +0530 Subject: [PATCH 6/6] feat(strings): add Kasai's algorithm for LCP array construction (#7324) * feat(strings): add Kasai's algorithm for LCP array Implement Kasai's algorithm to compute the Longest Common Prefix (LCP) array in O(N) time given a string and its suffix array. Add KasaiAlgorithm.java and KasaiAlgorithmTest.java. * style(strings): fix KasaiAlgorithmTest array initialization format for clang-format --- .../thealgorithms/strings/KasaiAlgorithm.java | 79 +++++++++++++++++++ .../strings/KasaiAlgorithmTest.java | 75 ++++++++++++++++++ 2 files changed, 154 insertions(+) create mode 100644 src/main/java/com/thealgorithms/strings/KasaiAlgorithm.java create mode 100644 src/test/java/com/thealgorithms/strings/KasaiAlgorithmTest.java diff --git a/src/main/java/com/thealgorithms/strings/KasaiAlgorithm.java b/src/main/java/com/thealgorithms/strings/KasaiAlgorithm.java new file mode 100644 index 000000000000..b8b10dcf4538 --- /dev/null +++ b/src/main/java/com/thealgorithms/strings/KasaiAlgorithm.java @@ -0,0 +1,79 @@ +package com.thealgorithms.strings; + +/** + * Kasai's Algorithm for constructing the Longest Common Prefix (LCP) array. + * + *

+ * 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. + *

+ * + * @see LCP array - Wikipedia + */ +public final class KasaiAlgorithm { + + private KasaiAlgorithm() { + } + + /** + * Computes the LCP array using Kasai's algorithm. + * + * @param text the original string + * @param suffixArr the suffix array of the string + * @return the LCP array of length N, where LCP[i] is the length of the longest + * common prefix of the suffixes indexed by suffixArr[i] and suffixArr[i+1]. + * The last element LCP[N-1] is always 0. + * @throws IllegalArgumentException if text or suffixArr is null, or their lengths differ + */ + public static int[] kasai(String text, int[] suffixArr) { + if (text == null || suffixArr == null) { + throw new IllegalArgumentException("Text and suffix array must not be null."); + } + int n = text.length(); + if (suffixArr.length != n) { + throw new IllegalArgumentException("Suffix array length must match text length."); + } + if (n == 0) { + return new int[0]; + } + + // Compute the inverse suffix array + // invSuff[i] stores the index of the suffix text.substring(i) in the suffix array + int[] invSuff = new int[n]; + for (int i = 0; i < n; i++) { + if (suffixArr[i] < 0 || suffixArr[i] >= n) { + throw new IllegalArgumentException("Suffix array contains out-of-bounds index."); + } + invSuff[suffixArr[i]] = i; + } + + int[] lcp = new int[n]; + int k = 0; // Length of the longest common prefix + + for (int i = 0; i < n; i++) { + // Suffix at index i has not a next suffix in suffix array + int rank = invSuff[i]; + if (rank == n - 1) { + k = 0; + continue; + } + + int nextSuffixIndex = suffixArr[rank + 1]; + + // Directly match characters to find LCP + while (i + k < n && nextSuffixIndex + k < n && text.charAt(i + k) == text.charAt(nextSuffixIndex + k)) { + k++; + } + + lcp[rank] = k; + + // Delete the starting character from the string + if (k > 0) { + k--; + } + } + + return lcp; + } +} diff --git a/src/test/java/com/thealgorithms/strings/KasaiAlgorithmTest.java b/src/test/java/com/thealgorithms/strings/KasaiAlgorithmTest.java new file mode 100644 index 000000000000..c22cc77df18a --- /dev/null +++ b/src/test/java/com/thealgorithms/strings/KasaiAlgorithmTest.java @@ -0,0 +1,75 @@ +package com.thealgorithms.strings; + +import static org.junit.jupiter.api.Assertions.assertArrayEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; + +import org.junit.jupiter.api.Test; + +public class KasaiAlgorithmTest { + + @Test + public void testKasaiBanana() { + String text = "banana"; + // Suffixes: + // 0: banana + // 1: anana + // 2: nana + // 3: ana + // 4: na + // 5: a + // + // Sorted Suffixes: + // 5: a + // 3: ana + // 1: anana + // 0: banana + // 4: na + // 2: nana + int[] suffixArr = {5, 3, 1, 0, 4, 2}; + + int[] expectedLcp = {1, 3, 0, 0, 2, 0}; + + assertArrayEquals(expectedLcp, KasaiAlgorithm.kasai(text, suffixArr)); + } + + @Test + public void testKasaiAaaa() { + String text = "aaaa"; + // Sorted Suffixes: + // 3: a + // 2: aa + // 1: aaa + // 0: aaaa + int[] suffixArr = {3, 2, 1, 0}; + int[] expectedLcp = {1, 2, 3, 0}; + + assertArrayEquals(expectedLcp, KasaiAlgorithm.kasai(text, suffixArr)); + } + + @Test + public void testKasaiEmptyString() { + assertArrayEquals(new int[0], KasaiAlgorithm.kasai("", new int[0])); + } + + @Test + public void testKasaiSingleChar() { + assertArrayEquals(new int[] {0}, KasaiAlgorithm.kasai("A", new int[] {0})); + } + + @Test + public void testKasaiNullTextOrSuffixArray() { + assertThrows(IllegalArgumentException.class, () -> KasaiAlgorithm.kasai(null, new int[] {0})); + assertThrows(IllegalArgumentException.class, () -> KasaiAlgorithm.kasai("A", null)); + } + + @Test + public void testKasaiInvalidSuffixArrayLength() { + assertThrows(IllegalArgumentException.class, () -> KasaiAlgorithm.kasai("A", new int[] {0, 1})); + } + + @Test + 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 + } +}