beam: KMP substring search primitive with tests
This commit is contained in:
1 parent
2e073df612
commit
c6ed0b223f
2 files changed
+102
No files matched your search
@@ -0,0 +1,50 @@
|
|||||||
|
"""Knuth-Morris-Pratt substring search.
|
||||||
|
|
||||||
|
Provides:
|
||||||
|
- build_pi(pattern): prefix (failure) function, O(m)
|
||||||
|
- search(text, pattern): all start indices, O(n+m)
|
||||||
|
- search_count(text, pattern): number of (possibly overlapping) occurrences
|
||||||
|
|
||||||
|
Correctness notes:
|
||||||
|
* Empty pattern matches at every position (0..len(text)) per Python str.find semantics? We return [] for empty pattern to avoid O(n) noise; callers wanting positions can handle separately. Actually we return list(range(len(text)+1)) to match str.find('') behaviour? Tests pin: empty pattern -> [] (documented).
|
||||||
|
* Overlapping occurrences are reported.
|
||||||
|
"""
|
||||||
|
|
||||||
|
from typing import List
|
||||||
|
|
||||||
|
|
||||||
|
def build_pi(pattern: str) -> List[int]:
|
||||||
|
m = len(pattern)
|
||||||
|
pi = [0] * m
|
||||||
|
k = 0
|
||||||
|
for i in range(1, m):
|
||||||
|
while k > 0 and pattern[i] != pattern[k]:
|
||||||
|
k = pi[k - 1]
|
||||||
|
if pattern[i] == pattern[k]:
|
||||||
|
k += 1
|
||||||
|
pi[i] = k
|
||||||
|
return pi
|
||||||
|
|
||||||
|
|
||||||
|
def search(text: str, pattern: str) -> List[int]:
|
||||||
|
if not pattern:
|
||||||
|
return []
|
||||||
|
n, m = len(text), len(pattern)
|
||||||
|
if m > n:
|
||||||
|
return []
|
||||||
|
pi = build_pi(pattern)
|
||||||
|
res: List[int] = []
|
||||||
|
k = 0
|
||||||
|
for i in range(n):
|
||||||
|
while k > 0 and text[i] != pattern[k]:
|
||||||
|
k = pi[k - 1]
|
||||||
|
if text[i] == pattern[k]:
|
||||||
|
k += 1
|
||||||
|
if k == m:
|
||||||
|
res.append(i - m + 1)
|
||||||
|
k = pi[k - 1]
|
||||||
|
return res
|
||||||
|
|
||||||
|
|
||||||
|
def search_count(text: str, pattern: str) -> int:
|
||||||
|
return len(search(text, pattern))
|
||||||
@@ -0,0 +1,52 @@
|
|||||||
|
import unittest
|
||||||
|
from kmp import build_pi, search, search_count
|
||||||
|
|
||||||
|
|
||||||
|
class TestKMP(unittest.TestCase):
|
||||||
|
def test_pi(self):
|
||||||
|
self.assertEqual(build_pi("aabaaab"), [0,1,0,1,2,2,3])
|
||||||
|
self.assertEqual(build_pi("aaaa"), [0,1,2,3])
|
||||||
|
self.assertEqual(build_pi(""), [])
|
||||||
|
self.assertEqual(build_pi("abc"), [0,0,0])
|
||||||
|
|
||||||
|
def test_search_basic(self):
|
||||||
|
self.assertEqual(search("ababcababa", "aba"), [0,5,7])
|
||||||
|
self.assertEqual(search("aaaa", "aa"), [0,1,2])
|
||||||
|
self.assertEqual(search("hello world", "xyz"), [])
|
||||||
|
self.assertEqual(search("abc", "abcd"), [])
|
||||||
|
|
||||||
|
def test_overlapping(self):
|
||||||
|
self.assertEqual(search("aaaaa", "aaa"), [0,1,2])
|
||||||
|
|
||||||
|
def test_single(self):
|
||||||
|
self.assertEqual(search("abc", "a"), [0])
|
||||||
|
self.assertEqual(search("abc", "c"), [2])
|
||||||
|
|
||||||
|
def test_empty_pattern(self):
|
||||||
|
self.assertEqual(search("abc", ""), [])
|
||||||
|
self.assertEqual(search("", ""), [])
|
||||||
|
|
||||||
|
def test_full_text(self):
|
||||||
|
self.assertEqual(search("abc", "abc"), [0])
|
||||||
|
|
||||||
|
def test_count(self):
|
||||||
|
self.assertEqual(search_count("ababab", "ab"), 3)
|
||||||
|
|
||||||
|
def test_unicode(self):
|
||||||
|
self.assertEqual(search("cafécafé", "café"), [0,4])
|
||||||
|
|
||||||
|
def test_random_vs_naive(self):
|
||||||
|
import random
|
||||||
|
rng = random.Random(42)
|
||||||
|
alpha = "ab"
|
||||||
|
for _ in range(200):
|
||||||
|
n = rng.randint(0, 60)
|
||||||
|
m = rng.randint(1, 8)
|
||||||
|
text = "".join(rng.choice(alpha) for _ in range(n))
|
||||||
|
pat = "".join(rng.choice(alpha) for _ in range(m))
|
||||||
|
naive = [i for i in range(len(text)-m+1) if text[i:i+m] == pat]
|
||||||
|
self.assertEqual(search(text, pat), naive)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
unittest.main()
|
||||||
Reference in new issue
Block a user