← Back to DevBytes

Solving Implement strStr() in Python: Step-by-Step Guide

Introduction to Implement strStr()

The strStr() problem is one of the most fundamental string matching challenges in computer science. Originally popularized by LeetCode, it asks you to implement a function that finds the first occurrence of a substring (needle) within a larger string (haystack) and returns its starting index. If the needle is not found, the function should return -1.

This problem is essentially asking you to reimplement Python's built-in str.find() method from scratch. While Python provides this functionality natively, understanding how to implement it manually is crucial for mastering string algorithms and performing well in technical interviews.

Why This Problem Matters

You might wonder why we need to implement something that Python already does efficiently. The answer lies in what this problem teaches you about algorithmic thinking and string manipulation.

Understanding the Problem Statement

Before diving into solutions, let's clearly define what we need to implement. Given two strings, haystack and needle, return the index of the first occurrence of needle in haystack. If needle is an empty string, return 0. If needle is not found, return -1.

Here are some example inputs and expected outputs:

Solution 1: Brute Force Approach

The most straightforward approach is to check every possible starting position in the haystack where the needle could begin. For each position, compare characters one by one until either a mismatch is found or the entire needle matches.

This approach has a time complexity of O(n × m) where n is the length of the haystack and m is the length of the needle. The space complexity is O(1) since we only use a few variables.

def strStr(haystack: str, needle: str) -> int:
    # Handle empty needle case
    if not needle:
        return 0
    
    n = len(haystack)
    m = len(needle)
    
    # If needle is longer than haystack, it cannot be found
    if m > n:
        return -1
    
    # Check every possible starting position
    for i in range(n - m + 1):
        # Try to match needle starting at position i
        match = True
        for j in range(m):
            if haystack[i + j] != needle[j]:
                match = False
                break
        
        if match:
            return i
    
    return -1

# Test the function
print(strStr("hello", "ll"))      # Output: 2
print(strStr("aaaaa", "bba"))     # Output: -1
print(strStr("abc", ""))          # Output: 0
print(strStr("mississippi", "issip"))  # Output: 4

The brute force solution works well for small strings and is easy to understand. However, it can be inefficient for large inputs because it may repeatedly compare characters that have already been matched.

Solution 2: Using Python Slicing

Python's string slicing provides a more elegant way to implement the brute force approach. Instead of comparing characters one by one, we can extract a substring of the same length as the needle and compare it directly.

def strStr(haystack: str, needle: str) -> int:
    if not needle:
        return 0
    
    n = len(haystack)
    m = len(needle)
    
    if m > n:
        return -1
    
    for i in range(n - m + 1):
        # Compare slice of haystack with needle
        if haystack[i:i + m] == needle:
            return i
    
    return -1

# Test the function
print(strStr("hello", "ll"))   # Output: 2
print(strStr("world", "or"))   # Output: 1
print(strStr("python", "th"))  # Output: 2

This version is more Pythonic and easier to read. While the theoretical time complexity remains O(n × m), Python's built-in string comparison is implemented in C and is significantly faster than manual character-by-character comparison in pure Python.

Solution 3: Using Python's Built-in find()

In a real-world scenario, the most practical solution is to use Python's built-in find() method. This is what you should use in production code unless you have specific reasons to implement your own.

def strStr(haystack: str, needle: str) -> int:
    return haystack.find(needle)

# Test the function
print(strStr("hello", "ll"))      # Output: 2
print(strStr("aaaaa", "bba"))     # Output: -1
print(strStr("abc", ""))          # Output: 0
print(strStr("", ""))             # Output: 0

The find() method handles all edge cases automatically, including empty strings, and returns -1 when the substring is not found. It is also highly optimized at the C level, making it the fastest option in practice.

Solution 4: KMP Algorithm

For large inputs, the Knuth-Morris-Pratt (KMP) algorithm offers better performance with a time complexity of O(n + m). The key insight behind KMP is that when a mismatch occurs, we can use information about previously matched characters to skip unnecessary comparisons.

KMP works by precomputing a partial match table (also called the failure function or LPS array) that tells us how far to shift the needle when a mismatch occurs. This avoids re-examining characters that we already know will match.

def strStr(haystack: str, needle: str) -> int:
    if not needle:
        return 0
    
    # Build the LPS (Longest Proper Prefix which is also Suffix) array
    def buildLPS(pattern: str) -> list:
        m = len(pattern)
        lps = [0] * m
        length = 0  # Length of the previous longest prefix suffix
        i = 1
        
        while i < m:
            if pattern[i] == pattern[length]:
                length += 1
                lps[i] = length
                i += 1
            else:
                if length != 0:
                    length = lps[length - 1]
                else:
                    lps[i] = 0
                    i += 1
        
        return lps
    
    n = len(haystack)
    m = len(needle)
    
    if m > n:
        return -1
    
    lps = buildLPS(needle)
    
    i = 0  # Index for haystack
    j = 0  # Index for needle
    
    while i < n:
        if haystack[i] == needle[j]:
            i += 1
            j += 1
            
            if j == m:
                return i - j  # Found the needle
        else:
            if j != 0:
                j = lps[j - 1]
            else:
                i += 1
    
    return -1

# Test the function
print(strStr("ABABDABACDABABCABAB", "ABABCABAB"))  # Output: 10
print(strStr("hello", "ll"))                        # Output: 2
print(strStr("mississippi", "issip"))               # Output: 4

The KMP algorithm is more complex but significantly more efficient for large strings with repetitive patterns. The LPS array construction takes O(m) time, and the search phase takes O(n) time, giving us a total time complexity of O(n + m).

Solution 5: Rabin-Karp Algorithm

The Rabin-Karp algorithm uses hashing to find the needle in the haystack. Instead of comparing characters directly, it compares hash values. When hash values match, it then verifies the actual characters to avoid false positives due to hash collisions.

def strStr(haystack: str, needle: str) -> int:
    if not needle:
        return 0
    
    n = len(haystack)
    m = len(needle)
    
    if m > n:
        return -1
    
    # Use a large prime for hashing to reduce collisions
    base = 256
    prime = 101
    
    # Calculate hash of needle and first window of haystack
    needle_hash = 0
    haystack_hash = 0
    h = 1  # The value of base^(m-1)
    
    for i in range(m - 1):
        h = (h * base) % prime
    
    for i in range(m):
        needle_hash = (base * needle_hash + ord(needle[i])) % prime
        haystack_hash = (base * haystack_hash + ord(haystack[i])) % prime
    
    # Slide the pattern over the text
    for i in range(n - m + 1):
        # If hash values match, check characters
        if needle_hash == haystack_hash:
            # Verify character by character
            match = True
            for j in range(m):
                if haystack[i + j] != needle[j]:
                    match = False
                    break
            if match:
                return i
        
        # Calculate hash for next window
        if i < n - m:
            haystack_hash = (base * (haystack_hash - ord(haystack[i]) * h) + 
                            ord(haystack[i + m])) % prime
            
            # Handle negative hash values
            if haystack_hash < 0:
                haystack_hash += prime
    
    return -1

# Test the function
print(strStr("hello", "ll"))      # Output: 2
print(strStr("world", "ld"))      # Output: 3
print(strStr("aaaaa", "bba"))     # Output: -1

Rabin-Karp has an average time complexity of O(n + m) but can degrade to O(n × m) in the worst case due to hash collisions. It is particularly useful when searching for multiple patterns simultaneously, as you can compute hashes for all patterns and compare them in a single pass.

Comparing the Solutions

Each solution has its own trade-offs. Here is a summary to help you choose the right approach for different situations:

Best Practices

When implementing strStr() or any string matching algorithm, keep these best practices in mind:

Common Pitfalls to Avoid

When solving this problem, developers often make several common mistakes. Being aware of these can save you debugging time:

Testing Your Implementation

A robust test suite is essential for verifying your implementation. Here is a comprehensive set of test cases you should include:

def test_strStr(strStr_func):
    # Basic cases
    assert strStr_func("hello", "ll") == 2
    assert strStr_func("world", "rl") == 2
    assert strStr_func("python", "th") == 2
    
    # Needle not found
    assert strStr_func("aaaaa", "bba") == -1
    assert strStr_func("abc", "d") == -1
    
    # Empty needle
    assert strStr_func("abc", "") == 0
    assert strStr_func("", "") == 0
    
    # Empty haystack, non-empty needle
    assert strStr_func("", "a") == -1
    
    # Needle equals haystack
    assert strStr_func("abc", "abc") == 0
    
    # Needle longer than haystack
    assert strStr_func("ab", "abc") == -1
    
    # Match at the beginning
    assert strStr_func("abcdef", "abc") == 0
    
    # Match at the end
    assert strStr_func("abcdef", "def") == 3
    
    # Overlapping patterns
    assert strStr_func("mississippi", "issip") == 4
    assert strStr_func("abababab", "abab") == 0
    
    # Single character cases
    assert strStr_func("a", "a") == 0
    assert strStr_func("a", "b") == -1
    
    print("All tests passed!")

# Run tests with any of our implementations
test_strStr(strStr)

Running these tests against your implementation will help ensure correctness across a wide range of scenarios. This is especially important if you are preparing for coding interviews where edge cases are heavily tested.

Conclusion

Implementing strStr() is an excellent exercise that teaches fundamental string matching concepts. While the brute force approach is sufficient for most practical purposes and Python's built-in find() method is the best choice for production code, understanding advanced algorithms like KMP and Rabin-Karp expands your algorithmic toolkit and prepares you for more complex string processing challenges. The key is to understand the trade-offs between simplicity and efficiency, and to choose the right approach based on your specific requirements. Whether you are preparing for an interview or building a text processing application, mastering these techniques will make you a more versatile and effective developer.

— Ad —

Google AdSense will appear here after approval

← Back to all articles