Strings in C++ | Lecture 43: String Matching & KMP Algorithm
The String Matching Problem (frequently referred to as finding a "Needle in a Haystack") asks us to determine whether a target pattern substring exists inside a primary text string, returning the starting index of its first occurrence. The Knuth-Morris-Pratt (KMP) Algorithm is a classic string-matching technique that improves search efficiency from the naive quadratic bound to linear time by eliminating redundant rewinds of the primary string pointer using a precomputed Longest Prefix Suffix (LPS) array.
1. Problem Statement: String Matching (Needle in a Haystack)
Given two strings:
haystack: The main string of length in which we perform the search.needle: The pattern string of length that we want to locate withinhaystack.
Objective
Return the -based index of the first character where needle appears in haystack. If needle is not present, return -1.
Example 1:
Haystack : a b c d e e f g h
Needle : d e f
Output : 3 (Index where 'd' begins in Haystack)
Example 2:
Haystack : a b a b c a b c a b a
Needle : a b a d
Output : -1 (Pattern not present)
2. Naive Brute Force Approach
The intuitive method checks every possible alignment of needle against haystack sequentially.
Algorithmic Mechanics
- Slide
needleacrosshaystackfrom index up to . - For each position , initialize a pointer
second = 0forneedleandfirst = iforhaystack. - Compare characters
haystack[first]andneedle[second]:- If they match, increment both
firstandsecond. - If a mismatch occurs, break out of the inner comparison loop.
- Reset the
needlesearch: shift the starting alignment inhaystackby 1 (i++), resettingfirstback to andsecondback to .
- If they match, increment both
Haystack : a b a b c a b c a b a
| | | X (Mismatch at 'c' vs 'd')
Needle : a b a d
Shift alignment by 1 step:
Haystack : a b a b c a b c a b a
X (Mismatch at 'b' vs 'a')
Needle : a b a d
Naive Implementation (C++)
#include <iostream>
#include <string>
using namespace std;
int strStrNaive(string haystack, string needle) {
int n = haystack.size();
int m = needle.size();
if (m == 0) return 0;
if (m > n) return -1;
// Outer loop checks all valid starting positions in haystack
for (int i = 0; i <= n - m; i++) {
int first = i;
int second = 0;
while (second < m && haystack[first] == needle[second]) {
first++;
second++;
}
// Complete pattern matched
if (second == m) {
return i; // Starting index
}
}
return -1; // Pattern not found
}
Complexity Breakdown
- Time Complexity: in the worst case (e.g.,
haystack = "aaaaaaaaab",needle = "aaab"). - Space Complexity: auxiliary space.
3. Real-World Analogy & Mental Hook: Why Rewinding is Wasteful
The Restaurant Story
Real-Life Analogy
Imagine visiting a high-end restaurant and placing an order for "Onions". A companion corrects your pronunciation to "Anions". When attempting to fix your speech mid-word, starting all the way back from scratch ("O... N... I... O... N...") wastes energy and time.
Instead, if you realize that the prefix "AN-" sounds almost identical to "ON-", you only correct the mismatched syllable rather than restarting the entire word from the first letter.
The fundamental flaw of the Naive algorithm is pointer regression. When a mismatch occurs after matching characters, the main text pointer first is dragged back to where the match started, completely ignoring the structural information derived from the characters already matched.
4. Key Mechanism: Leveraging the LPS (Longest Prefix Suffix) Array
To prevent pointer first from ever rewinding, the KMP algorithm utilizes information about the pattern string itself via the LPS Array.
What is the LPS Array?
For a given pattern needle of length , LPS[i] stores the length of the longest proper prefix of needle[0...i] that is also a suffix of needle[0...i].
Note: A proper prefix cannot be equal to the entire string itself.
The Jump Rule on Mismatch
When haystack[first] != needle[second]:
- We know that
needle[0 ... second - 1]successfully matchedhaystack[first - second ... first - 1]. - Instead of resetting
first, we look upLPS[second - 1]. - Set
second = LPS[second - 1]. - This action shifts
needleto align its longest matching prefix with the suffix of the already matched string section inhaystack.
5. Step-by-Step KMP Algorithm Execution Trace
Let us trace the matching process using the string "onions" as needle against "onionionions" as haystack.
Step 1: Compute LPS for needle = "onions"
Step 2: Trace Search Execution
haystack = "o n i o n i o n i o n s" (Length n = 12)
needle = "o n i o n s" (Length m = 6)
-
Matching Phase 1:
first = 0..4,second = 0..4:haystack[0..4]("onion") matchesneedle[0..4]("onion").- At
first = 5('i') andsecond = 5('s'): Mismatch ('i' != 's').
-
KMP Shift Strategy:
- Do NOT rewind
firstback to 1! - Inspect
LPS[second - 1]LPS[4] = 2. - Update
second = 2. Pointerfirstremains at5.
- Do NOT rewind
Vertical Alignment after KMP Jump:
haystack : o n i o n i o n i o n s
^ (first stays at index 5)
|
needle : o n i o n s
^ (second jumps to index 2)
-
Matching Phase 2:
- Compare
haystack[5]('i') withneedle[2]('i'): Match! - Both pointers advance:
firstgoessecondgoes
secondreachesm = 6(end of pattern).
- Compare
-
Result Calculation:
6. C++ Implementation: KMP Algorithm
Here is the complete production-grade C++ solution using the KMP Algorithm.
#include <iostream>
#include <vector>
#include <string>
using namespace std;
class Solution {
private:
// Helper function to precompute the LPS (Longest Prefix Suffix) array
void constructLPS(const string& needle, vector<int>& lps) {
int m = needle.size();
int len = 0; // Length of the previous longest prefix suffix
lps[0] = 0; // LPS[0] is always 0
int i = 1;
while (i < m) {
if (needle[i] == needle[len]) {
len++;
lps[i] = len;
i++;
} else {
if (len != 0) {
// Fall back to previous longest prefix length
len = lps[len - 1];
} else {
// No prefix matches suffix
lps[i] = 0;
i++;
}
}
}
}
public:
int strStr(string haystack, string needle) {
int n = haystack.size();
int m = needle.size();
if (m == 0) return 0;
if (m > n) return -1;
// Step 1: Precompute LPS array for needle
vector<int> lps(m, 0);
constructLPS(needle, lps);
int first = 0; // Index for haystack
int second = 0; // Index for needle
// Step 2: Linear search scan
while (first < n && second < m) {
if (haystack[first] == needle[second]) {
first++;
second++;
} else {
if (second != 0) {
// Shift pattern using LPS without rewinding 'first'
second = lps[second - 1];
} else {
// Mismatch at index 0 of needle; simply advance 'first'
first++;
}
}
}
// Step 3: Verify match condition
if (second == m) {
return first - second; // Match starting index
}
return -1; // Pattern not found
}
};
int main() {
Solution solver;
string haystack = "onionionions";
string needle = "onions";
int index = solver.strStr(haystack, needle);
cout << "Pattern found at index: " << index << endl; // Output: 5
return 0;
}
7. Comparative Analysis: Naive vs. KMP
8. Edge Cases & Debugging Mindset
When implementing KMP under timed conditions or technical interviews, engineers frequently run into specific logic traps:
Pitfall 1: Out-of-Bounds Access when second == 0
- Error: Attempting to read
LPS[second - 1]whensecond == 0results in evaluatingLPS[-1]. - Fix: Wrap the jump in an explicit guard condition:
if (second != 0) second = lps[second - 1]; else first++;.
Pitfall 2: Memory Overhead via Pass-by-Value
- Error: Passing
string needleorvector<int> lpsby value in helper functions creates deep memory copies on each function invocation, spiking runtime to . - Fix: Pass large objects via const reference (
const string& needle,vector<int>& lps).
Pitfall 3: Substring Index Calculation
- Error: Returning
firstdirectly whensecond == m. - Fix: The match started earlier! The correct index calculation is always
first - second.
9. Summary and Key Takeaways
- Sub-string Search Standard: The goal of string matching is to find the first occurrence of pattern
needle() inside stringhaystack(). - Naive Limitation: The brute force algorithm takes quadratic time because it rewinds the main text pointer upon encountering any mismatched character.
- Core KMP Paradigm: KMP guarantees runtime efficiency by ensuring that the primary text pointer
firstnever moves backward. - Role of LPS Array: The Longest Prefix Suffix (LPS) array encodes structural self-similarities of the pattern, allowing
secondto bypass redundant comparisons. - Space-Time Tradeoff: KMP uses extra space to store the precomputed LPS values of the pattern string.
- Mismatch State Transitions:
- If
haystack[first] == needle[second]: Advance bothfirst++andsecond++. - If mismatch and
second > 0: Fall back tosecond = LPS[second - 1]. - If mismatch and
second == 0: Incrementfirst++.
- If
- Match Identification: A successful pattern match occurs when
second == m, returning the starting index calculated asfirst - second.