What is strStr()?
The strStr() function, also known as "find substring in string" or "needle in haystack," is a fundamental string manipulation operation. Given two strings β a haystack (the main string to search within) and a needle (the substring to find) β the function returns the index of the first occurrence of the needle in the haystack. If the needle is not found, it returns -1. In many programming languages, this is equivalent to indexOf() in JavaScript, find() in Python, or strpos() in PHP.
The function signature typically looks like:
int strStr(string haystack, string needle)
Edge cases to consider:
- Empty needle: Conventionally returns 0 (the empty string is found at position 0)
- Needle longer than haystack: Returns -1 immediately
- Multiple occurrences: Only the first occurrence index matters
- Overlapping matches: Must be handled correctly depending on the algorithm
Why Understanding strStr() Matters
π Deploy your AI agent in 10 minutes
Managed Hermes hosting. Zero DevOps. 100M tokens/mo included.
Try it free →String searching is not just an academic exercise β it powers real-world functionality across countless domains:
- Text editors and IDEs: Find-and-replace operations rely on efficient substring search
- Network intrusion detection: Scanning packet payloads for malicious patterns
- Bioinformatics: Locating gene sequences within DNA strings that can be billions of characters long
- Search engines: Matching query terms against indexed documents
- Compilers: Tokenizing source code by finding keywords and operators
Beyond practical utility, strStr() serves as an excellent gateway to understanding algorithm design trade-offs. The progression from a naive O(nΒ·m) solution to sophisticated O(n+m) algorithms demonstrates how mathematical insight β like precomputation and state machines β can dramatically reduce computational cost. Mastering these techniques builds intuition for pattern matching, dynamic programming, and automata theory that transfers to more complex problems.
Solution 1: Naive Brute Force Approach
The simplest approach slides a window of length equal to the needle across the haystack, checking at each position whether the substring matches character by character.
Algorithm Steps
- Iterate
ifrom 0 tohaystack.length - needle.length - For each
i, compare characters athaystack[i+j]withneedle[j]forj = 0toneedle.length - 1 - If all characters match, return
i - If the loop completes without a match, return -1
Complete Implementation
public int strStrNaive(String haystack, String needle) {
// Edge case: empty needle
if (needle.isEmpty()) {
return 0;
}
int n = haystack.length();
int m = needle.length();
// Needle longer than haystack β impossible match
if (m > n) {
return -1;
}
// Slide window across haystack
for (int i = 0; i <= n - m; i++) {
int j;
for (j = 0; j < m; j++) {
if (haystack.charAt(i + j) != needle.charAt(j)) {
break; // Mismatch, move to next starting position
}
}
// If we completed inner loop without breaking, full match found
if (j == m) {
return i;
}
}
return -1; // No match found
}
Complexity Analysis
- Time Complexity: O(nΒ·m) β In the worst case, for each of the n-m+1 starting positions, we may compare up to m characters. Consider haystack = "aaaaaa...a" and needle = "aaa...ab" β each attempt fails only at the last character, forcing nearly m comparisons per position.
- Space Complexity: O(1) β No additional data structures are allocated beyond loop variables.
The naive approach is acceptable for small strings or one-off searches, but it degrades badly on repetitive patterns. Its simplicity makes it a great baseline, but production scenarios demand better.
Solution 2: Knuth-Morris-Pratt (KMP) Algorithm
The KMP algorithm eliminates redundant comparisons by precomputing a Longest Prefix Suffix (LPS) table. When a mismatch occurs, instead of starting over from the next character, the algorithm uses the LPS table to skip ahead to a position where certain characters are already guaranteed to match.
Core Insight: The LPS Array
For a pattern string, the LPS value at index i represents the length of the longest proper prefix of the pattern that is also a suffix of the substring pattern[0..i]. This tells us how many characters we can safely skip when a mismatch happens.
For example, consider needle = "ababc":
- LPS[0] = 0 (single character, no proper prefix)
- LPS[1] = 0 ("ab" β no prefix matches suffix except empty)
- LPS[2] = 1 ("aba" β prefix "a" matches suffix "a")
- LPS[3] = 2 ("abab" β prefix "ab" matches suffix "ab")
- LPS[4] = 0 ("ababc" β no prefix matches suffix)
Resulting LPS array: [0, 0, 1, 2, 0]
Building the LPS Table
private int[] buildLPS(String pattern) {
int m = pattern.length();
int[] lps = new int[m];
int len = 0; // Length of the current matching prefix
int i = 1; // Start from index 1 (lps[0] is always 0)
while (i < m) {
if (pattern.charAt(i) == pattern.charAt(len)) {
len++;
lps[i] = len;
i++;
} else {
if (len != 0) {
// Fall back to previous prefix length
len = lps[len - 1];
// Do NOT increment i here β re-evaluate with new len
} else {
lps[i] = 0;
i++;
}
}
}
return lps;
}
This construction runs in O(m) time. The key trick: when characters mismatch and len > 0, we set len = lps[len-1] and stay at the same i. This recursive fallback uses previously computed LPS values to avoid backtracking in the pattern itself.
KMP Search Implementation
public int strStrKMP(String haystack, String needle) {
if (needle.isEmpty()) {
return 0;
}
int n = haystack.length();
int m = needle.length();
if (m > n) {
return -1;
}
// Precompute the LPS table
int[] lps = buildLPS(needle);
int i = 0; // Index for haystack
int j = 0; // Index for needle
while (i < n) {
if (haystack.charAt(i) == needle.charAt(j)) {
i++;
j++;
// Full match found
if (j == m) {
return i - j; // Starting index of match
}
} else {
if (j != 0) {
// Use LPS to skip ahead β no need to restart from scratch
j = lps[j - 1];
// i stays the same, we'll re-compare with new j
} else {
// j is 0, just move haystack pointer forward
i++;
}
}
}
return -1;
}
How KMP Avoids Redundant Work
Imagine haystack = "ababcabcab" and needle = "abcab". When a mismatch occurs at position 5 (haystack[5]='c' vs needle[5] doesn't exist since needle length is 5... let's use a clearer example):
Consider haystack = "aaaaab" and needle = "aaab". At i=0, we match "aaa" then hit 'a' vs 'b' at j=3. Instead of sliding to i=1 and re-comparing "aa" from scratch, KMP knows from LPS[2]=2 that the first two characters of the needle match the last two characters we just saw. It sets j=2 and continues comparing from haystack[3] vs needle[2], effectively skipping redundant checks.
Complexity Analysis
- Preprocessing Time: O(m) β Building the LPS array makes a single pass over the needle, with the inner while loop bounded by the total number of fallbacks (at most m).
- Search Time: O(n) β The haystack pointer
inever decreases. When a mismatch occurs and j > 0, j falls back via LPS while i stays put. The total number of fallbacks across the entire search is bounded by n, so the amortized time per character is constant. - Overall Time Complexity: O(n + m) β Linear in both input sizes, a dramatic improvement over the naive O(nΒ·m).
- Space Complexity: O(m) β The LPS array requires space proportional to the needle length.
Solution 3: Rabin-Karp (Rolling Hash) Algorithm
Rabin-Karp takes a completely different approach: instead of comparing characters, it compares hash values. It computes a hash for the needle, then slides a window over the haystack, updating the hash in O(1) time at each step using a rolling hash technique. Only when hash values match does it perform an actual character-by-character comparison to rule out collisions.
The Rolling Hash Mechanism
A standard rolling hash uses a base B (typically 256 for ASCII, or a prime like 101) and a modulus M (a large prime to avoid overflow). The hash of a string of length m is:
hash(s) = (s[0]Β·B^(m-1) + s[1]Β·B^(m-2) + ... + s[m-1]Β·B^0) mod M
When sliding the window forward by one character, the old hash can be updated in constant time:
newHash = ((oldHash - oldCharΒ·B^(m-1)) Β· B + newChar) mod M
Complete Rabin-Karp Implementation
public int strStrRabinKarp(String haystack, String needle) {
if (needle.isEmpty()) {
return 0;
}
int n = haystack.length();
int m = needle.length();
if (m > n) {
return -1;
}
// Parameters for rolling hash
final int BASE = 256;
final int MOD = 1_000_000_007; // Large prime to minimize collisions
long basePow = 1; // Will store BASE^(m-1) mod MOD
// Compute BASE^(m-1) % MOD
for (int i = 0; i < m - 1; i++) {
basePow = (basePow * BASE) % MOD;
}
// Compute hash of needle
long needleHash = 0;
for (int i = 0; i < m; i++) {
needleHash = (needleHash * BASE + needle.charAt(i)) % MOD;
}
// Compute hash of first window in haystack
long windowHash = 0;
for (int i = 0; i < m; i++) {
windowHash = (windowHash * BASE + haystack.charAt(i)) % MOD;
}
// Slide the window
for (int i = 0; i <= n - m; i++) {
// Check hash match
if (windowHash == needleHash) {
// Verify character by character to rule out hash collision
boolean match = true;
for (int j = 0; j < m; j++) {
if (haystack.charAt(i + j) != needle.charAt(j)) {
match = false;
break;
}
}
if (match) {
return i;
}
}
// Slide window: remove haystack[i] and add haystack[i+m]
if (i < n - m) {
// Remove contribution of outgoing character
windowHash = (windowHash - haystack.charAt(i) * basePow % MOD + MOD) % MOD;
// Shift left and add incoming character
windowHash = (windowHash * BASE + haystack.charAt(i + m)) % MOD;
}
}
return -1;
}
Handling Hash Collisions
Two different strings can produce the same hash value modulo M. This is why the code always performs a full string comparison when hashes match. In practice, with a well-chosen large prime modulus, collisions are extremely rare, and the algorithm behaves as if it's O(n+m) on average. However, an adversary could craft inputs that cause many collisions (e.g., by exploiting the specific modulus), degrading performance to O(nΒ·m) in the worst case.
Complexity Analysis
- Average Time Complexity: O(n + m) β Hash computation is O(m) initially, then O(1) per window slide. With few collisions, verification checks are negligible.
- Worst-case Time Complexity: O(nΒ·m) β If every window produces a hash collision, each position requires full string comparison, degenerating to the naive approach.
- Space Complexity: O(1) β Only a handful of integer variables are needed.
Rabin-Karp shines when searching for multiple patterns simultaneously β you can compute hashes for many needles and check each window against all of them. This multi-pattern capability makes it popular in plagiarism detection and network filtering systems.
Solution 4: Boyer-Moore Algorithm (Overview)
Boyer-Moore is often the fastest algorithm in practice for large alphabets (like English text). It preprocesses the needle to create two heuristic tables and then compares from right to left, allowing it to skip large portions of the haystack.
Two Key Heuristics
- Bad Character Rule: When a mismatch occurs at position j in the needle, look up the mismatched haystack character in a table. If that character doesn't appear in the needle at all, skip the entire needle length. If it appears, align the needle so that character matches.
- Good Suffix Rule: When a suffix of the needle has matched but then a mismatch occurs, use precomputed shift values based on that suffix to determine how far to jump.
Simplified Boyer-Moore Implementation (Bad Character Rule Only)
public int strStrBoyerMoore(String haystack, String needle) {
if (needle.isEmpty()) {
return 0;
}
int n = haystack.length();
int m = needle.length();
if (m > n) {
return -1;
}
// Precompute last occurrence positions for each character in needle
// For characters not in needle, store -1
int[] last = new int[256]; // Assuming ASCII
for (int i = 0; i < 256; i++) {
last[i] = -1;
}
for (int i = 0; i < m; i++) {
last[needle.charAt(i)] = i;
}
int i = 0; // Haystack index where we attempt to align needle start
while (i <= n - m) {
int j = m - 1; // Start comparing from the rightmost character
// Compare from right to left
while (j >= 0 && haystack.charAt(i + j) == needle.charAt(j)) {
j--;
}
// If j < 0, we matched the entire needle
if (j < 0) {
return i;
}
// Mismatch at needle[j] with haystack[i+j]
// Get last occurrence of the mismatched character in needle
int lastOcc = last[haystack.charAt(i + j)];
// Shift: align needle so that the mismatched char matches
// If lastOcc < j, shift by j - lastOcc
// If lastOcc > j (character appears later in needle), shift by 1
// If lastOcc == -1 (character not in needle), shift past it entirely
int shift = Math.max(1, j - lastOcc);
i += shift;
}
return -1;
}
Complexity Analysis
- Best-case Time Complexity: O(n/m) β When many characters in the haystack don't appear in the needle, the algorithm skips m characters at a time, requiring only n/m comparisons.
- Average-case Time Complexity: O(n) β On typical English text, Boyer-Moore outperforms KMP by a constant factor.
- Worst-case Time Complexity: O(nΒ·m) β Certain pathological inputs (e.g., haystack = "aaaaaaaaa", needle = "aaaa") force linear scanning.
- Space Complexity: O(1) with fixed alphabet size β The last-occurrence table is bounded by the character set (256 for ASCII, or 65536 for extended sets).
Comparative Analysis Summary
Here's how the four solutions stack up against each other:
βββββββββββββββββββ¬βββββββββββββββββββ¬βββββββββββββββ¬ββββββββββββββββ¬βββββββββββββββββββββββ
β Algorithm β Average Time β Worst Time β Space β Best For β
βββββββββββββββββββΌβββββββββββββββββββΌβββββββββββββββΌββββββββββββββββΌβββββββββββββββββββββββ€
β Naive β O(nΒ·m) β O(nΒ·m) β O(1) β Small strings, β
β β β β β learning baseline β
βββββββββββββββββββΌβββββββββββββββββββΌβββββββββββββββΌββββββββββββββββΌβββββββββββββββββββββββ€
β KMP β O(n + m) β O(n + m) β O(m) β Guaranteed linear, β
β β β β β repetitive patterns β
βββββββββββββββββββΌβββββββββββββββββββΌβββββββββββββββΌββββββββββββββββΌβββββββββββββββββββββββ€
β Rabin-Karp β O(n + m)* β O(nΒ·m) β O(1) β Multiple pattern β
β β β β β search simultaneously β
βββββββββββββββββββΌβββββββββββββββββββΌβββββββββββββββΌββββββββββββββββΌβββββββββββββββββββββββ€
β Boyer-Moore β O(n/m) best β O(nΒ·m) β O(1) fixed β Large alphabets, β
β β O(n) average β β alphabet β practical text search β
βββββββββββββββββββ΄βββββββββββββββββββ΄βββββββββββββββ΄ββββββββββββββββ΄βββββββββββββββββββββββ
* Average case with low collision probability; degrades with adversarial inputs.
Best Practices for Choosing and Implementing strStr()
Match the Algorithm to Your Data
- Short strings (n, m < 100): The naive approach is perfectly fine. The overhead of building LPS tables or computing hashes outweighs the algorithmic gains for small inputs.
- Long texts with diverse characters (e.g., English prose): Boyer-Moore is typically the fastest in practice due to aggressive skipping.
- Repetitive or binary data: KMP guarantees linear time regardless of input characteristics, making it the safest choice for mission-critical applications where worst-case performance matters.
- Searching for many patterns at once: Rabin-Karp (or its multi-pattern variant using Bloom filters) excels here.
Implementation Tips
- Always handle edge cases first: Empty needle, needle longer than haystack, null inputs β these guard clauses prevent unnecessary computation and potential exceptions.
- Use integer arithmetic carefully in rolling hashes: Overflow can cause subtle bugs. Always apply modulus after each arithmetic operation, and add MOD before taking modulus to handle negative intermediate values:
((a - b) % MOD + MOD) % MOD. - Prefer character arrays over string indexing for performance-critical code: Converting strings to
char[]once and indexing into arrays avoids the bounds checking and method call overhead ofcharAt()in tight loops. - Benchmark with representative data: Theoretical complexity tells part of the story; constant factors from cache locality, branch prediction, and JIT compilation can flip rankings in practice.
- Consider built-in functions first: Languages like Java (
indexOf), Python (find), and C++ (std::search) use highly optimized implementations β often a blend of algorithms with runtime heuristics. Unless you have specific constraints, leverage these.
Testing Your Implementation
A robust test suite should include:
// Test cases to validate your strStr() implementation
// 1. Basic functionality
assert strStr("hello", "ll") == 2;
assert strStr("hello", "hello") == 0;
assert strStr("hello", "world") == -1;
// 2. Empty needle
assert strStr("anything", "") == 0;
// 3. Needle longer than haystack
assert strStr("short", "longer_needle") == -1;
// 4. Match at boundaries
assert strStr("abc", "a") == 0;
assert strStr("abc", "c") == 2;
// 5. Overlapping patterns
assert strStr("aaaaa", "aa") == 0; // First occurrence
assert strStr("ababa", "aba") == 0; // Overlapping: occurs at 0 and 2
// 6. Repetitive pattern (stress test for naive approach)
String haystack = "a".repeat(100000) + "b";
String needle = "a".repeat(9999) + "b";
// Should return correct index without timing out
// 7. Case sensitivity
assert strStr("Hello", "hello") == -1;
assert strStr("Hello", "Hell") == 0;
Conclusion
Implementing strStr() opens a window into the rich landscape of string algorithm design. Starting from the straightforward naive sliding window, we've progressed through KMP's elegant LPS-based state machine that guarantees linear time, Rabin-Karp's hash-driven approach that generalizes beautifully to multi-pattern search, and Boyer-Moore's right-to-left scanning with heuristic skipping that dominates in practical text processing. Each algorithm represents a different philosophical approach: exhaustive comparison, precomputed automata, numeric fingerprinting, and rule-based jumping. The choice among them hinges on your input characteristics, performance requirements, and the number of patterns you need to find. By internalizing these techniques, you equip yourself not just to solve a LeetCode problem, but to make informed engineering decisions whenever pattern matching appears β and in software, it appears everywhere.