From 6b37d04938493ee9071a6e1446271105ed053fd6 Mon Sep 17 00:00:00 2001 From: Ricardo Ribeiro Rodrigues <72521349+RicardoRibeiroRodrigues@users.noreply.github.com> Date: Tue, 3 Sep 2024 08:45:06 -0300 Subject: [PATCH] Add Wildcard Pattern Matching (#470) --- .../PatternMatching/WildCardMatcherTests.cs | 36 +++++++ .../PatternMatching/WildCardMatcher.cs | 97 +++++++++++++++++++ README.md | 1 + 3 files changed, 134 insertions(+) create mode 100644 Algorithms.Tests/Strings/PatternMatching/WildCardMatcherTests.cs create mode 100644 Algorithms/Strings/PatternMatching/WildCardMatcher.cs diff --git a/Algorithms.Tests/Strings/PatternMatching/WildCardMatcherTests.cs b/Algorithms.Tests/Strings/PatternMatching/WildCardMatcherTests.cs new file mode 100644 index 00000000..5361557a --- /dev/null +++ b/Algorithms.Tests/Strings/PatternMatching/WildCardMatcherTests.cs @@ -0,0 +1,36 @@ +using Algorithms.Strings.PatternMatching; +using NUnit.Framework; + +namespace Algorithms.Tests.Strings.PatternMatching; + +public static class WildCardMatcherTests +{ + [TestCase("aab", "c*a*b", true)] + [TestCase("aaa", "aa", false)] + [TestCase("aaa", "a.a", true)] + [TestCase("aaab", "aa*", false)] + [TestCase("aaab", ".*", true)] + [TestCase("a", "bbbb", false)] + [TestCase("", "bbbb", false)] + [TestCase("a", "", false)] + [TestCase("", "", true)] + public static void MatchPattern(string inputString, string pattern, bool expected) + { + // Act + var result = WildCardMatcher.MatchPattern(inputString, pattern); + + // Assert + Assert.That(result, Is.EqualTo(expected)); + } + + [Test] + public static void MatchPatternThrowsArgumentException() + { + // Arrange + var inputString = "abc"; + var pattern = "*abc"; + + // Assert + Assert.Throws(() => WildCardMatcher.MatchPattern(inputString, pattern)); + } +} diff --git a/Algorithms/Strings/PatternMatching/WildCardMatcher.cs b/Algorithms/Strings/PatternMatching/WildCardMatcher.cs new file mode 100644 index 00000000..cf15ce21 --- /dev/null +++ b/Algorithms/Strings/PatternMatching/WildCardMatcher.cs @@ -0,0 +1,97 @@ +using System; + +namespace Algorithms.Strings.PatternMatching; + +/// +/// Implentation of regular expression matching with support for '.' and '*'. +/// '.' Matches any single character. +/// '*' Matches zero or more of the preceding element. +/// The matching should cover the entire input string (not partial). +/// +public static class WildCardMatcher +{ + /// + /// Using bottom-up dynamic programming for matching the input string with the pattern. + /// + /// Time complexity: O(n*m), where n is the length of the input string and m is the length of the pattern. + /// + /// Constrain: The pattern cannot start with '*'. + /// + /// The input string to match. + /// The pattern to match. + /// True if the input string matches the pattern, false otherwise. + /// Thrown when the pattern starts with '*'. + public static bool MatchPattern(string inputString, string pattern) + { + if (pattern.Length > 0 && pattern[0] == '*') + { + throw new ArgumentException("Pattern cannot start with *"); + } + + var inputLength = inputString.Length + 1; + var patternLength = pattern.Length + 1; + + // DP 2d matrix, where dp[i, j] is true if the first i characters in the input string match the first j characters in the pattern + // This DP is initialized to all falses, as it is the default value for a boolean. + var dp = new bool[inputLength, patternLength]; + + // Empty string and empty pattern are a match + dp[0, 0] = true; + + // Since the empty string can only match a pattern that has a * in it, we need to initialize the first row of the DP matrix + for (var j = 1; j < patternLength; j++) + { + if (pattern[j - 1] == '*') + { + dp[0, j] = dp[0, j - 2]; + } + } + + // Now using bottom-up approach to find for all remaining lenghts of input and pattern + for (var i = 1; i < inputLength; i++) + { + for (var j = 1; j < patternLength; j++) + { + MatchRemainingLenghts(inputString, pattern, dp, i, j); + } + } + + return dp[inputLength - 1, patternLength - 1]; + } + + // Helper method to match the remaining lengths of the input string and the pattern + // This method is called for all i and j where i > 0 and j > 0 + private static void MatchRemainingLenghts(string inputString, string pattern, bool[,] dp, int i, int j) + { + // If the characters match or the pattern has a ., then the result is the same as the previous positions. + if (inputString[i - 1] == pattern[j - 1] || pattern[j - 1] == '.') + { + dp[i, j] = dp[i - 1, j - 1]; + } + else if (pattern[j - 1] == '*') + { + MatchForZeroOrMore(inputString, pattern, dp, i, j); + } + else + { + // If the characters do not match, then the result is false, which is the default value. + } + } + + // Helper method to match for the "*" pattern. + private static void MatchForZeroOrMore(string inputString, string pattern, bool[,] dp, int i, int j) + { + if (dp[i, j - 2]) + { + dp[i, j] = true; + } + else if (inputString[i - 1] == pattern[j - 2] || pattern[j - 2] == '.') + { + dp[i, j] = dp[i - 1, j]; + } + else + { + // Leave the default value of false + } + } +} diff --git a/README.md b/README.md index fd4f1942..ef5a1cfd 100644 --- a/README.md +++ b/README.md @@ -191,6 +191,7 @@ find more than one implementation for the same objective but using different alg * [Rabin Karp](./Algorithms/Strings/PatternMatching/RabinKarp.cs) * [Boyer Moore](./Algorithms/Strings/PatternMatching/BoyerMoore.cs) * [Knuth–Morris–Pratt Search](./Algorithms/Strings/PatternMatching/KnuthMorrisPrattSearcher.cs) + * [WildCard Pattern Matching](./Algorithms/Strings/PatternMatching/WildCardMatcher.cs) * [Z-block substring search](./Algorithms/Strings/PatternMatching/ZblockSubstringSearch.cs) * [Longest Consecutive Character](./Algorithms/Strings/GeneralStringAlgorithms.cs) * [Palindrome Checker](./Algorithms/Strings/Palindrome.cs)