Algorytm Rabin-Karp
1. Idea algorytmu
Rabin-Karp (1987) wykorzystuje hashing do szybkiego porównywania substringów.
1.1. Problem z algorytmem naiwnym:
Text: ABCDEFGH
Pattern: DEF
Naiwny: Porównuje znak po znaku
ABC vs DEF → A != D ✗
BCD vs DEF → B != D ✗
CDE vs DEF → C != D ✗
DEF vs DEF → D == D, E == E, F == F ✓
Rabin-Karp: Porównuje HASH(substring) == HASH(pattern)
HASH("ABC") vs HASH("DEF") → różne hasze ✗
HASH("BCD") vs HASH("DEF") → różne hasze ✗
HASH("CDE") vs HASH("DEF") → różne hasze ✗
HASH("DEF") vs HASH("DEF") → te same hasze! ✓ → weryfikuj znak po znaku
Kluczowa idea: Hashing jest szybki O(1), gdy używamy rolling hash!
2. Rolling Hash (hash przesuwany)
2.1. Czym jest rolling hash?
Rolling hash to funkcja hashująca, która może być efektywnie aktualizowana przy przesuwaniu okna o jeden znak.
Text: ABCDEF
Hash("ABC") → oblicz od zera
Hash("BCD") → usuń 'A', dodaj 'D' (O(1)!)
Hash("CDE") → usuń 'B', dodaj 'E' (O(1)!)
2.2. Funkcja hash - polynomial rolling hash:
Dla stringa s = [c₀, c₁, c₂, ..., c_{m-1}]:
hash(s) = (c₀ × d^{m-1} + c₁ × d^{m-2} + ... + c_{m-1} × d^0) mod q
gdzie:
- d = baza (np. 256 dla ASCII)
- q = duża liczba pierwsza (unikanie kolizji)
- c_i = wartość znaku (np. ord(c_i))
2.3. Przykład:
3. Implementacja podstawowa
3.1. Prosty hash (bez rolling):
3.2. Algorytm Rabin-Karp bez rolling hash:
Problem: Obliczanie hasha każdego substringa to O(m) → razem O(n × m)!
4. Rolling Hash - efektywna aktualizacja
4.1. Formuła aktualizacji:
Mamy: hash(s[i:i+m])
Chcemy: hash(s[i+1:i+m+1])
Strategia:
1. Usuń pierwszy znak: s[i]
2. Dodaj nowy znak: s[i+m]
hash_new = (d × (hash_old - s[i] × d^{m-1}) + s[i+m]) mod q
4.2. Wizualizacja:
Text: ABCDEF
m = 3
hash("ABC") = 65×10² + 66×10¹ + 67×10⁰
= 6500 + 660 + 67 = 7227
hash("BCD") = ?
Usuń 'A': 7227 - 65×10² = 7227 - 6500 = 727
Przesuń: 727 × 10 = 7270
Dodaj 'D': 7270 + 68 = 7338
hash("BCD") = 7338 (mod q)
5. Implementacja z rolling hash
Złożoność: - Preprocessing: O(m) - Rolling hash update: O(1) ✅ - Razem średnio: O(n + m) ✅
6. Wizualizacja działania
7. Obsługa kolizji
7.1. Problem kolizji:
7.2. Rozwiązanie:
7.3. Wybór parametrów:
8. Wyszukiwanie wielu wzorców
Rabin-Karp jest idealny do wyszukiwania WIELU wzorców jednocześnie!
Złożoność: O(n × k + m_total) - k = liczba wzorców - m_total = suma długości wzorców
9. Zastosowania praktyczne
9.1. Wykrywanie plagiatów:
9.2. Filtrowanie spamu:
10. Porównanie z innymi algorytmami
11. Optymalizacje
11.1. Używanie większej liczby pierwszej:
11.2. Double hashing:
12. Podsumowanie
Algorytm Rabin-Karp:
- Używa hashingu do szybkiego porównywania
- Rolling hash → aktualizacja O(1) ✅
- Idealny dla wielu wzorców jednocześnie ✅
- Wymaga weryfikacji (kolizje możliwe)
Złożoność:
| Przypadek | Złożoność | Uwagi |
|---|---|---|
| Średni | O(n + m) ✅ | Mało kolizji |
| Najgorszy | O(n × m) ❌ | Wiele kolizji hash |
| Preprocessing | O(m) | Obliczanie hash wzorca |
| Pamięć | O(1) | Tylko zmienne |
Rolling hash:
hash_new = (d × (hash_old - s[i] × d^{m-1}) + s[i+m]) mod q
Aktualizacja w O(1)! ✅
Implementacja podstawowa:
Zalety: - ✅ Średnio O(n + m) – efektywny - ✅ Wiele wzorców → bardzo szybki! ✅ - ✅ O(1) pamięci dodatkowej - ✅ Prosty koncept (hashing) - ✅ Rolling hash → elegancka optymalizacja
Wady: - ❌ Wymaga weryfikacji (kolizje) - ❌ Najgorszy przypadek O(n × m) - ❌ Wrażliwy na dobór parametrów (d, q) - ❌ Dla jednego wzorca KMP często lepszy
Kiedy używać Rabin-Karp:
✅ Wiele wzorców jednocześnie (główna siła!) ✅ Wykrywanie plagiatów (porównywanie dokumentów) ✅ Filtrowanie spamu (wiele słów kluczowych) ✅ DNA sequencing (wiele sekwencji) ✅ Prostota – łatwiejszy niż KMP/Boyer-Moore
❌ Jeden wzorzec → KMP/Boyer-Moore lepsze ❌ Gwarancja O(n+m) → KMP ❌ Krytyczne aplikacje (kolizje niedopuszczalne)
Porównanie algorytmów:
| Algorytm | Średni | Najgorszy | Wiele wzorców | Prostota |
|---|---|---|---|---|
| Naiwny | O(n) | O(n×m) | ❌ | ✅✅✅ |
| KMP | O(n+m) | O(n+m) ✅ | ❌ | ✅✅ |
| Rabin-Karp | O(n+m) | O(n×m) | ✅✅✅ | ✅✅ |
| Boyer-Moore | O(n/m) ✅ | O(n×m) | ❌ | ✅ |
Kluczowa obserwacja:
Rabin-Karp świeci przy WIELU wzorcach:
Szukanie 1 wzorca:
- KMP: O(n + m)
- Rabin-Karp: O(n + m)
Podobne!
Szukanie k wzorców:
- KMP: O(k × (n + m)) ← k razy wolniej!
- Rabin-Karp: O(n + m_total) ← prawie bez narzutu! ✅
Zastosowania w praktyce:
- Wykrywanie plagiatów (document comparison)
- Spam filtering (multiple keywords)
- Intrusion detection (network signatures)
- DNA sequence matching (bioinformatics)
- File deduplication (content-defined chunking)
Co dalej warto poznać:
- Boyer-Moore – najszybszy dla długich wzorców
- Aho-Corasick – optymalizacja dla wielu wzorców (automat)
- Suffix Array/Tree – preprocessing tekstu zamiast wzorca
- Content-Defined Chunking – zastosowanie w deduplikacji
- Bloom Filters – probabilistyczne wyszukiwanie