Skip to content
Closed
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
Original file line number Diff line number Diff line change
@@ -0,0 +1,179 @@
package com.thealgorithms.dynamicprogramming;

/**
* Implements regular expression matching with support for '.' and '*'.
*
* <p>
* 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.
*
* <p>
* This solution uses dynamic programming with memoization for efficient computation.
*
* <p>
* For more information:
* @see <a href="https://en.wikipedia.org/wiki/Regular_expression">Regular Expression</a>
* @see <a href="https://leetcode.com/problems/regular-expression-matching/">LeetCode Problem 10</a>
*
* <p>
* Example:
* <pre>
* Input: s = "aa", p = "a" → Output: false
* Input: s = "aa", p = "a*" → Output: true
* Input: s = "ab", p = ".*" → Output: true
* </pre>
*
* <p>
* 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];
}
}
122 changes: 122 additions & 0 deletions src/test/java/com/thealgorithms/RegularExpressionMatchingTest.java
Original file line number Diff line number Diff line change
@@ -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.
*
* <p>
* For more information about regular expression matching:
* @see <a href="https://en.wikipedia.org/wiki/Regular_expression">Regular Expression</a>
* @see <a href="https://leetcode.com/problems/regular-expression-matching/">LeetCode Problem 10</a>
*/
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*."));
}
}
Loading