Strings · String Fundamentals

Naive Substring Search

Checking every possible starting position for a match, character by character - the direct, unoptimized way to find one string inside another, correct and simple though not the fastest for large or adversarial inputs.

  • 6 min read
  • 9 practice questions
  • 2 graded problems
On this page
  1. Description
  2. The approach
  3. A second flavor: a rolling hash (Rabin-Karp)
  4. Where you'll see it
  5. Which tool for which job?
  6. Common mistakes
  7. Check yourself
  8. Key takeaways
  9. Quick revise
  10. Practice

Description

Finding where a pattern first appears in a text is the core of every editor's Find, of grep and of log filtering. The simplest method tries every alignment, and for most real text it's fast. Knowing exactly when it isn't, and what to use then, matters more than the code. This skill covers the naive scan, the rolling-hash speed-up (Rabin-Karp), and the hashing pitfalls that turn fingerprints into false matches.

We'll go from simple to more technical:

  1. The pattern: try every alignment, comparing in place.
  2. A second flavor: a rolling hash, with every hash match verified.
  3. Where substring search runs in production (Find, grep, log filters, signature scanning).
  4. What breaks it: loop bounds, repetitive input, per-position copies, trusting hashes.

The approach

Java

int strStr(String haystack, String needle) {
    int n = haystack.length(), m = needle.length();
    for (int i = 0; i + m <= n; i++) {                    // every start where the needle still fits
        int j = 0;
        while (j < m && haystack.charAt(i + j) == needle.charAt(j)) j++;
        if (j == m) return i;                             // all m characters matched
    }
    return -1;
}

Python

def str_str(haystack: str, needle: str) -> int:
    n, m = len(haystack), len(needle)
    for i in range(n - m + 1):                       # every start where the needle still fits
        j = 0
        while j < m and haystack[i + j] == needle[j]:
            j += 1
        if j == m:
            return i                                 # all m characters matched
    return -1

The bound i + m <= n allows the last possible alignment, where the needle ends on the text's last character (in Python, range(n - m + 1)). i < n - m misses exactly that alignment ("abc" and "c" returns -1), and i < n reads past the end.

Each alignment compares characters until the first mismatch. On ordinary text that happens almost immediately, so the search is close to O(n). On repetitive text it doesn't: searching 200,000 'a's for 1,000 'a's followed by a 'b' compares about 1,000 characters at every one of 199,000 alignments - 199 million comparisons, O(n * m). Compare in place, too: haystack.substring(i, i + m).equals(needle) copies a new string at every position, while haystack.startsWith(needle, i) doesn't. In Python, too, haystack[i:i + m] == needle copies a slice and haystack.startswith(needle, i) doesn't.

A second flavor: a rolling hash (Rabin-Karp)

Instead of comparing characters at every alignment, compare a fingerprint of each window with the needle's - and update the window's fingerprint in O(1) as it slides:

Java

int rabinKarp(String text, String needle) {
    int n = text.length(), m = needle.length();
    if (m > n) return -1;
    final long B = 256, P = 1_000_000_007L;
    long pow = 1, hn = 0, h = 0;
    for (int k = 0; k < m; k++) {
        if (k > 0) pow = pow * B % P;                     // B^(m-1) mod P
        hn = (hn * B + needle.charAt(k)) % P;
        h = (h * B + text.charAt(k)) % P;
    }
    for (int i = 0; ; i++) {
        if (h == hn && text.startsWith(needle, i)) return i;   // equal hashes can still differ: verify
        if (i + m == n) return -1;
        h = (h - text.charAt(i) * pow % P + P) % P;            // drop the leftmost character
        h = (h * B + text.charAt(i + m)) % P;                  // append the next one
    }
}

Python

def rabin_karp(text: str, needle: str) -> int:
    n, m = len(text), len(needle)
    if m > n:
        return -1
    B, P = 256, 1_000_000_007
    pow_b = pow(B, m - 1, P)                         # B^(m-1) mod P
    hn = h = 0
    for k in range(m):
        hn = (hn * B + ord(needle[k])) % P
        h = (h * B + ord(text[k])) % P
    for i in range(n - m + 1):
        if h == hn and text.startswith(needle, i):
            return i                                 # equal hashes can still differ: verify
        if i + m < n:
            h = (h - ord(text[i]) * pow_b) % P       # drop the leftmost character (Python's % is never negative)
            h = (h * B + ord(text[i + m])) % P       # append the next one
    return -1

The expected cost is O(n + m): characters are compared only when the hashes agree, and with a large prime that's almost always a real match. The verification is not optional - different windows can share a hash. For very small alphabets an exact encoding avoids collisions entirely: with A, C, G, T as 2-bit codes, a 10-letter DNA window is a 20-bit int, updated with window = ((window << 2) | code) & ((1 << 20) - 1).

Where you'll see it

Java's String.indexOf is exactly this try-every-alignment scan; HotSpot runs it with SIMD instructions, which makes it fast but leaves the worst case O(n * m). C's glibc strstr uses the Two-Way algorithm, linear in the worst case, and GNU grep uses Boyer-Moore-style skipping. Rolling hashes power rsync, which finds matching blocks between file versions with a rolling checksum, and content-defined chunking in backup and deduplication tools.

Which tool for which job?

You needUse
A short pattern, one searchthe naive scan (or the library's indexOf / find)
Many patterns of the same lengthRabin-Karp with the patterns' hashes in a set
Repeated substrings of a lengthrolling hash, or an exact bit encoding for tiny alphabets
A guaranteed linear worst caseKMP or Z
Thousands of patterns at onceAho-Corasick
Hashing that attackers can't breaka large prime modulus (2^61 - 1) and a random base

Common mistakes

MistakeWhat happensDo this instead
i < n - mmisses a match at the endi <= n - m
Naive search on repetitive inputO(n × m)KMP for untrusted input
substring at every positionan allocation per positioncompare in place
Trusting equal hashesfalse matches (39 collisions in 300,000 windows)verify every hash match
Hashing modulo 2^64 (natural overflow)Thue-Morse strings collide for any basea prime modulus
Using String.hashCode as an identity"Aa" and "BB" collideequality checks, or a stronger hash

Check yourself

Try to answer before opening each one - recalling beats re-reading.

1. Why is the loop bound i + m <= n and not i < n - m?

Show answer

The last alignment starts at i = n - m, where the needle ends on the text's final character. i < n - m stops one alignment short, so a needle at the very end ("abc", "c") is never found.

2. What input makes naive search do the most work?

Show answer

Repetitive text where almost every alignment matches for a long time before failing - 200,000 'a's searched for 1,000 'a's and a 'b'. Each alignment compares about m characters, for about n * m in total (199 million comparisons there).

3. Why does Rabin-Karp compare characters when the hashes are equal?

Show answer

Many different strings share each hash value, so equal hashes only mean "probably equal". Verifying costs O(m) per candidate, and with a good hash almost every candidate is a real match, keeping the expected total O(n + m).

Key takeaways

  • Try every start with i + m <= n; compare in place, not with substring.
  • Naive search is near O(n) on typical text but O(n * m) on repetitive text - and Java's indexOf is naive.
  • Rabin-Karp slides a hash in O(1) per step and must verify hash matches.
  • Tiny alphabets allow exact rolling encodings (2 bits per DNA letter).
  • For guaranteed linear time, use KMP (or Two-Way).

Quick revise

The whole skill on one page, for the night before an interview.

This page puts the lesson and the deep dive together. Skim it before practice or an interview, and go back to the full lesson for anything you don't recognize.

Pick the tool

You needUse
One short patternnaive / library search
Many same-length patternsRabin-Karp plus a set
Linear worst caseKMP / Z
Thousands of patternsAho-Corasick
Safe hashingprime modulus (2^61 - 1), random base

Snippets worth knowing by heart

Java

for (int i = 0; i + m <= n; i++) {                    // <=: the last alignment counts
    int j = 0;
    while (j < m && text.charAt(i + j) == pat.charAt(j)) j++;
    if (j == m) return i;
}
return -1;

Python

MOD = (1 << 61) - 1                      # a Mersenne prime: rare collisions, no 2^64 weakness
BASE = random.randrange(256, MOD)        # random base: attackers can't precompute collisions

The traps

  • The last alignment: i < n - m misses a match at the end. Loop while i <= n - m.
  • Repetitive input: "aaaa...ab" makes naive search O(n x m). Use KMP for untrusted input.
  • Per-position substrings: calling substring everywhere allocates each time. Compare in place.
  • Unverified hash matches: equal hashes can be different strings. Verify every hash match.
  • Modulo 2^64: with natural overflow, Thue-Morse strings collide for any base. Use a prime modulus.

Staff-level points, one line each

  • 300,000 random windows: 39 collisions modulo 10^9 + 7, 0 modulo 2^61 - 1.
  • Thue-Morse (length 2,048) and its complement collide modulo 2^64 with base 131 and with base 1,000,003.
  • "Aa" and "BB" both hash to 2112. 1,024 strings shared one hash code, and HashMap's tree bins still found all of them.
  • Aho-Corasick finds every signature in one pass.

Say it in an interview

  • "Naive is O(n × m) worst case but fine for short patterns."
  • "Rabin-Karp verifies every hash match, because collisions happen."
  • "I'd hash modulo a big prime with a random base, never with plain 64-bit overflow."
  • "For many patterns, Aho-Corasick."

Practice

This skill comes with 9 multiple-choice questions that tell you which misconception a wrong answer reveals and 2 graded problems. Your answers are graded and your mastery is tracked, so you'll know when the skill holds up. Running code and saving progress need a free account.

Practice Naive Substring Search free

Repeated DNA Sequences

MediumProCoding problemJava · Python

DNA is written with the letters A, C, G and T. Find every stretch of 10 letters that appears more than once in s (two appearances may overlap). Report each such stretch once; the driver sorts them and prints them space-separated, or prints an empty line if there are none.

The full statement, the examples and the hidden test suite are part of AI Proof Engineers Pro.

Practice Naive Substring Search free

No card needed. Lessons, practice questions and easy problems are free forever.

Free plan, no card · 7-day Pro trial · Cancel anytime