3407. Substring Matching Pattern
Problem Description
You are given two strings: a string s
and a pattern string p
that contains exactly one asterisk (*
) character.
The asterisk in pattern p
acts as a wildcard that can match any sequence of characters (including an empty sequence). Your task is to determine whether the pattern p
can be found as a substring within string s
after replacing the *
with appropriate characters.
For example:
- If
s = "abcdef"
andp = "a*f"
, the answer istrue
because we can replace*
with"bcde"
to get"abcdef"
, which matchess
. - If
s = "abcdef"
andp = "a*c"
, the answer istrue
because we can replace*
with"b"
to get"abc"
, which is a substring ofs
. - If
s = "abcdef"
andp = "x*y"
, the answer isfalse
because no replacement of*
can makep
a substring ofs
.
The solution works by splitting the pattern p
at the *
character, which gives us the prefix and suffix that must appear in order within string s
. It searches for the prefix first, then searches for the suffix starting from after where the prefix was found. If both parts are found in the correct order, the pattern can match a substring of s
.
Intuition
When we see a pattern with a single *
wildcard, we can think of it as dividing the pattern into two parts: everything before the *
and everything after the *
. For the pattern to match a substring of s
, these two parts must appear in s
in the correct order.
The key insight is that the *
can represent any sequence of characters, but it doesn't change the fact that the parts before and after it must still exist in s
in their exact form and in the right sequence. We don't actually need to figure out what the *
represents - we just need to verify that the fixed parts of the pattern can be found.
Think of it like looking for landmarks on a road: if pattern p = "abc*xyz"
, we're essentially asking "Can we find 'abc' somewhere in s
, and then find 'xyz' somewhere after that?" The *
just represents whatever distance or characters exist between these two landmarks.
This leads us to a simple greedy approach:
- Split the pattern by
*
to get the fixed parts (prefix and suffix) - Search for the first part in
s
- If found, search for the second part starting from after where the first part ended
- If both parts are found in order, the pattern can match
The beauty of this approach is that we're always looking for the earliest possible match for each part, which maximizes our chances of finding all required parts in sequence. If we can't find the parts in this greedy manner, then no valid match exists.
Solution Approach
The implementation uses a straightforward string matching approach with Python's built-in string methods:
-
Split the pattern: We use
p.split("*")
to divide the pattern into parts separated by the asterisk. Since there's exactly one*
, this gives us a list with two elements - the prefix (before*
) and the suffix (after*
). -
Initialize position tracker: We maintain a variable
i
that tracks our current position in strings
. This ensures we search for pattern parts in the correct order from left to right. -
Sequential matching: We iterate through each part obtained from splitting:
- For each part
t
, we uses.find(t, i)
to search for it ins
starting from positioni
- The
find()
method returns the index of the first occurrence oft
starting from positioni
, or-1
if not found
- For each part
-
Validation check: If any part returns
-1
(not found), we immediately returnFalse
since the pattern cannot match. -
Update position: After successfully finding a part at position
j
, we updatei = j + len(t)
. This moves our search position to just after the matched part, ensuring the next part must appear after the current one. -
Return result: If we successfully find all parts in order (the loop completes without returning
False
), we returnTrue
.
The algorithm has a time complexity of O(n * m)
where n
is the length of s
and m
is the length of p
, due to the string searching operations. The space complexity is O(m)
for storing the split parts of the pattern.
This greedy approach works because we only need to verify that the fixed parts of the pattern exist in the correct order - we don't need to explicitly determine what the *
represents.
Ready to land your dream job?
Unlock your dream job with a 3-minute evaluator for a personalized learning plan!
Start EvaluatorExample Walkthrough
Let's walk through the solution with s = "abcdefgh"
and p = "bc*fg"
.
Step 1: Split the pattern by '*'
p.split("*")
gives us["bc", "fg"]
- So we have prefix = "bc" and suffix = "fg"
Step 2: Initialize position tracker
- Set
i = 0
(start searching from the beginning ofs
)
Step 3: Search for first part ("bc")
- Call
s.find("bc", 0)
to search for "bc" starting from position 0 - Found "bc" at index 1 in "abcdefgh"
- Update
i = 1 + len("bc") = 1 + 2 = 3
Step 4: Search for second part ("fg")
- Call
s.find("fg", 3)
to search for "fg" starting from position 3 - Found "fg" at index 5 in "abcdefgh"
- Update
i = 5 + len("fg") = 5 + 2 = 7
Step 5: All parts found successfully
- Return
True
The pattern matches because we can replace *
with "de" to get "bcdefg", which is a substring of "abcdefgh" (from index 1 to 6).
Counter-example: If p = "bc*xy"
with the same s = "abcdefgh"
:
- Split gives us
["bc", "xy"]
- Find "bc" at index 1, update
i = 3
- Search for "xy" starting from position 3:
s.find("xy", 3)
returns-1
(not found) - Return
False
immediately
This demonstrates how the algorithm ensures parts appear in the correct order and fails fast when a required part cannot be found.
Solution Implementation
1class Solution:
2 def hasMatch(self, s: str, p: str) -> bool:
3 """
4 Check if pattern p (with * wildcards) matches string s.
5 The * can match zero or more characters.
6
7 Args:
8 s: The target string to match against
9 p: The pattern string containing * wildcards
10
11 Returns:
12 True if pattern matches the string, False otherwise
13 """
14 # Current position in string s where we start searching
15 current_position = 0
16
17 # Split pattern by * to get fixed segments that must appear in order
18 pattern_segments = p.split("*")
19
20 # Check each segment appears in the string in the correct order
21 for segment in pattern_segments:
22 # Find the next occurrence of this segment starting from current position
23 found_position = s.find(segment, current_position)
24
25 # If segment not found, pattern doesn't match
26 if found_position == -1:
27 return False
28
29 # Move current position past this matched segment
30 current_position = found_position + len(segment)
31
32 # All segments found in order - pattern matches
33 return True
34
1class Solution {
2 public boolean hasMatch(String s, String p) {
3 // Split pattern by wildcard '*' to get segments that must appear in order
4 String[] segments = p.split("\\*");
5
6 // Track current position in the string
7 int currentPosition = 0;
8
9 // Iterate through each segment of the pattern
10 for (int segmentIndex = 0; segmentIndex < segments.length; segmentIndex++) {
11 String segment = segments[segmentIndex];
12
13 // Skip empty segments (occurs with consecutive wildcards or wildcards at boundaries)
14 if (segment.isEmpty()) {
15 continue;
16 }
17
18 // First segment must match from the beginning if pattern doesn't start with '*'
19 if (segmentIndex == 0 && !p.startsWith("*")) {
20 if (!s.startsWith(segment)) {
21 return false;
22 }
23 currentPosition = segment.length();
24 }
25 // Last segment must match to the end if pattern doesn't end with '*'
26 else if (segmentIndex == segments.length - 1 && !p.endsWith("*")) {
27 if (!s.endsWith(segment) || s.lastIndexOf(segment) < currentPosition) {
28 return false;
29 }
30 currentPosition = s.length();
31 }
32 // Middle segments or segments with wildcards on both sides
33 else {
34 // Find the next occurrence of this segment starting from current position
35 int foundIndex = s.indexOf(segment, currentPosition);
36
37 // If segment not found, pattern doesn't match
38 if (foundIndex == -1) {
39 return false;
40 }
41
42 // Update position to after the found segment
43 currentPosition = foundIndex + segment.length();
44 }
45 }
46
47 // Pattern matches successfully
48 return true;
49 }
50}
51
1class Solution {
2public:
3 bool hasMatch(string s, string p) {
4 // Initialize pointers for tracking position in string s
5 int searchStartPos = 0;
6
7 // Initialize variables for pattern parsing
8 int patternStartIdx = 0;
9 int starIdx;
10
11 // Process each segment of pattern p separated by '*'
12 while ((starIdx = p.find("*", patternStartIdx)) != string::npos) {
13 // Extract the substring between current position and next '*'
14 string patternSegment = p.substr(patternStartIdx, starIdx - patternStartIdx);
15
16 // Search for this pattern segment in string s starting from searchStartPos
17 int foundPos = s.find(patternSegment, searchStartPos);
18
19 // If pattern segment not found, no match possible
20 if (foundPos == string::npos) {
21 return false;
22 }
23
24 // Move search position past the matched segment
25 searchStartPos = foundPos + patternSegment.length();
26
27 // Move pattern pointer past the '*'
28 patternStartIdx = starIdx + 1;
29 }
30
31 // Process the remaining pattern after the last '*' (or entire pattern if no '*')
32 string remainingPattern = p.substr(patternStartIdx);
33
34 // Check if remaining pattern exists in string s
35 int finalMatchPos = s.find(remainingPattern, searchStartPos);
36
37 // If remaining pattern not found, return false
38 if (finalMatchPos == string::npos) {
39 return false;
40 }
41
42 // Match found for all pattern segments
43 return true;
44 }
45};
46
1/**
2 * Checks if string s matches pattern p where '*' acts as a wildcard
3 * that can match zero or more characters
4 * @param s - The source string to match against
5 * @param p - The pattern string containing wildcards
6 * @returns true if the pattern matches the string, false otherwise
7 */
8function hasMatch(s: string, p: string): boolean {
9 // Current position in the source string
10 let currentIndex = 0;
11
12 // Split pattern by '*' to get literal segments that must appear in order
13 const patternSegments = p.split('*');
14
15 // Check each segment appears in the source string in the correct order
16 for (const segment of patternSegments) {
17 // Find the next occurrence of this segment starting from current position
18 const foundIndex = s.indexOf(segment, currentIndex);
19
20 // If segment not found, pattern doesn't match
21 if (foundIndex === -1) {
22 return false;
23 }
24
25 // Move current position past the matched segment
26 currentIndex = foundIndex + segment.length;
27 }
28
29 // All segments found in order, pattern matches
30 return true;
31}
32
Time and Space Complexity
Time Complexity: O(n * m)
The algorithm splits the pattern p
by the wildcard character *
, which takes O(m)
time where m
is the length of the pattern. For each substring segment from the split (let's say there are k
segments), we perform a find()
operation on string s
. The find()
method has a worst-case time complexity of O(n)
where n
is the length of string s
. In the worst case, we might need to search through most of the string for each segment. Since the total length of all segments is at most m
, and each segment requires potentially O(n)
time to find, the overall time complexity is O(n * k)
where k ≤ m
. This simplifies to O(n * m)
in the worst case.
Space Complexity: O(m)
The split("*")
operation creates a list of substrings from the pattern p
. In the worst case where there are no wildcards, this list contains one string of length m
. When there are wildcards, the total length of all substrings still sums to at most m
(excluding the wildcard characters). Therefore, the space complexity is O(m)
for storing the split results. The remaining variables (i
, j
, t
) use constant space O(1)
.
Learn more about how to find time and space complexity quickly.
Common Pitfalls
Pitfall 1: Not Handling Empty Pattern Segments
The current solution doesn't properly handle cases where the pattern has empty segments (i.e., when *
appears at the beginning or end of the pattern).
Problematic Case:
s = "abc"
,p = "*abc"
- The split results in["", "abc"]
s = "abc"
,p = "abc*"
- The split results in["abc", ""]
The issue is that s.find("", i)
always returns i
(finding an empty string always succeeds at the current position), which might give incorrect results for validation logic.
Solution: Skip empty segments or handle them explicitly:
def hasMatch(self, s: str, p: str) -> bool:
current_position = 0
pattern_segments = p.split("*")
for segment in pattern_segments:
# Skip empty segments (when * is at beginning/end)
if not segment:
continue
found_position = s.find(segment, current_position)
if found_position == -1:
return False
current_position = found_position + len(segment)
return True
Pitfall 2: Incorrect Assumption About Substring Matching
The problem states we need to check if the pattern can be found as a substring within string s
, not if it matches the entire string. The current solution actually checks if the pattern segments appear in order within s
, which is correct for substring matching. However, developers might mistakenly think they need to match the entire string.
Clarification Example:
s = "xxxabcyyy"
,p = "a*c"
should returnTrue
(matches substring "abc")- A common mistake would be thinking this should return
False
because the pattern doesn't match the entire string
Pitfall 3: Multiple Asterisks Assumption
While the problem states there's exactly one asterisk, the code using split("*")
would incorrectly handle multiple asterisks without validation.
Solution: Add validation if needed:
def hasMatch(self, s: str, p: str) -> bool:
# Validate exactly one asterisk
if p.count("*") != 1:
raise ValueError("Pattern must contain exactly one asterisk")
# Rest of the implementation...
Which technique can we use to find the middle of a linked list?
Recommended Readings
Coding Interview Patterns Your Personal Dijkstra's Algorithm to Landing Your Dream Job The goal of AlgoMonster is to help you get a job in the shortest amount of time possible in a data driven way We compiled datasets of tech interview problems and broke them down by patterns This way
Recursion Recursion is one of the most important concepts in computer science Simply speaking recursion is the process of a function calling itself Using a real life analogy imagine a scenario where you invite your friends to lunch https assets algo monster recursion jpg You first call Ben and ask
Runtime Overview When learning about algorithms and data structures you'll frequently encounter the term time complexity This concept is fundamental in computer science and offers insights into how long an algorithm takes to complete given a certain input size What is Time Complexity Time complexity represents the amount of time
Want a Structured Path to Master System Design Too? Don’t Miss This!