Algorytm Knuth-Morris-Pratt (KMP)
1. Problem z algorytmem naiwnym
Algorytm naiwny marnuje informacje z poprzednich porównań:
Text: AAAAAAB
Pattern: AAAB
Pozycja 0: AAAA vs AAAB
✓✓✓✗
Pozycja 1: AAAA vs AAAB ← Znów porównujemy te same A!
✓✓✓✗
Problem: Nie wykorzystujemy faktu, że już znamy pierwsze 3 A!
KMP rozwiązuje ten problem!
2. Idea algorytmu KMP
Knuth-Morris-Pratt (1977) wykorzystuje preprocessing wzorca do uniknięcia zbędnych porównań.
2.1. Kluczowa obserwacja:
Gdy znajdziemy niezgodność, możemy pominąć niektóre pozycje, bo już wiemy, co jest w tekście!
Text: ABABCABABA
Pattern: ABABD
✓✓✓✓✗ (niezgodność na D)
Wiemy, że text[0:4] = "ABAB" (dopasowane)
Zamiast przesuwać pattern o 1:
ABABCABABA
ABABD ← Bez sensu! "B" != "A"
Przesuwamy inteligentnie:
ABABCABABA
ABABD ← Kontynuuj od "AB"
KMP nigdy nie cofa się w tekście! Skanuje tekst tylko raz. ✅
3. Tablica LPS (Longest Proper Prefix which is also Suffix)
3.1. Czym jest LPS?
LPS[i] = długość najdłuższego właściwego prefiksu, który jest także sufiksem dla pattern[0:i+1].
Właściwy prefiks = prefiks krótszy niż cały string.
3.2. Przykład:
Pattern: "ABABD"
i=0: "A"
Prefiksy: "" (pusty)
Sufiksy: "" (pusty)
LPS[0] = 0
i=1: "AB"
Prefiksy: "", "A"
Sufiksy: "", "B"
Wspólne: ""
LPS[1] = 0
i=2: "ABA"
Prefiksy: "", "A", "AB"
Sufiksy: "", "A", "BA"
Wspólne: "A"
LPS[2] = 1
i=3: "ABAB"
Prefiksy: "", "A", "AB", "ABA"
Sufiksy: "", "B", "AB", "BAB"
Wspólne: "AB"
LPS[3] = 2
i=4: "ABABD"
Prefiksy: "", "A", "AB", "ABA", "ABAB"
Sufiksy: "", "D", "BD", "ABD", "BABD"
Wspólne: ""
LPS[4] = 0
LPS = [0, 0, 1, 2, 0]
3.3. Zastosowanie LPS:
Gdy znajdziemy niezgodność na pattern[j], przeskakujemy do pattern[LPS[j-1]] zamiast zaczynać od początku!
4. Budowanie tablicy LPS
4.1. Algorytm:
Złożoność: O(m) – liniowa! ✅
4.2. Wizualizacja budowania LPS:
Pattern: "ABABD"
i=0: lps[0] = 0 (zawsze)
i=1: pattern[1]='B' vs pattern[0]='A'
B != A → lps[1] = 0
i=2: pattern[2]='A' vs pattern[0]='A'
A == A → length=1, lps[2] = 1
i=3: pattern[3]='B' vs pattern[1]='B'
B == B → length=2, lps[3] = 2
i=4: pattern[4]='D' vs pattern[2]='A'
D != A → cofnij: length = lps[1] = 0
D != pattern[0]='A' → lps[4] = 0
LPS = [0, 0, 1, 2, 0]
5. Wyszukiwanie KMP
5.1. Implementacja:
Złożoność: - Preprocessing: O(m) - Wyszukiwanie: O(n) - Razem: O(n + m) ✅
6. Wizualizacja działania KMP
Text: ABABCABABA
Pattern: ABABD
LPS: [0, 0, 1, 2, 0]
i=0, j=0: text[0]='A' == pattern[0]='A' ✓
i=1, j=1: text[1]='B' == pattern[1]='B' ✓
i=2, j=2: text[2]='A' == pattern[2]='A' ✓
i=3, j=3: text[3]='B' == pattern[3]='B' ✓
i=4, j=4: text[4]='C' != pattern[4]='D' ✗
Niezgodność! j=4, więc:
j = lps[j-1] = lps[3] = 2
i=4, j=2: text[4]='C' != pattern[2]='A' ✗
Niezgodność! j=2, więc:
j = lps[j-1] = lps[1] = 0
i=4, j=0: text[4]='C' != pattern[0]='A' ✗
j=0, więc i++
i=5, j=0: text[5]='A' == pattern[0]='A' ✓
i=6, j=1: text[6]='B' == pattern[1]='B' ✓
i=7, j=2: text[7]='A' == pattern[2]='A' ✓
i=8, j=3: text[8]='B' == pattern[3]='B' ✓
i=9, j=4: text[9]='A' != pattern[4]='D' ✗
Niezgodność! j=4, więc:
j = lps[j-1] = lps[3] = 2
i=9, j=2: text[9]='A' == pattern[2]='A' ✓
i=10, j=3: Koniec tekstu
Nie znaleziono dopasowania dla "ABABD"
7. Debug version z wizualizacją
8. Porównanie: KMP vs Naiwny
8.1. Najgorszy przypadek:
8.2. Liczba porównań:
Text: AAAAAAB (7 znaków)
Pattern: AAAB (4 znaki)
Algorytm naiwny:
- Pozycja 0: AAAA vs AAAB → 4 porównania
- Pozycja 1: AAAA vs AAAB → 4 porównania
- Pozycja 2: AAAA vs AAAB → 4 porównania
- Pozycja 3: AAAB vs AAAB → 4 porównania ✓
Razem: 16 porównań
KMP:
- Preprocessing: O(m) = 4 operacje
- Wyszukiwanie: każdy znak tekstu sprawdzany raz = 7 porównań
Razem: ~11 operacji (znacznie mniej!)
9. Zastosowania KMP
9.1. Znajdowanie wszystkich wystąpień:
9.2. Sprawdzanie, czy pattern występuje:
9.3. Znajdowanie pierwszego wystąpienia:
10. Modyfikacje algorytmu KMP
10.1. Case-insensitive KMP:
10.2. Liczenie wystąpień:
11. Powtarzające się wzorce w tekście
12. Benchmark KMP vs inne algorytmy
13. Podsumowanie
Algorytm Knuth-Morris-Pratt (KMP):
- Efektywny algorytm wyszukiwania wzorca
- Preprocessing wzorca → tablica LPS
- Nigdy nie cofa się w tekście ✅
- Gwarantowana złożoność O(n + m)
Złożoność:
| Operacja | Złożoność | Uwagi |
|---|---|---|
| Preprocessing | O(m) | Budowanie tablicy LPS |
| Wyszukiwanie | O(n) | Jeden przebieg tekstu |
| Razem | O(n + m) ✅ | Liniowa! Najgorszy = średni |
| Pamięć | O(m) | Tablica LPS |
Tablica LPS:
LPS[i] = długość najdłuższego właściwego prefiksu pattern[0:i+1],
który jest także sufiksem
Przykład: "ABABD" → LPS = [0, 0, 1, 2, 0]
Implementacja budowania LPS:
Implementacja KMP:
Zalety: - ✅ O(n + m) gwarantowane – nawet najgorszy przypadek! - ✅ Nigdy nie cofa się w tekście (jeden przebieg) - ✅ Efektywny dla długich wzorców - ✅ Efektywny dla powtarzających się znaków - ✅ Znajduje wszystkie wystąpienia (overlapping)
Wady: - ❌ Wymaga preprocessingu (tablica LPS) - ❌ O(m) dodatkowej pamięci - ❌ Bardziej skomplikowany niż naiwny - ❌ W praktyce często wolniejszy od Boyer-Moore
Porównanie:
| Algorytm | Najgorszy | Preprocessing | Pamięć | Prostota |
|---|---|---|---|---|
| Naiwny | O(n×m) ❌ | Brak | O(1) ✅ | Bardzo prosta |
| KMP | O(n+m) ✅ | O(m) | O(m) | Średnia |
| Boyer-Moore | O(n×m) | O(m + σ) | O(m + σ) | Złożona |
| Rabin-Karp | O(n×m) | O(m) | O(1) | Średnia |
Kiedy używać KMP:
✅ Długie wzorce (m > 100) ✅ Najgorszy przypadek krytyczny (AAAA...B) ✅ Gwarancja O(n + m) wymagana ✅ Streaming – przetwarzanie tekstu w locie ✅ Edukacja – elegancki algorytm do nauki
❌ Krótkie wzorce → naiwny wystarczy ❌ Losowe dane → Boyer-Moore szybszy ❌ Wiele wzorców → Aho-Corasick ❌ Proste zadania → Python builtin
Kluczowa obserwacja:
KMP wykorzystuje informację z poprzednich porównań!
Text: ABABCABABA
Pattern: ABABD
✓✓✓✓✗
Wiemy, że dopasowane "ABAB" ma prefiks "AB" == sufiks "AB"
Więc możemy przeskoczyć:
ABABCABABA
AB... ← Kontynuuj od "AB", nie od początku!
Zastosowania w praktyce:
- Kompilatory (lexical analysis)
- Wyszukiwarki tekstu (grep, find)
- Bioinformatyka (DNA sequences)
- Intrusion detection systems
- Data compression algorithms
Co dalej warto poznać:
- Boyer-Moore – szybszy w praktyce (skanuje od końca)
- Rabin-Karp – hashing, dobry dla wielu wzorców
- Aho-Corasick – wiele wzorców jednocześnie (KMP++)
- Z-algorithm – wszystkie prefiksy w O(n)
- Suffix Array/Tree – zaawansowane struktury