Hi👋SpeedAlgo • Deliberate Practice & Cognitive Ergonomics for Software Engineers

An interactive algorithm mastery and technical interview preparation platform published by Hi👋WebEnterprise. Built for senior and staff software engineers preparing for rigorous coding screens at top tech companies (FAANG/MAMAA).

12 Core Algorithmic Patterns & 189 Practice Problems

  • 1. Two Pointers (10 Paradigms, 34 Problems): Converging pointers, sorted pair sums, container with most water, trapping rain water, 3Sum, plus the Sliding Window and Fast & Slow Pointers paradigms (Floyd cycle detection, monotonic window invariants, longest substrings, minimum window).
  • 2. Binary Search (8 Paradigms, 12 Problems): Monotonic predicate partitioning, boundary searching, rotated arrays, median of two sorted arrays, matrix median on value range.
  • 3. Bit Manipulation (5 Paradigms, 8 Problems): Bitmasking, XOR tricks, counting set bits, subset enumeration via bitmasks.
  • 4. Math & Geometry (7 Paradigms, 13 Problems): Sieve of Eratosthenes, integer/roman conversions, modular arithmetic, geometric simulation.
  • 5. Tree/Graph Depth-First Search (10 Paradigms, 19 Problems): Path sums, lowest common ancestor, tree diameter, subtree serialization, validating BSTs.
  • 6. Tree/Graph Breadth-First Search (4 Paradigms, 11 Problems): Level-order traversals, shortest path, rotting oranges, word ladders.
  • 7. Graphs (10 Paradigms, 18 Problems): Topological sort, cycle detection, Dijkstra, bipartite validation, network delay.
  • 8. Backtracking (6 Paradigms, 14 Problems): Subsets, permutations, combinations, constraint satisfaction, pruning, N-Queens.
  • 9. Dynamic Programming (11 Paradigms, 18 Problems): Memoization vs tabulation, knapsack, coin change, edit distance, longest common subsequence.
  • 10. Heap / Priority Queue (8 Paradigms, 10 Problems): Running medians, top-k elements, interval scheduling, IPO, k-way merges.
  • 11. Advanced Data Structures (6 Paradigms, 14 Problems): Trie, Union-Find, LRU Cache, LFU Cache, Monotonic Stacks.
  • 12. Intervals & Stack / Miscellaneous (11 Paradigms, 18 Problems): Merge intervals, daily temperatures, largest rectangle in histogram, trapping rain water via stack.

4-Stage Deliberate Practice Framework

  1. Stage 1 (Compare & Learn): Multi-language Rosetta Stone contrasting abstract invariants with concrete solutions across Python, C#, Java, TypeScript, C++, Go, and Rust.
  2. Stage 2 (Active Recall): Reconstruct algorithmic template invariants from memory with real-time feedback before looking at solutions.
  3. Stage 3 (Senior SWE AI Mock Coach): Simulated senior mock interview evaluating Big-O space/time tradeoffs, edge cases, and code reviews in Monaco Editor.
  4. Stage 4 (Solve on Your Own): Timed sandbox challenges verified against automated test suites in Python, C#, Java, and TypeScript.

Equipped with SM-2 Spaced Repetition Review Hub, Studio Cockpit workspace layout, and interactive study notes.

Pricing, Access & Commercial Terms

  • Core Curriculum: 100% Free. No credit card required.
  • Compute Coins: 40 free coins upon signup, +20 daily login bonus, +25 referral bonus.
  • 24-Hour AI Coaching Pass: 5 compute coins unlocks unlimited senior SWE AI coaching for a full 24 hours.
  • BYOK (Bring Your Own Key): Completely free unlimited AI coaching if using your own Gemini/OpenAI API key.
  • Refund & Subscription Policy: No recurring charges, no subscription traps, and no paid paywalls. Free tier provides full learning path.
  • Platform Operator: Hi👋WebEnterprise Inc. Support & policies at hispeedalgo.com.
Skip to main content
Hi👋SpeedAlgo

Invariant-First Algorithmic Mastery

201Items
Theory Context•Miscellaneous & Sweeps
EasyLC 28

Find the Index of the First Occurrence in a String (LeetCode 28)

You will see how the KMP prefix function finds a word in a text without ever reading a character of the text twice.

Target Frequency:GoogleMetaMicrosoft

You get two strings, a text haystack and a word needle. Look for the places where needle appears in haystack as a block of consecutive characters, and return the index in haystack where the leftmost of them begins.

If needle appears nowhere in haystack, return -1.

Worked Examples

Example 1
Input:haystack = "sadbutsad", needle = "sad"
Output:0
s0a1d2b3u4t5s6a7d8first sadsecond sad
Explanation: The scan stops at the first full match, which starts at index `0`; the second copy of `"sad"`, at index `6`, is never reached.
Example 2
Input:haystack = "leetcode", needle = "leeto"
Output:-1
l0e1e2t3c4o5d6e7c != o
Explanation: `"leet"` matches at index 0, but the next letter is `c`, not `o`, and `"leeto"` appears nowhere else, so the answer is `-1`.

⚖️Formal Constraints & Bounds

  • 1 <= haystack.length, needle.length <= 104

  • haystack and needle consist of only lowercase English characters.

Deep-Dive & Conceptual Insights

Why It Works & Core Invariant

After a mismatch, the characters just matched are a prefix of needle you already know, so the next place a match could start is set by needle alone: lps records, for every prefix, its longest border, and the search falls back to it instead of re-reading haystack.

Real-World Scenario & Production Applications

Searching a log stream for an error signature, a packet stream for an attack signature, or a DNA read for a marker sequence: the text arrives once and is long, so a search that backs up and re-reads it after every near-miss wastes work, or can't back up at all on a stream. KMP reads each character of the text exactly once.

Step-by-Step Execution Trace Table

The debugger's first preset, the trap case haystack = "abababc", needle = "ababc" (lps = [0, 0, 1, 2, 0]):

ichk beforeWhat the code doesk after
0a0ch == needle[0]: extend1
1b1ch == needle[1]: extend2
2a2ch == needle[2]: extend3
3b3ch == needle[3]: extend4
4a4a != needle[4] (c): k = lps[3] = 2, the border "ab" is kept; then a == needle[2]: extend3
5b3ch == needle[3]: extend4
6c4ch == needle[4]: extend, k == m5
return 6 - 5 + 1 = 2. Resetting k to 0 at i = 4 would have lost the match and returned -1.
Scroll horizontally to see all columns, or expand to full screen
Core Invariant Specification & Code Shape
Archetype Code Shape
Python
1After a mismatch, the letters you just matched are a prefix of `needle`, so you already know them: the next possible match is set by `needle` alone, not by re-reading `haystack`.
2Keep this true: after reading `haystack[i]`, `k` is the length of the longest prefix of `needle` that ends at `i`. `lps[j]` is the longest proper prefix of `needle[:j + 1]` that is also its suffix.
3The shape: two loops of the same form, first one over `needle` from index 1 that fills `lps`, then one over `haystack`. Inside each, shrink `k` while the next letter doesn't fit, then grow it on a match; the second loop stops as soon as all of `needle` is matched.
4The trap: on a mismatch set `k = lps[k - 1]`, never `k = 0`. On `"abababc"` with `needle = "ababc"` the reset loses the border `"ab"` and the only match, at index `2`.

Target: Find the Index of the First Occurrence in a String (LeetCode 28). `lps[i]` is the longest proper prefix of `needle[:i + 1]` that is also its suffix; it depends on `needle` only.

Boundary Model: Sorted Coordinate Sweep-Line & Monotonic Stack

Intervals sorted by start coordinate; monotonic stack preserves strictly increasing or decreasing element values.

Loop Invariant Termination

Sweep: if curr.start <= prev.end: merge; Stack: while stack and nums[i] >= stack[-1]: stack.pop().

Conceptual Narrative

🧭 Conceptual Foundation & Pattern Intuition

The simple search tries every start in haystack and compares needle letter by letter, so after a near-miss it moves one step right and re-reads text it has already seen. That is up to N · M comparisons. KMP never re-reads: when a mismatch comes after k matched letters, those k letters are needle[:k], which you already know. The longest proper prefix of needle[:k] that is also its suffix, its longest border, is the most you can keep, because the next possible match must start at a suffix of what was read that is also a prefix of needle. lps[k - 1] stores that length, computed once from needle alone, so the search sets k = lps[k - 1] and tries the same text letter again.

🎼 The Analogy: Joining a Chant Halfway

You are listening for a chant, "ba-ba-ba-da", in a crowd. You hear "ba-ba-ba-ba" and the last syllable is wrong. You don't rewind the crowd and start listening from scratch: the last "ba-ba" you heard is already the start of the chant, so you carry on as if you had heard two syllables. Knowing, for every point in the chant, how much of it you may keep after a wrong syllable is exactly the lps table.

🪄 The Mathematical Harmony / Magic Trick
Code / Blueprint
while k > 0 and ch != needle[k]:
k = lps[k - 1]
if ch == needle[k]:
k += 1
if k == m:
return i - m + 1
 

k is how much of needle ends at i. A mismatch shrinks k to the next shorter border, lps[k - 1], again and again until the letter fits or k is 0; a match grows it by one. Each letter of haystack raises k at most once and every fall back lowers it, so all the fall backs together cost at most N: the search is linear. The trap is resetting k = 0 on a mismatch: on "abababc" with needle = "ababc" it throws away the border "ab" at i = 4, which is where the only match begins.

💡 Summary

Build lps for needle with the same fall-back loop, then walk haystack once: fall back with k = lps[k - 1] while the letter doesn't fit, extend on a match, and return i - m + 1 when k == m. O(N+M)O(N + M)O(N+M) time, O(M)O(M)O(M) space for lps.

  • Resetting on a mismatch (the trap): k = 0 throws away the border of what was just matched. On haystack = "abababc", needle = "ababc" the only match starts inside the failed attempt at index 0, so the reset returns -1 instead of 2; fall back with k = lps[k - 1].

  • if instead of while: one letter may need several fall backs. On haystack = "aabaa", needle = "aaa" a single if leaves k = 1 at the b and reports a match at index 2 that isn't there; the answer is -1.

  • lps[k] instead of lps[k - 1]: after k matched letters, the border you need is that of needle[:k], stored at lps[k - 1]; lps[k] belongs to a prefix one letter longer.

  • Returning the end: the match ends at i, so it starts at i - m + 1.

  • Trying every start: correct, but it re-reads the text after every near-miss: about 2.5 * 107 letter comparisons on "a" * 104 with needle = "a" * 4999 + "b".

Senior SWE Reasoning Architecture

4-Phase Thought Process Model

You will see how a senior engineer spots a pattern search and defends the KMP fall back out loud.

Pattern Recognition Signals

The 10-second spot

Find needle in haystack "as a block of consecutive characters" and return "the leftmost of them": an exact pattern search in a text that can be long. Trying every start re-reads the text after every near-miss; KMP reads it once.

Formulating the Predicate & Invariants

Turning intuition into a boolean rule

After reading haystack[i], k is the length of the longest prefix of needle that ends at i. On a mismatch k = lps[k - 1] until ch == needle[k] or k == 0; on a match k += 1; at k == m the answer is i - m + 1.

Silent Failure Traps & Edge Cases

Where confident candidates still lose points

  • Fall back to k = lps[k - 1], not k = 0: on haystack = "abababc", needle = "ababc" the reset throws away the border "ab" at i = 4, where the only match begins, and returns -1 instead of 2.

  • while, not if: one letter may need several fall backs. On haystack = "aabaa", needle = "aaa" a single if leaves k = 1 at the b, then counts the b as nothing and reports a match at 2 that isn't there (the answer is -1).

  • Use lps[k - 1], the border of the k letters matched, not lps[k]: lps[k] describes a prefix one letter longer than what was read.

  • Return the start, i - m + 1, not i: the match ends at i.

The 60-Second Interview Pitch

Say this out loud before you type a single line

I'd use KMP. The naive search tries every start and re-reads the text after each near-miss, which is up to N times M comparisons. KMP never re-reads. First I build a table from the needle alone: for each prefix, the length of its longest proper prefix that is also a suffix. Then I scan the text once, keeping k, how much of the needle ends at the current letter. On a match I extend k; on a mismatch I don't go back in the text, I fall back to the table value for the k letters I just matched, because that border is the longest part of what I read that can still start a match, and I repeat while the letter doesn't fit. The trap is resetting k to zero, which loses matches that start inside a failed attempt. Each letter raises k at most once, so the scan is linear: O(N plus M) time and O(M) space.

So: lps from the pattern alone, one forward pass over the text, and k = lps[k - 1] on every mismatch.

Big-O Invariant Derivation

Complexity & Mathematical Proof

TIME COMPLEXITY

O(N + M)

Look at the second loop: for i, ch in enumerate(haystack) runs N times, and each round does k += 1 at most once. The inner while looks quadratic, but every k = lps[k - 1] makes k strictly smaller, because lps[k - 1] < k, and k never goes below 0. k rises at most N times in all, so it can fall at most N times in all: the whole loop is O(N). The first loop is the same argument on needle against itself, O(M). Total: O(N + M).

SPACE COMPLEXITY

O(M)

lps holds M integers; k, i and ch are single values. Space: O(M), and the answer is one integer.

Formal Recurrence Relation

T(N, M) = O(M) to build lps + O(N) to scan = O(N + M)

Look at the second loop: for i, ch in enumerate(haystack) runs N times, and each round does k += 1 at most once. The inner while looks quadratic, but every k = lps[k - 1] makes k strictly smaller, because lps[k - 1] < k, and k never goes below 0. k rises at most N times in all, so it can fall at most N times in all: the whole loop is O(N). The first loop is the same argument on needle against itself, O(M). Total: O(N + M).

Derivation Progression

Build lps

O(M)

for i in range(1, m): k += 1 at most once per round, and each k = lps[k - 1] lowers k, so the fall backs add up to at most M.

Scan haystack

O(N)

for i, ch in enumerate(haystack): k rises at most once per letter, so it falls at most N times in all.

Total

O(N + M)

Two linear passes; no letter of haystack is read twice.

Variable Definitions

NNN

Length of haystack

MMM

Length of needle, m in the code

Memory Architecture & Bounds

🟣 Call Stack

O(1) Iterative, no recursion

🔵 Auxiliary Heap

O(M): lps

🟢 Output Space

O(1): one integer

Boundary Best / Worst Cases

Best Case

O(M)O(M)O(M) plus a short scan: a match at index 0 returns after M letters of haystack

Average Case

O(N+M)O(N + M)O(N+M)

Worst Case

O(N+M)O(N + M)O(N+M): no match, or a match at the very end

Recurrence Tree Topology

Recurrence Tree Topology
Synthesizing vector architecture diagram...
Staff+ Engineering Perspective

Senior SWE Deconstruction & Hardware Caveats

ARCHITECTURAL SIGNALS & INTERVIEW TRIGGERS

Triggers: "as a block of consecutive characters", "the leftmost of them". Find a word in a long text, the text read once: KMP, a border table of the word and a scan that never backs up.

CONSTRAINTS & BOUNDS

N,M≤104N, M \le 10^4N,M≤104. Trying every start is O(N⋅M)O(N \cdot M)O(N⋅M), about 2.5⋅1072.5 \cdot 10^72.5⋅107 letter comparisons when needle is half of haystack; KMP is O(N+M)O(N + M)O(N+M), and a rolling hash (Rabin-Karp) is O(N+M)O(N + M)O(N+M) expected.

FAANG PRODUCTION TRAPS & EDGE CASES

Fall back with k = lps[k - 1], never k = 0. On a stream, KMP needs only lps and k (O(M) memory) and never buffers the text, which a naive search can't do. For many patterns at once, the same border idea becomes the Aho-Corasick automaton; for one pattern, str.find in CPython is already a linear-time search in C.

Core Algorithmic State Invariants

1. A Mismatch Keeps the Longest Border

After `k` matched letters, `needle[:k]` is known, so `k = lps[k - 1]` keeps its longest border: the longest part of what was read that can still start a match. Resetting to `0` would miss a match that begins inside a failed attempt.

2. The Pattern Describes Itself

`lps` is built from `needle` alone, with the same `while k > 0 and needle[i] != needle[k]: k = lps[k - 1]` loop the search uses: building it is searching `needle` in itself.

3. Never Read the Text Twice

`i` only moves forward and each letter raises `k` at most once, so all the fall backs together are at most `N`: O(N + M) time, O(M) space for `lps`.

Theory Context•Miscellaneous & Sweeps
EasyLC 28

Find the Index of the First Occurrence in a String (LeetCode 28)

You will see how the KMP prefix function finds a word in a text without ever reading a character of the text twice.

Target Frequency:GoogleMetaMicrosoft

You get two strings, a text haystack and a word needle. Look for the places where needle appears in haystack as a block of consecutive characters, and return the index in haystack where the leftmost of them begins.

If needle appears nowhere in haystack, return -1.

Worked Examples

Example 1
Input:haystack = "sadbutsad", needle = "sad"
Output:0
s0a1d2b3u4t5s6a7d8first sadsecond sad
Explanation: The scan stops at the first full match, which starts at index `0`; the second copy of `"sad"`, at index `6`, is never reached.
Example 2
Input:haystack = "leetcode", needle = "leeto"
Output:-1
l0e1e2t3c4o5d6e7c != o
Explanation: `"leet"` matches at index 0, but the next letter is `c`, not `o`, and `"leeto"` appears nowhere else, so the answer is `-1`.

⚖️Formal Constraints & Bounds

  • 1 <= haystack.length, needle.length <= 104

  • haystack and needle consist of only lowercase English characters.

Deep-Dive & Conceptual Insights

Why It Works & Core Invariant

After a mismatch, the characters just matched are a prefix of needle you already know, so the next place a match could start is set by needle alone: lps records, for every prefix, its longest border, and the search falls back to it instead of re-reading haystack.

Real-World Scenario & Production Applications

Searching a log stream for an error signature, a packet stream for an attack signature, or a DNA read for a marker sequence: the text arrives once and is long, so a search that backs up and re-reads it after every near-miss wastes work, or can't back up at all on a stream. KMP reads each character of the text exactly once.

Step-by-Step Execution Trace Table

The debugger's first preset, the trap case haystack = "abababc", needle = "ababc" (lps = [0, 0, 1, 2, 0]):

ichk beforeWhat the code doesk after
0a0ch == needle[0]: extend1
1b1ch == needle[1]: extend2
2a2ch == needle[2]: extend3
3b3ch == needle[3]: extend4
4a4a != needle[4] (c): k = lps[3] = 2, the border "ab" is kept; then a == needle[2]: extend3
5b3ch == needle[3]: extend4
6c4ch == needle[4]: extend, k == m5
return 6 - 5 + 1 = 2. Resetting k to 0 at i = 4 would have lost the match and returned -1.
Scroll horizontally to see all columns, or expand to full screen
Core Invariant Specification & Code Shape
Archetype Code Shape
Python
1After a mismatch, the letters you just matched are a prefix of `needle`, so you already know them: the next possible match is set by `needle` alone, not by re-reading `haystack`.
2Keep this true: after reading `haystack[i]`, `k` is the length of the longest prefix of `needle` that ends at `i`. `lps[j]` is the longest proper prefix of `needle[:j + 1]` that is also its suffix.
3The shape: two loops of the same form, first one over `needle` from index 1 that fills `lps`, then one over `haystack`. Inside each, shrink `k` while the next letter doesn't fit, then grow it on a match; the second loop stops as soon as all of `needle` is matched.
4The trap: on a mismatch set `k = lps[k - 1]`, never `k = 0`. On `"abababc"` with `needle = "ababc"` the reset loses the border `"ab"` and the only match, at index `2`.

Target: Find the Index of the First Occurrence in a String (LeetCode 28). `lps[i]` is the longest proper prefix of `needle[:i + 1]` that is also its suffix; it depends on `needle` only.

Boundary Model: Sorted Coordinate Sweep-Line & Monotonic Stack

Intervals sorted by start coordinate; monotonic stack preserves strictly increasing or decreasing element values.

Loop Invariant Termination

Sweep: if curr.start <= prev.end: merge; Stack: while stack and nums[i] >= stack[-1]: stack.pop().

Conceptual Narrative

🧭 Conceptual Foundation & Pattern Intuition

The simple search tries every start in haystack and compares needle letter by letter, so after a near-miss it moves one step right and re-reads text it has already seen. That is up to N · M comparisons. KMP never re-reads: when a mismatch comes after k matched letters, those k letters are needle[:k], which you already know. The longest proper prefix of needle[:k] that is also its suffix, its longest border, is the most you can keep, because the next possible match must start at a suffix of what was read that is also a prefix of needle. lps[k - 1] stores that length, computed once from needle alone, so the search sets k = lps[k - 1] and tries the same text letter again.

🎼 The Analogy: Joining a Chant Halfway

You are listening for a chant, "ba-ba-ba-da", in a crowd. You hear "ba-ba-ba-ba" and the last syllable is wrong. You don't rewind the crowd and start listening from scratch: the last "ba-ba" you heard is already the start of the chant, so you carry on as if you had heard two syllables. Knowing, for every point in the chant, how much of it you may keep after a wrong syllable is exactly the lps table.

🪄 The Mathematical Harmony / Magic Trick
Code / Blueprint
while k > 0 and ch != needle[k]:
k = lps[k - 1]
if ch == needle[k]:
k += 1
if k == m:
return i - m + 1
 

k is how much of needle ends at i. A mismatch shrinks k to the next shorter border, lps[k - 1], again and again until the letter fits or k is 0; a match grows it by one. Each letter of haystack raises k at most once and every fall back lowers it, so all the fall backs together cost at most N: the search is linear. The trap is resetting k = 0 on a mismatch: on "abababc" with needle = "ababc" it throws away the border "ab" at i = 4, which is where the only match begins.

💡 Summary

Build lps for needle with the same fall-back loop, then walk haystack once: fall back with k = lps[k - 1] while the letter doesn't fit, extend on a match, and return i - m + 1 when k == m. O(N+M)O(N + M)O(N+M) time, O(M)O(M)O(M) space for lps.

  • Resetting on a mismatch (the trap): k = 0 throws away the border of what was just matched. On haystack = "abababc", needle = "ababc" the only match starts inside the failed attempt at index 0, so the reset returns -1 instead of 2; fall back with k = lps[k - 1].

  • if instead of while: one letter may need several fall backs. On haystack = "aabaa", needle = "aaa" a single if leaves k = 1 at the b and reports a match at index 2 that isn't there; the answer is -1.

  • lps[k] instead of lps[k - 1]: after k matched letters, the border you need is that of needle[:k], stored at lps[k - 1]; lps[k] belongs to a prefix one letter longer.

  • Returning the end: the match ends at i, so it starts at i - m + 1.

  • Trying every start: correct, but it re-reads the text after every near-miss: about 2.5 * 107 letter comparisons on "a" * 104 with needle = "a" * 4999 + "b".

Senior SWE Reasoning Architecture

4-Phase Thought Process Model

You will see how a senior engineer spots a pattern search and defends the KMP fall back out loud.

Pattern Recognition Signals

The 10-second spot

Find needle in haystack "as a block of consecutive characters" and return "the leftmost of them": an exact pattern search in a text that can be long. Trying every start re-reads the text after every near-miss; KMP reads it once.

Formulating the Predicate & Invariants

Turning intuition into a boolean rule

After reading haystack[i], k is the length of the longest prefix of needle that ends at i. On a mismatch k = lps[k - 1] until ch == needle[k] or k == 0; on a match k += 1; at k == m the answer is i - m + 1.

Silent Failure Traps & Edge Cases

Where confident candidates still lose points

  • Fall back to k = lps[k - 1], not k = 0: on haystack = "abababc", needle = "ababc" the reset throws away the border "ab" at i = 4, where the only match begins, and returns -1 instead of 2.

  • while, not if: one letter may need several fall backs. On haystack = "aabaa", needle = "aaa" a single if leaves k = 1 at the b, then counts the b as nothing and reports a match at 2 that isn't there (the answer is -1).

  • Use lps[k - 1], the border of the k letters matched, not lps[k]: lps[k] describes a prefix one letter longer than what was read.

  • Return the start, i - m + 1, not i: the match ends at i.

The 60-Second Interview Pitch

Say this out loud before you type a single line

I'd use KMP. The naive search tries every start and re-reads the text after each near-miss, which is up to N times M comparisons. KMP never re-reads. First I build a table from the needle alone: for each prefix, the length of its longest proper prefix that is also a suffix. Then I scan the text once, keeping k, how much of the needle ends at the current letter. On a match I extend k; on a mismatch I don't go back in the text, I fall back to the table value for the k letters I just matched, because that border is the longest part of what I read that can still start a match, and I repeat while the letter doesn't fit. The trap is resetting k to zero, which loses matches that start inside a failed attempt. Each letter raises k at most once, so the scan is linear: O(N plus M) time and O(M) space.

So: lps from the pattern alone, one forward pass over the text, and k = lps[k - 1] on every mismatch.

Big-O Invariant Derivation

Complexity & Mathematical Proof

TIME COMPLEXITY

O(N + M)

Look at the second loop: for i, ch in enumerate(haystack) runs N times, and each round does k += 1 at most once. The inner while looks quadratic, but every k = lps[k - 1] makes k strictly smaller, because lps[k - 1] < k, and k never goes below 0. k rises at most N times in all, so it can fall at most N times in all: the whole loop is O(N). The first loop is the same argument on needle against itself, O(M). Total: O(N + M).

SPACE COMPLEXITY

O(M)

lps holds M integers; k, i and ch are single values. Space: O(M), and the answer is one integer.

Formal Recurrence Relation

T(N, M) = O(M) to build lps + O(N) to scan = O(N + M)

Look at the second loop: for i, ch in enumerate(haystack) runs N times, and each round does k += 1 at most once. The inner while looks quadratic, but every k = lps[k - 1] makes k strictly smaller, because lps[k - 1] < k, and k never goes below 0. k rises at most N times in all, so it can fall at most N times in all: the whole loop is O(N). The first loop is the same argument on needle against itself, O(M). Total: O(N + M).

Derivation Progression

Build lps

O(M)

for i in range(1, m): k += 1 at most once per round, and each k = lps[k - 1] lowers k, so the fall backs add up to at most M.

Scan haystack

O(N)

for i, ch in enumerate(haystack): k rises at most once per letter, so it falls at most N times in all.

Total

O(N + M)

Two linear passes; no letter of haystack is read twice.

Variable Definitions

NNN

Length of haystack

MMM

Length of needle, m in the code

Memory Architecture & Bounds

🟣 Call Stack

O(1) Iterative, no recursion

🔵 Auxiliary Heap

O(M): lps

🟢 Output Space

O(1): one integer

Boundary Best / Worst Cases

Best Case

O(M)O(M)O(M) plus a short scan: a match at index 0 returns after M letters of haystack

Average Case

O(N+M)O(N + M)O(N+M)

Worst Case

O(N+M)O(N + M)O(N+M): no match, or a match at the very end

Recurrence Tree Topology

Recurrence Tree Topology
Synthesizing vector architecture diagram...
Staff+ Engineering Perspective

Senior SWE Deconstruction & Hardware Caveats

ARCHITECTURAL SIGNALS & INTERVIEW TRIGGERS

Triggers: "as a block of consecutive characters", "the leftmost of them". Find a word in a long text, the text read once: KMP, a border table of the word and a scan that never backs up.

CONSTRAINTS & BOUNDS

N,M≤104N, M \le 10^4N,M≤104. Trying every start is O(N⋅M)O(N \cdot M)O(N⋅M), about 2.5⋅1072.5 \cdot 10^72.5⋅107 letter comparisons when needle is half of haystack; KMP is O(N+M)O(N + M)O(N+M), and a rolling hash (Rabin-Karp) is O(N+M)O(N + M)O(N+M) expected.

FAANG PRODUCTION TRAPS & EDGE CASES

Fall back with k = lps[k - 1], never k = 0. On a stream, KMP needs only lps and k (O(M) memory) and never buffers the text, which a naive search can't do. For many patterns at once, the same border idea becomes the Aho-Corasick automaton; for one pattern, str.find in CPython is already a linear-time search in C.

Core Algorithmic State Invariants

1. A Mismatch Keeps the Longest Border

After `k` matched letters, `needle[:k]` is known, so `k = lps[k - 1]` keeps its longest border: the longest part of what was read that can still start a match. Resetting to `0` would miss a match that begins inside a failed attempt.

2. The Pattern Describes Itself

`lps` is built from `needle` alone, with the same `while k > 0 and needle[i] != needle[k]: k = lps[k - 1]` loop the search uses: building it is searching `needle` in itself.

3. Never Read the Text Twice

`i` only moves forward and each letter raises `k` at most once, so all the fall backs together are at most `N`: O(N + M) time, O(M) space for `lps`.

Rosetta Dual-Monaco Comparison
Python 3
CANONICAL INVARIANT TEMPLATE
Loading...
CONCRETE: FIND THE INDEX OF THE FIRST OCCURRENCE IN A STRING (LEETCODE 28)
T = O(N + M)S = O(M)
Loading...
Pattern Implementation Mapping TableCanonical Invariant ⟷ Concrete Code ⟷ Engineering Rationale
Canonical InvariantConcrete CodeEngineering Rationale
The border table of the patternlps = [0] * m`lps[i]` is the longest proper prefix of `needle[:i + 1]` that is also its suffix; it depends on `needle` only.
Build it with the same fall-back loopwhile k > 0 and needle[i] != needle[k]: k = lps[k - 1]Building `lps` is the search run on `needle` against itself.
Read the text oncefor i, ch in enumerate(haystack):`i` only moves forward; nothing in `haystack` is read twice.
On a mismatch, keep the longest borderwhile k > 0 and ch != needle[k]: k = lps[k - 1]The trap: `k = lps[k - 1]`, not `k = 0`, so a match that starts inside a failed attempt is not lost.
On a match, extendif ch == needle[k]: k += 1`k` counts the letters of `needle` that end at `i`.
The whole pattern has matchedif k == m: return i - m + 1The match ends at `i`, so it starts `m - 1` letters earlier.
© 2026 Hi👋WebEnterprise. All rights reserved.
Sitemap•llms.txt•