Facebook Pixel

3407. Substring Matching Pattern

EasyStringString Matching
Leetcode Link

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" and p = "a*f", the answer is true because we can replace * with "bcde" to get "abcdef", which matches s.
  • If s = "abcdef" and p = "a*c", the answer is true because we can replace * with "b" to get "abc", which is a substring of s.
  • If s = "abcdef" and p = "x*y", the answer is false because no replacement of * can make p a substring of s.

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.

Quick Interview Experience
Help others by sharing your interview experience
Have you seen this problem before?

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:

  1. Split the pattern by * to get the fixed parts (prefix and suffix)
  2. Search for the first part in s
  3. If found, search for the second part starting from after where the first part ended
  4. 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:

  1. 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 *).

  2. Initialize position tracker: We maintain a variable i that tracks our current position in string s. This ensures we search for pattern parts in the correct order from left to right.

  3. Sequential matching: We iterate through each part obtained from splitting:

    • For each part t, we use s.find(t, i) to search for it in s starting from position i
    • The find() method returns the index of the first occurrence of t starting from position i, or -1 if not found
  4. Validation check: If any part returns -1 (not found), we immediately return False since the pattern cannot match.

  5. Update position: After successfully finding a part at position j, we update i = j + len(t). This moves our search position to just after the matched part, ensuring the next part must appear after the current one.

  6. Return result: If we successfully find all parts in order (the loop completes without returning False), we return True.

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 Evaluator

Example 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 of s)

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 return True (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...
Discover Your Strengths and Weaknesses: Take Our 3-Minute Quiz to Tailor Your Study Plan:

Which technique can we use to find the middle of a linked list?


Recommended Readings

Want a Structured Path to Master System Design Too? Don’t Miss This!

Load More