LeetCode 3407 - Substring Matching Pattern
The problem gives us two strings: - s, the source string. - p, a pattern string containing exactly one ''. The special character '' can represent any sequence of characters, including an empty sequence.
Difficulty: 🟢 Easy
Topics: String, String Matching
Solution
LeetCode 3407. Substring Matching Pattern
Problem Understanding
The problem gives us two strings:
s, the source string.p, a pattern string containing exactly one'*'.
The special character '*' can represent any sequence of characters, including an empty sequence. The goal is to determine whether there exists some substring of s that can match the pattern after replacing '*' with an arbitrary string.
It is important to notice that we are not matching the entire string s. We only need to find a substring of s that matches the pattern.
Since p contains exactly one '*', we can split it into two parts:
- A prefix before
'*' - A suffix after
'*'
For example:
"ee*e"becomes prefix"ee"and suffix"e""u*"becomes prefix"u"and suffix"""*abc"becomes prefix""and suffix"abc"
A substring matches the pattern if:
- It starts with the prefix.
- It ends with the suffix.
- Any characters between them are covered by
'*'.
The constraints are very small:
|s| ≤ 50|p| ≤ 50
This means even relatively inefficient solutions would pass. However, there is a very simple and elegant linear solution.
Several edge cases are worth noting:
- The prefix may be empty.
- The suffix may be empty.
'*'may represent an empty string.- The prefix and suffix may touch directly with no characters between them.
- The suffix must appear after the matched prefix occurrence.
Because the problem guarantees exactly one '*', we never need to handle multiple wildcards.
Approaches
Brute Force
A straightforward solution is to generate every possible substring of s.
For each substring:
- Check whether it starts with the prefix.
- Check whether it ends with the suffix.
- Verify that the substring is long enough to contain both parts.
If any substring satisfies these conditions, return true.
This works because every possible substring is examined, so any valid match will eventually be found.
However, there are O(n²) substrings, and checking each one may require O(n) work, resulting in O(n³) time in the worst case.
Key Insight
Since there is only one '*', the pattern structure is extremely simple.
Suppose:
p = prefix * suffix
A matching substring must contain:
prefix ... suffix
where the dots represent any sequence of characters, possibly empty.
Therefore, we only need to find:
- An occurrence of
prefix. - An occurrence of
suffixthat starts at or after the end of that prefix occurrence.
If such positions exist, then the substring from the beginning of the prefix to the end of the suffix forms a valid match.
Because the string lengths are tiny, we can simply locate the earliest valid prefix occurrence and then search for the suffix beginning at the end of that prefix.
Approach Comparison
| Approach | Time Complexity | Space Complexity | Notes |
|---|---|---|---|
| Brute Force | O(n³) | O(1) | Enumerate all substrings and validate each |
| Optimal | O(n) | O(1) | Split pattern into prefix and suffix, then locate them in order |
Algorithm Walkthrough
- Find the position of
'*'in the pattern. - Split the pattern into:
prefix = p[:star_index]suffix = p[star_index + 1:]
- Search for the first occurrence of
prefixinsides.
If no occurrence exists, return false immediately because every valid match must start with the prefix.
4. Let prefix_end be the index immediately after the matched prefix.
This represents the earliest position where the wildcard portion may begin.
5. Search for suffix inside s, starting from prefix_end.
This guarantees that the suffix appears after the prefix, allowing '*' to cover everything between them.
6. If such a suffix occurrence exists, return true.
7. Otherwise, return false.
Why it works
Let the pattern be:
prefix * suffix
Any valid matching substring must contain an occurrence of prefix followed later by an occurrence of suffix. The wildcard can absorb every character between those two occurrences, including zero characters.
Therefore, the existence of a valid match is equivalent to finding prefix and then finding suffix at or after the end of that prefix occurrence. The algorithm checks exactly this condition, so it is correct.
Problem Understanding
The problem is asking whether a given pattern string p, which contains exactly one asterisk '*', can be matched as a substring of another string s. The asterisk '*' acts as a wildcard that can represent any sequence of characters, including an empty sequence. The input s represents the string we are searching within, and p represents the pattern that may include this single wildcard. The output is a boolean value: true if there exists a substring of s that matches the pattern when the '*' is replaced by some sequence of characters, and false otherwise.
The constraints indicate that both s and p are small, with a maximum length of 50 characters. This means we can afford to perform operations that are quadratic in nature, but linear solutions are still preferable. The key edge cases involve patterns where the '*' is at the start, middle, or end of p, as well as cases where the pattern length exceeds the string length if we replace '*' with an empty string.
Important guarantees include the fact that p contains exactly one '*', so we do not need to handle multiple wildcards. The problem could trip up a naive implementation if we forget to account for the '*' matching zero characters or if we try to match the pattern without considering splitting it into the parts before and after the '*'.
Approaches
The brute-force approach would involve generating all possible substrings of s and attempting to match them with all possible expansions of the '*' in p. For each possible substring of s, we would replace '*' with sequences of characters to see if it matches exactly. This approach is correct but inefficient, because it generates too many possibilities and scales poorly with string length.
The key insight for an optimal solution is to treat the pattern as two fixed parts separated by the '*'. We can split p into a prefix (before '*') and a suffix (after '*'). Then, for any substring of s to match p, it must start with the prefix and end with the suffix. This allows us to slide the prefix along s and check if a corresponding suffix exists after it without generating all possible intermediate substrings.
| Approach | Time Complexity | Space Complexity | Notes |
|---|---|---|---|
| Brute Force | O(n^3) | O(n) | Generate all substrings of s and attempt all expansions of '*' |
| Optimal | O(n * m) | O(1) | Split pattern at '*' and match prefix and suffix directly |
Algorithm Walkthrough
- Split the pattern
pat the'*'into two strings:prefix(everything before'*') andsuffix(everything after'*'). This isolates the parts that must match exactly ins. - Iterate over all possible starting indices
iinswhere the substring could start matching theprefix. The maximum validiislen(s) - len(prefix)because the prefix must fit. - For each starting index
i, first check if the substring starting atimatches theprefix. - If the prefix matches, calculate the remaining length needed in
safteri + len(prefix)to match thesuffix. Check if the remaining characters ofsare sufficient and if the substring ending at the corresponding index matches thesuffix. - If both the prefix and suffix conditions are satisfied, return
truebecause a valid substring match exists. - If the loop finishes without returning, return
false, indicating no substring ofsmatches the patternp.
Why it works: The algorithm works because splitting at '*' reduces the problem to checking fixed substrings at the beginning and end of a potential match. Since '*' can represent any sequence, we only need to ensure the characters before and after it match exactly, and the characters in between can vary freely.
Python Solution
class Solution:
def hasMatch(self, s: str, p: str) -> bool:
star_index = p.index("*")
prefix = p[:star_index]
suffix = p[star_index + 1:]
prefix_pos = s.find(prefix)
if prefix_pos == -1:
return False
prefix_end = prefix_pos + len(prefix)
suffix_pos = s.find(suffix, prefix_end)
return suffix_pos != -1
The implementation begins by locating the wildcard and splitting the pattern into its fixed prefix and suffix portions.
The method find() is then used to locate the prefix within s. If the prefix does not exist, no valid match is possible.
Once the prefix is found, the algorithm computes the first position immediately after that prefix. Any characters between this position and the suffix can be matched by the wildcard.
Finally, another find() searches for the suffix beginning from that position. If such an occurrence exists, the pattern can be transformed into a substring of s, so the method returns True.
star_index = p.index('*')
prefix = p[:star_index]
suffix = p[star_index+1:]
n, m1, m2 = len(s), len(prefix), len(suffix)
for i in range(n - m1 + 1):
if s[i:i+m1] == prefix:
remaining_length = n - (i + m1)
if remaining_length >= m2 and s[i+m1:i+m1+m2] == suffix:
return True
return False
The implementation first finds the index of the `'*'` and splits the pattern into `prefix` and `suffix`. It then iterates over all possible starting positions in `s` where the prefix could fit. For each candidate start, it checks if the prefix matches exactly. If the prefix matches, it checks if the remaining substring can accommodate the suffix and if the suffix matches. The algorithm returns `true` if both match, otherwise it returns `false`.
## Go Solution
```go
func hasMatch(s string, p string) bool {
starIndex := -1
for i, ch := range p {
if ch == '*' {
starIndex = i
break
}
}
prefix := p[:starIndex]
suffix := p[starIndex+1:]
prefixPos := -1
for i := 0; i+len(prefix) <= len(s); i++ {
if s[i:i+len(prefix)] == prefix {
prefixPos = i
break
}
}
if prefixPos == -1 {
return false
}
prefixEnd := prefixPos + len(prefix)
for i := prefixEnd; i+len(suffix) <= len(s); i++ {
if s[i:i+len(suffix)] == suffix {
return true
}
}
return false
}
The Go solution follows the same logic as the Python version. Since Go does not provide a direct equivalent of Python's find() method on strings, the code performs simple substring scans manually.
There are no concerns about integer overflow because all lengths are at most 50. Empty prefixes and empty suffixes are handled naturally because slicing and comparisons with empty strings work correctly.
Worked Examples
Example 1
s = "leetcode"
p = "ee*e"
Split the pattern:
| Variable | Value |
|---|---|
| prefix | "ee" |
| suffix | "e" |
Find prefix:
| Operation | Result |
|---|---|
| s.find("ee") | 1 |
Compute:
| Variable | Value |
|---|---|
| prefix_end | 3 |
Search for suffix from index 3:
| Operation | Result |
|---|---|
| s.find("e", 3) | 7 |
Since a suffix occurrence exists, return:
true
Matching substring:
eetcode
Example 2
s = "car"
p = "c*v"
Split:
| Variable | Value |
|---|---|
| prefix | "c" |
| suffix | "v" |
Find prefix:
| Operation | Result |
|---|---|
| s.find("c") | 0 |
Compute:
| Variable | Value |
|---|---|
| prefix_end | 1 |
Search for suffix:
| Operation | Result |
|---|---|
| s.find("v", 1) | -1 |
No suffix exists after the prefix.
false
Example 3
s = "luck"
p = "u*"
Split:
| Variable | Value |
|---|---|
| prefix | "u" |
| suffix | "" |
Find prefix:
| Operation | Result |
|---|---|
| s.find("u") | 1 |
Compute:
| Variable | Value |
|---|---|
| prefix_end | 2 |
Search for empty suffix:
| Operation | Result |
|---|---|
| s.find("", 2) | 2 |
An empty suffix always matches.
true
starIndex := -1
for i, ch := range p {
if ch == '*' {
starIndex = i
break
}
}
prefix := p[:starIndex]
suffix := p[starIndex+1:]
n, m1, m2 := len(s), len(prefix), len(suffix)
for i := 0; i <= n-m1; i++ {
if s[i:i+m1] == prefix {
remainingLength := n - (i + m1)
if remainingLength >= m2 && s[i+m1:i+m1+m2] == suffix {
return true
}
}
}
return false
}
In Go, the algorithm is nearly identical. We iterate to find the `'*'` index manually and slice the strings accordingly. Go requires explicit bounds checking and slicing syntax, but the logic mirrors the Python solution.
## Worked Examples
**Example 1: s = "leetcode", p = "ee*e"**
| Step | i | prefix match? | remaining_length | suffix match? | Result |
| --- | --- | --- | --- | --- | --- |
| 1 | 0 | "le" != "ee" | - | - | continue |
| 2 | 1 | "ee" == "ee" | 6 - (1+2)=3 | "cod" != "e" | continue |
| 3 | 2 | "et" != "ee" | - | - | continue |
| 4 | 3 | "tc" != "ee" | - | - | continue |
| 5 | 4 | "co" != "ee" | - | - | continue |
| 6 | 5 | "od" != "ee" | - | - | continue |
| 7 | 6 | "de" != "ee" | - | - | continue |
| 8 | 1 (prefix match at i=1) | suffix "e" matches at i+len(prefix)+len(*) | true | | |
**Example 2: s = "car", p = "c*v"**
No index satisfies both prefix "c" and suffix "v", returns false.
__Example 3: s = "luck", p = "u_"_*
Prefix "u" matches at index 1, suffix is empty, returns true.
## Complexity Analysis
| Measure | Complexity | Explanation |
| --- | --- | --- |
| Time | O(n) | Each search scans the string at most once |
| Space | O(1) | Only a few variables are stored |
Although string-search implementations may internally use different algorithms, with the given constraints the overall work is linear in the length of `s`. No auxiliary data structures proportional to the input size are required.
| Time | O(n * m) | We iterate through each position in `s` for prefix matching, and suffix check is O(m) where m is length of suffix |
| Space | O(1) | Only a few integer and substring variables are used, no additional data structures |
The complexity is reasonable given n, m <= 50.
## Test Cases
sol = Solution()
assert sol.hasMatch("leetcode", "eee") is True # example 1 assert sol.hasMatch("car", "cv") is False # example 2 assert sol.hasMatch("luck", "u*") is True # example 3
assert sol.hasMatch("abc", "*") is True # wildcard matches empty substring assert sol.hasMatch("abc", "c") is True # empty prefix assert sol.hasMatch("abc", "a") is True # empty suffix
assert sol.hasMatch("abc", "abc") is True # wildcard matches empty string assert sol.hasMatch("abc", "ac") is True # wildcard matches middle character
assert sol.hasMatch("abc", "d*") is False # prefix missing assert sol.hasMatch("abc", "*d") is False # suffix missing
assert sol.hasMatch("aaaa", "aaa") is True # repeated characters assert sol.hasMatch("abcd", "bcd") is True # match inside string
assert sol.hasMatch("abcd", "cda") is False # suffix cannot appear after prefix assert sol.hasMatch("a", "a") is True # minimum length source string assert sol.hasMatch("a", "*a") is True # minimum length source string
assert sol.hasMatch("abcde", "abde") is True # long exact span assert sol.hasMatch("abcde", "abf") is False # suffix absent
### Test Summary
| Test | Why |
| --- | --- |
| `"leetcode", "ee*e"` | Official example |
| `"car", "c*v"` | Official negative example |
| `"luck", "u*"` | Official example with empty suffix |
| `"abc", "*"` | Wildcard only |
| `"abc", "*c"` | Empty prefix |
| `"abc", "a*"` | Empty suffix |
| `"abc", "ab*c"` | Wildcard matches empty string |
| `"abc", "a*c"` | Wildcard matches non-empty string |
| `"abc", "d*"` | Missing prefix |
| `"abc", "*d"` | Missing suffix |
| `"aaaa", "aa*a"` | Repeated characters |
| `"abcd", "bc*d"` | Internal substring match |
| `"abcd", "cd*a"` | Incorrect ordering |
| `"a", "a*"` | Smallest source string |
| `"a", "*a"` | Smallest source string with empty prefix |
| `"abcde", "ab*de"` | Full span match |
| `"abcde", "ab*f"` | Suffix absent |
## Edge Cases
### Empty Prefix
Patterns such as `"*abc"` have no fixed prefix. A common bug is assuming a prefix always exists and attempting to search for it.
In the implementation, `prefix` becomes an empty string. Python's `find("")` returns `0`, which correctly indicates that matching may begin at the start of the string. The algorithm therefore works without any special handling.
### Empty Suffix
Patterns such as `"abc*"` have no fixed suffix. Any substring beginning with `"abc"` should be considered valid.
The implementation searches for an empty string after the prefix. Since an empty string is always found at the search position, the algorithm correctly returns `True` whenever the prefix exists.
### Wildcard Matching an Empty Sequence
The problem explicitly allows `'*'` to represent zero characters. Patterns like `"ab*c"` should therefore match `"abc"`.
The algorithm searches for the suffix beginning exactly at `prefix_end`. If the suffix starts immediately after the prefix, the gap length is zero, which naturally represents the wildcard matching an empty sequence.
### Repeated Character Ambiguity
Strings such as `"aaaa"` and patterns such as `"aa*a"` can have many possible prefix and suffix occurrences. A careless implementation might pair the wrong occurrences.
The algorithm only needs to find one valid ordering where the suffix appears at or after the end of a prefix occurrence. As soon as such an arrangement exists, a matching substring exists, so the method correctly returns `True`.
# provided examples
assert Solution().hasMatch("leetcode", "ee*e") == True # Example 1
assert Solution().hasMatch("car", "c*v") == False # Example 2
assert Solution().hasMatch("luck", "u*") == True # Example 3
# boundary tests
assert Solution().hasMatch("a", "*") == True # single char s, pattern is only '*'
assert Solution().hasMatch("abc", "a*c") == True # '*' in middle
assert Solution().hasMatch("abc", "abc*") == True # '*' at end
assert Solution().hasMatch("abc", "*abc") == True # '*' at start
assert Solution().hasMatch("abc", "d*") == False # no match
assert Solution().hasMatch("aaa", "a*a") == True # repeating chars
| Test | Why |
|---|---|
| "leetcode", "ee*e" | Checks normal prefix and suffix match |
| "car", "c*v" | Ensures false is returned when no match |
| "luck", "u*" | Checks matching with empty suffix |
| "a", "*" | Single character and only wildcard |
| "abc", "a*c" | Wildcard in middle |
| "abc", "abc*" | Wildcard at end |
| "abc", "*abc" | Wildcard at start |
| "d*", "abc" | Non-matching prefix |
| "aaa", "a*a" | Repeating characters |
Edge Cases
The first edge case is when