KMP Pattern Matching Algorithm in DSA Rust - Time & Space Complexity
We want to understand how the time taken by the KMP algorithm changes as the input text and pattern grow.
Specifically, how fast can KMP find a pattern inside a text?
Analyze the time complexity of the following KMP pattern matching code.
fn compute_lps(pattern: &str) -> Vec {
let mut lps = vec![0; pattern.len()];
let mut length = 0;
let mut i = 1;
while i < pattern.len() {
if pattern.as_bytes()[i] == pattern.as_bytes()[length] {
length += 1;
lps[i] = length;
i += 1;
} else if length != 0 {
length = lps[length - 1];
} else {
lps[i] = 0;
i += 1;
}
}
lps
}
fn kmp_search(text: &str, pattern: &str) -> Vec {
let lps = compute_lps(pattern);
let mut result = Vec::new();
let mut i = 0; // index for text
let mut j = 0; // index for pattern
while i < text.len() {
if text.as_bytes()[i] == pattern.as_bytes()[j] {
i += 1;
j += 1;
}
if j == pattern.len() {
result.push(i - j);
j = lps[j - 1];
} else if i < text.len() && text.as_bytes()[i] != pattern.as_bytes()[j] {
if j != 0 {
j = lps[j - 1];
} else {
i += 1;
}
}
}
result
}
This code finds all occurrences of a pattern inside a text efficiently using the KMP algorithm.
Look at the loops and repeated steps:
- Primary operation: The main while loop in
kmp_searchthat scans the text once. - How many times: Each character in the text is visited at most once; the pattern index moves forward or backward using the lps array.
- Secondary operation: The
compute_lpsfunction loops over the pattern once to build the lps array.
The time grows roughly in a straight line with the size of the text and pattern.
| Input Size (n = text length) | Approx. Operations |
|---|---|
| 10 | About 20 steps (10 for text + 10 for pattern) |
| 100 | About 200 steps |
| 1000 | About 2000 steps |
Pattern observation: Doubling the input roughly doubles the work, showing linear growth.
Time Complexity: O(n + m)
This means the algorithm checks the text and pattern in a way that grows linearly with their sizes, making it very efficient.
[X] Wrong: "KMP takes quadratic time because it has nested loops."
[OK] Correct: Although there are loops inside loops, the pattern index never moves backward more than necessary, so each character is processed only a few times, keeping the total steps linear.
Understanding KMP's time complexity shows you can analyze efficient algorithms that avoid repeated work, a key skill in coding challenges and real projects.
"What if we used a naive pattern search instead of KMP? How would the time complexity change?"
Practice
KMP Pattern Matching Algorithm?Solution
Step 1: Understand the problem KMP solves
KMP is designed to search for a pattern inside a larger text efficiently without rechecking characters unnecessarily.Step 2: Compare with other options
Sorting, reversing strings, and factorial calculations are unrelated to pattern searching.Final Answer:
To find occurrences of a pattern string within a text efficiently -> Option AQuick Check:
KMP = Pattern search [OK]
- Confusing KMP with sorting algorithms
- Thinking KMP reverses strings
- Assuming KMP calculates mathematical functions
Solution
Step 1: Understand prefix table initialization
The prefix table (lps) needs to be mutable and initialized with zeros for the length of the pattern.Step 2: Check each option
let mut lps: Vec<usize> = vec![0; pattern.len()]; correctly creates a mutable vector of usize with zeros. let lps = [0; pattern.len()]; creates an array but Rust arrays need constant size. let mut lps = Vec::new(); creates an empty vector without size. let lps: Vec<i32> = Vec::with_capacity(pattern.len()); creates a vector with capacity but no elements initialized.Final Answer:
let mut lps: Vec<usize> = vec![0; pattern.len()]; -> Option DQuick Check:
Mutable zero-filled vector = let mut lps: Vec<usize> = vec![0; pattern.len()]; [OK]
- Using fixed-size arrays without constant size
- Not initializing vector elements
- Using wrong data types for lps
fn main() {
let text = "ABABABCABABC";
let pattern = "ABABC";
let positions = kmp_search(text, pattern);
println!("{:?}", positions);
}
fn kmp_search(text: &str, pattern: &str) -> Vec<usize> {
let lps = compute_lps(pattern);
let mut result = Vec::new();
let mut i = 0;
let mut j = 0;
while i < text.len() {
if text.as_bytes()[i] == pattern.as_bytes()[j] {
i += 1;
j += 1;
if j == pattern.len() {
result.push(i - j);
j = lps[j - 1];
}
} else if j > 0 {
j = lps[j - 1];
} else {
i += 1;
}
}
result
}
fn compute_lps(pattern: &str) -> Vec<usize> {
let mut lps = vec![0; pattern.len()];
let mut length = 0;
let mut i = 1;
while i < pattern.len() {
if pattern.as_bytes()[i] == pattern.as_bytes()[length] {
length += 1;
lps[i] = length;
i += 1;
} else if length > 0 {
length = lps[length - 1];
} else {
lps[i] = 0;
i += 1;
}
}
lps
}Solution
Step 1: Understand KMP search output
The function returns starting indices where the pattern "ABABC" appears in the text "ABABABCABABC".Step 2: Trace pattern matches
Pattern matches at index 2 ("ABABC" in "ABABABC") and index 7 ("ABABC" in "ABABC"), so output is [2, 7].Final Answer:
[2, 7] -> Option BQuick Check:
Pattern found at indices 2 and 7 [OK]
- Counting overlapping matches incorrectly
- Confusing indices starting at 0 or 1
- Misreading pattern occurrences
fn compute_lps(pattern: &str) -> Vec<usize> {
let mut lps = vec![0; pattern.len()];
let mut length = 0;
let mut i = 0;
while i < pattern.len() {
if pattern.as_bytes()[i] == pattern.as_bytes()[length] {
length += 1;
lps[i] = length;
i += 1;
} else if length > 0 {
length = lps[length - 1];
} else {
lps[i] = 0;
i += 1;
}
}
lps
}Solution
Step 1: Analyze loop initialization
The prefix table computation starts comparing from the second character, so i must start at 1, not 0.Step 2: Check other variables and return type
Length starts at 0 correctly, lps vector type usize is correct, and returning a vector is expected.Final Answer:
The loop should start with i = 1, not 0 -> Option AQuick Check:
Start i at 1 for prefix table [OK]
- Starting loop at 0 causing incorrect prefix values
- Changing length initialization wrongly
- Wrong return type assumptions
Solution
Step 1: Understand KMP time complexity
KMP processes the text and pattern in linear time by using the prefix table to skip unnecessary comparisons.Step 2: Compare with other complexities
Naive search is O(n*m), binary search is unrelated, and prefix table computation is O(m), not quadratic.Final Answer:
O(n + m) because KMP avoids rechecking characters using the prefix table -> Option CQuick Check:
KMP = linear time O(n + m) [OK]
- Assuming nested loops cause O(n*m)
- Confusing prefix table with binary search
- Thinking prefix table is quadratic
Ready to prove it?
Pass the test and this pattern moves unknown
