String searching
Fast algorithms
Substring search
Data structures
Pattern matching

Fast algorithm for searching for substrings in a string

Master System Design with Codemia

Enhance your system design skills with over 120 practice problems, detailed solutions, and hands-on exercises.

Introduction

The fastest substring-search strategy depends on what you mean by fast: fastest to implement, fastest in worst-case theory, or fastest in real-world text processing. For everyday application code, the best answer is often the language's built-in search. For algorithm study, the important names are Knuth-Morris-Pratt, Boyer-Moore, and Rabin-Karp.

Start With The Practical Answer

If you are writing ordinary application code, use the standard library search function unless you have a measured reason not to.

In Python:

python
1text = "abracadabra"
2pattern = "cada"
3
4index = text.find(pattern)
5print(index)

The built-in implementation is highly optimized in native code and will usually beat a hand-written algorithm in real codebases.

That is the practical answer.

The Naive Algorithm

The naive approach checks the pattern at every possible starting position.

python
1def naive_search(text: str, pattern: str) -> int:
2    n = len(text)
3    m = len(pattern)
4
5    for i in range(n - m + 1):
6        if text[i:i + m] == pattern:
7            return i
8    return -1
9
10
11print(naive_search("abracadabra", "cada"))

Its worst-case time complexity is O(n * m), where n is the text length and m is the pattern length.

It is simple, but it does repeated work when mismatches happen late.

Knuth-Morris-Pratt: Good Worst-Case Behavior

KMP avoids rechecking characters by preprocessing the pattern into a prefix table.

python
1def build_lps(pattern: str) -> list[int]:
2    lps = [0] * len(pattern)
3    length = 0
4    i = 1
5
6    while i < len(pattern):
7        if pattern[i] == pattern[length]:
8            length += 1
9            lps[i] = length
10            i += 1
11        elif length != 0:
12            length = lps[length - 1]
13        else:
14            lps[i] = 0
15            i += 1
16
17    return lps

The full KMP search runs in O(n + m) worst-case time. That is algorithmically strong, especially when predictable worst-case performance matters.

Boyer-Moore: Often Fast In Practice

Boyer-Moore compares from the end of the pattern and can skip ahead aggressively when mismatches happen. On natural-language text and large alphabets, it is often very fast in practice.

The reason people like Boyer-Moore is not just asymptotic notation. It is that it often examines fewer characters than simpler algorithms on typical input.

That said, it is more complex to implement correctly than the naive method or KMP.

Rabin-Karp: Useful For Multiple Patterns Or Rolling Hashes

Rabin-Karp uses hashing. Instead of comparing the whole pattern at every position, it compares hash values and only performs full verification when hashes match.

A small rolling-hash sketch looks like this:

python
1def rolling_hash_search(text: str, pattern: str) -> bool:
2    m = len(pattern)
3    if m > len(text):
4        return False
5
6    target = hash(pattern)
7    for i in range(len(text) - m + 1):
8        if hash(text[i:i + m]) == target and text[i:i + m] == pattern:
9            return True
10    return False
11
12
13print(rolling_hash_search("abracadabra", "cada"))

This sketch uses Python's hash in a simple way for illustration, not as a production rolling-hash implementation. The algorithm is interesting because it generalizes well to searching many patterns.

Which Algorithm Should You Choose?

A practical rule is:

  • use the built-in string search for ordinary application code
  • use KMP when you want strong worst-case guarantees and are implementing the algorithm yourself
  • use Boyer-Moore when you care about very fast searching on typical text
  • use Rabin-Karp when hash-based multi-pattern workflows make sense

The best algorithm is not universal. It depends on input characteristics and whether you care more about implementation simplicity, worst-case guarantees, or average-case speed.

Multiple Queries Change The Tradeoff

If you search the same text many times for many different patterns, suffix arrays, suffix automata, or suffix trees can become relevant. Those data structures shift the problem from "one search in one string" to "many searches after preprocessing."

That is a different problem class from a single substring search.

Common Pitfalls

The most common mistake is reimplementing a complex search algorithm by hand when the language runtime already provides an excellent optimized search.

Another mistake is quoting theoretical complexity without considering the actual workload. Boyer-Moore may outperform a theoretically elegant alternative on real text, while a built-in search may beat both for practical reasons.

Developers also confuse single-pattern and multi-pattern problems. The best tool can change completely when the query pattern changes many times.

Finally, do not treat all O(n + m) algorithms as interchangeable. Constants, alphabet size, and implementation quality matter in substring search.

Summary

  • For most real programs, the built-in substring search is the best starting point.
  • The naive algorithm is simple but can take O(n * m) time in the worst case.
  • KMP gives O(n + m) worst-case performance.
  • Boyer-Moore is often very fast on typical text.
  • Rabin-Karp is useful when hashing and multi-pattern scenarios matter.

Course illustration
Course illustration

All Rights Reserved.