diff --git a/src/main/java/com/thealgorithms/dynamicprogramming/RegularExpressionMatching.java b/src/main/java/com/thealgorithms/dynamicprogramming/RegularExpressionMatching.java new file mode 100644 index 000000000000..b3bc0afad971 --- /dev/null +++ b/src/main/java/com/thealgorithms/dynamicprogramming/RegularExpressionMatching.java @@ -0,0 +1,179 @@ +package com.thealgorithms.dynamicprogramming; + +/** + * Implements regular expression matching with support for '.' and '*'. + * + *
+ * The regular expression matching problem involves determining if a given string + * matches a pattern containing special characters '.' and '*'. The '.' matches + * any single character, while '*' matches zero or more of the preceding element. + * + *
+ * This solution uses dynamic programming with memoization for efficient computation. + * + *
+ * For more information: + * @see Regular Expression + * @see LeetCode Problem 10 + * + *
+ * Example: + *
+ * Input: s = "aa", p = "a" → Output: false + * Input: s = "aa", p = "a*" → Output: true + * Input: s = "ab", p = ".*" → Output: true + *+ * + *
+ * Time Complexity: O(m * n) where m is length of s and n is length of p + * Space Complexity: O(m * n) for the memoization table + */ +public final class RegularExpressionMatching { + private RegularExpressionMatching() { + // Private constructor to prevent instantiation + } + + /** + * Determines if the input string matches the given pattern. + * + * @param inputString the input string to match (contains only lowercase English letters) + * @param pattern the pattern (contains lowercase English letters, '.', and '*') + * @return true if the entire string matches the pattern, false otherwise + * @throws IllegalArgumentException if input strings are null or pattern is invalid + */ + public static boolean isMatch(String inputString, String pattern) { + if (inputString == null || pattern == null) { + throw new IllegalArgumentException("Input strings cannot be null"); + } + + if (!isValidPattern(pattern)) { + throw new IllegalArgumentException("Invalid pattern format"); + } + + Boolean[][] memo = new Boolean[inputString.length() + 1][pattern.length() + 1]; + return dynamicProgramming(0, 0, inputString, pattern, memo); + } + + /** + * Helper method that performs the actual dynamic programming computation. + * + * @param stringIndex current index in string s + * @param patternIndex current index in pattern p + * @param inputString the input string + * @param pattern the pattern + * @param memo memoization table storing computed results + * @return true if s[i:] matches p[j:], false otherwise + */ + private static boolean dynamicProgramming(int stringIndex, int patternIndex, + String inputString, String pattern, + Boolean[][] memo) { + if (memo[stringIndex][patternIndex] != null) { + return memo[stringIndex][patternIndex]; + } + + boolean result; + + if (patternIndex == pattern.length()) { + result = (stringIndex == inputString.length()); + } else { + boolean currentMatch = stringIndex < inputString.length() && + (pattern.charAt(patternIndex) == '.' || + pattern.charAt(patternIndex) == inputString.charAt(stringIndex)); + + if (patternIndex + 1 < pattern.length() && pattern.charAt(patternIndex + 1) == '*') { + result = dynamicProgramming(stringIndex, patternIndex + 2, inputString, pattern, memo) || + (currentMatch && dynamicProgramming(stringIndex + 1, patternIndex, inputString, pattern, memo)); + } else { + result = currentMatch && dynamicProgramming(stringIndex + 1, patternIndex + 1, inputString, pattern, memo); + } + } + + memo[stringIndex][patternIndex] = result; + return result; + } + + /** + * Validates that the pattern follows the constraints. + * + * @param pattern the pattern to validate + * @return true if pattern is valid, false otherwise + */ + private static boolean isValidPattern(String pattern) { + if (pattern.isEmpty()) { + return true; + } + + if (pattern.charAt(0) == '*') { + return false; + } + + for (int i = 0; i < pattern.length(); i++) { + char currentChar = pattern.charAt(i); + if (!isValidPatternChar(currentChar)) { + return false; + } + + if (currentChar == '*' && (i == 0 || pattern.charAt(i - 1) == '*')) { + return false; + } + } + + return true; + } + + /** + * Checks if a character is valid in a pattern. + */ + private static boolean isValidPatternChar(char character) { + return (character >= 'a' && character <= 'z') || character == '.' || character == '*'; + } + + /** + * Alternative iterative DP solution (bottom-up approach). + * + * @param inputString the input string + * @param pattern the pattern + * @return true if string matches pattern, false otherwise + */ + public static boolean isMatchIterative(String inputString, String pattern) { + if (inputString == null || pattern == null) { + throw new IllegalArgumentException("Input strings cannot be null"); + } + + if (!isValidPattern(pattern)) { + throw new IllegalArgumentException("Invalid pattern format"); + } + + int stringLength = inputString.length(); + int patternLength = pattern.length(); + + boolean[][] dp = new boolean[stringLength + 1][patternLength + 1]; + + dp[0][0] = true; + + for (int j = 2; j <= patternLength; j++) { + if (pattern.charAt(j - 1) == '*') { + dp[0][j] = dp[0][j - 2]; + } + } + + for (int i = 1; i <= stringLength; i++) { + for (int j = 1; j <= patternLength; j++) { + char stringChar = inputString.charAt(i - 1); + char patternChar = pattern.charAt(j - 1); + + if (patternChar == '.' || patternChar == stringChar) { + dp[i][j] = dp[i - 1][j - 1]; + } else if (patternChar == '*') { + char previousChar = pattern.charAt(j - 2); + dp[i][j] = dp[i][j - 2]; + if (previousChar == '.' || previousChar == stringChar) { + dp[i][j] = dp[i][j] || dp[i - 1][j]; + } + } + } + } + + return dp[stringLength][patternLength]; + } +} diff --git a/src/test/java/com/thealgorithms/RegularExpressionMatchingTest.java b/src/test/java/com/thealgorithms/RegularExpressionMatchingTest.java new file mode 100644 index 000000000000..bc06efcc5d95 --- /dev/null +++ b/src/test/java/com/thealgorithms/RegularExpressionMatchingTest.java @@ -0,0 +1,122 @@ +package com.thealgorithms.dynamicprogramming; + +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import org.junit.jupiter.api.Test; + +/** + * Unit tests for RegularExpressionMatching algorithm. + * + *
+ * For more information about regular expression matching: + * @see Regular Expression + * @see LeetCode Problem 10 + */ +class RegularExpressionMatchingTest { + + @Test + void testBasicMatching() { + assertTrue(RegularExpressionMatching.isMatch("abc", "abc")); + assertFalse(RegularExpressionMatching.isMatch("abc", "abcd")); + assertFalse(RegularExpressionMatching.isMatch("abcd", "abc")); + } + + @Test + void testDotWildcard() { + assertTrue(RegularExpressionMatching.isMatch("abc", "a.c")); + assertTrue(RegularExpressionMatching.isMatch("axc", "a.c")); + assertFalse(RegularExpressionMatching.isMatch("abc", "a..")); + assertTrue(RegularExpressionMatching.isMatch("abc", "...")); + assertFalse(RegularExpressionMatching.isMatch("ab", "...")); + } + + @Test + void testStarQuantifier() { + assertTrue(RegularExpressionMatching.isMatch("aa", "a*")); + assertTrue(RegularExpressionMatching.isMatch("aaa", "a*")); + assertTrue(RegularExpressionMatching.isMatch("", "a*")); + assertFalse(RegularExpressionMatching.isMatch("b", "a*")); + assertTrue(RegularExpressionMatching.isMatch("aab", "c*a*b")); + assertTrue(RegularExpressionMatching.isMatch("b", "c*a*b")); + } + + @Test + void testDotStarCombination() { + assertTrue(RegularExpressionMatching.isMatch("abc", ".*")); + assertTrue(RegularExpressionMatching.isMatch("xyz", ".*")); + assertTrue(RegularExpressionMatching.isMatch("", ".*")); + assertTrue(RegularExpressionMatching.isMatch("abc123", ".*")); + assertTrue(RegularExpressionMatching.isMatch("abc", "a.*c")); + assertTrue(RegularExpressionMatching.isMatch("axxxc", "a.*c")); + assertFalse(RegularExpressionMatching.isMatch("abc", "a.*d")); + } + + @Test + void testComplexPatterns() { + assertTrue(RegularExpressionMatching.isMatch("mississippi", "mis*is*ip*.")); + assertTrue(RegularExpressionMatching.isMatch("mississippi", "mis*is*p*.")); + assertFalse(RegularExpressionMatching.isMatch("mississippi", "mis*is*ip*..")); + assertTrue(RegularExpressionMatching.isMatch("a", "a*a*a*")); + assertTrue(RegularExpressionMatching.isMatch("aaa", "a*a*a*")); + assertTrue(RegularExpressionMatching.isMatch("", "a*b*c*")); + } + + @Test + void testEdgeCases() { + assertTrue(RegularExpressionMatching.isMatch("", "")); + assertTrue(RegularExpressionMatching.isMatch("", "a*")); + assertTrue(RegularExpressionMatching.isMatch("", ".*")); + assertFalse(RegularExpressionMatching.isMatch("", "a")); + assertFalse(RegularExpressionMatching.isMatch("", ".")); + assertTrue(RegularExpressionMatching.isMatch("a", "a")); + assertTrue(RegularExpressionMatching.isMatch("a", ".")); + assertFalse(RegularExpressionMatching.isMatch("a", "b")); + assertFalse(RegularExpressionMatching.isMatch("a", "aa")); + } + + @Test + void testInvalidInputs() { + assertThrows(IllegalArgumentException.class, + () -> RegularExpressionMatching.isMatch(null, "pattern")); + assertThrows(IllegalArgumentException.class, + () -> RegularExpressionMatching.isMatch("string", null)); + assertThrows(IllegalArgumentException.class, + () -> RegularExpressionMatching.isMatch(null, null)); + assertThrows(IllegalArgumentException.class, + () -> RegularExpressionMatching.isMatch("test", "*abc")); + assertThrows(IllegalArgumentException.class, + () -> RegularExpressionMatching.isMatch("test", "a**b")); + } + + @Test + void testIterativeImplementation() { + assertTrue(RegularExpressionMatching.isMatchIterative("aa", "a*")); + assertTrue(RegularExpressionMatching.isMatchIterative("ab", ".*")); + assertFalse(RegularExpressionMatching.isMatchIterative("aa", "a")); + assertTrue(RegularExpressionMatching.isMatchIterative("aab", "c*a*b")); + + String[] testStrings = {"", "a", "aa", "ab", "aaa", "aab"}; + String[] testPatterns = {"", "a", "a*", ".*", "a.b", "c*a*b"}; + + for (String string : testStrings) { + for (String patternString : testPatterns) { + if (!patternString.isEmpty() && patternString.charAt(0) != '*') { + boolean recursiveResult = RegularExpressionMatching.isMatch(string, patternString); + boolean iterativeResult = RegularExpressionMatching.isMatchIterative(string, patternString); + assertTrue(recursiveResult == iterativeResult); + } + } + } + } + + @Test + void testLeetCodeExamples() { + assertFalse(RegularExpressionMatching.isMatch("aa", "a")); + assertTrue(RegularExpressionMatching.isMatch("aa", "a*")); + assertTrue(RegularExpressionMatching.isMatch("ab", ".*")); + assertTrue(RegularExpressionMatching.isMatch("aab", "c*a*b")); + assertFalse(RegularExpressionMatching.isMatch("mississippi", "mis*is*p*.")); + } +}