Jak odnaleźć wzorzec w tekście?

Rozważmy następujący problem. Mamy ciąg znaków: jedno słowo (np. „kot”) lub kilka wyrazów – np. zdanie „ala ma kota” (dla uproszczenia pomijamy wielkości liter). Problem wyszukania wzorca w tekście polega na odnalezieniu takiej liczby, która określa, ile początkowych znaków tekstu należy usunąć, aby rozpoczynał się on właśnie od wzorca. W przytoczonym przykładzie taką cyfrą będzie 7. Po usunięciu z ciągu „ala ma kota” 7 początkowych znaków, zostaniemy z wyrażeniem „kota”. Wówczas łatwo zauważyć, że wzorzec „kot” pasuje do początku zdania.

W jaki sposób odbywa się szukanie wzorca w tekście? Pomocna jest tzw. metoda naiwna, określana często mianem brute forcebrute forcebrute force. Polega ona na wykonywaniu porównania wzorca z początkiem tekstu dla każdej liczby znaków, które z niego usuniemy. Takie wyszukiwanie w podanym przykładzie będzie wyglądało następująco:

R1PPQUTSZhpVo
Źródło: Contentplus.pl Sp. z o.o., licencja: CC BY-SA 3.0.

Podstawową wadą tej metody jest to, że nie jest ona optymalna. Nie nadaje się do poszukiwania długich wzorców w długich tekstach. Istnieją algorytmy, które dużo efektywniej radzą sobie w podobnych sytuacjach. Jednym z takich algorytmów jest algorytm Knutha‑Morrisa‑Pratta.

Wstęp do algorytmu Knutha‑Morrisa‑Pratta

Ważne!

Algorytm Knutha‑Morrisa‑Pratta wyszukiwania wzorca

Specyfikacja problemu:

Dane:

  • t – łańcuch znakowy

  • w – poszukiwany w łańcuchu t wzorzec

Wynik:

Wypisanie kolejnych wystąpień wzorca w łańcuchu.

Algorytm ten jest pierwszym odkrytym algorytmem, który rozwiązuje problem wyszukiwania wzorca w tekście, cechuje się złożonością liniową względem długości tekstu oraz wzorca. Jego nazwa pochodzi od trzech matematyków‑informatyków, którzy wspólnie opublikowali ostateczną wersję algorymtu: Donald Ervin Knuth, Vaughan Ronald Pratt oraz James Hiram Morris. W skrócie algorytm nazywamy „KMP”.

Algorytm KMP sprawdza po kolei każdy znak tekstu, do którego możemy dopasować wzorzec. W momencie, gdy pierwszy znak wzorca oraz aktualnie przetwarzany znak tekstu są takie same, rozpoczynamy sprawdzanie po kolei, czy następne znaki wzorca i tekstu są identyczne. Na pierwszy rzut oka metoda ta działa w taki sam sposób, jak metoda naiwna. Usprawnienie pojawia się jednak w momencie, gdy już znaleźliśmy potencjalne dopasowanie, które okazuje się błędne. W takiej sytuacji algorytm KMP pomija sprawdzanie dopasowania na tych następnych pozycjach, które na pewno okażą się błędne.

Pomocna w tym działaniu jest analiza wstępna wzorca, która polega na utworzeniu tablicy częściowych dopasowań. Przechowuje ona liczby całkowite. Długość takiej tablicy to długość wzorca powiększona o 1. Aby najłatwiej wytłumaczyć proces analizy wzorca w algorytmie KMP, posłużymy się wcześniejszą jego wersją (znaną jako algorytm MP), a następnie rozszerzymy ją o pewien dodatkowy przypadek, dzięki któremu powstała jego obecna wersja.

Chcąc dokładnie omówić tę procedurę, musimy wprowadzić kilka pojęć:

  1. prefiks – składająca się z k znaków przednia część łańcucha znaków;

  2. sufiks – składająca się z k znaków końcowa część łańcucha znaków;

  3. prefikso‑sufiks – składająca się z k znaków część łańcucha znaków, która występuje zarówno z przodu, jak i z tyłu;

  4. szerokość prefikso‑sufiksu – długość prefiksu lub sufiksu, z którego składa się prefikso‑sufiks.

Budowa tablicy częściowych dopasowań w algorytmie MP polega na wyznaczeniu maksymalnego prefikso‑sufiksu dla każdego możliwego prefiksu we wzorcu.

Ciekawostka

Algorytmy wyszukiwania wzorca w tekście stosowane są często w genetyce. Przykład, którym się posłużymy, będzie operował na znakach A, C, G, T, czyli na symbolach oznaczających zasady azotowe, z których zbudowane jest DNA. Więcej na temat DNA przeczytasz w e‑materiale DNA jako nośnik informacji genetycznejPO5R814osDNA jako nośnik informacji genetycznej.

Oznaczmy ciąg znaków wzorzec jako W oraz tablicę częściowych dopasowań jako T.

Krok 0:

Na zerowej pozycji w tablicy T wstawimy liczbę -1, która będzie naszym wartownikiemwartownikwartownikiem.

RrnePchYkQS1U
Źródło: Contentplus.pl Sp. z o.o., licencja: CC BY-SA 3.0.

Krok 1:

Pierwszy możliwy prefiks wzorca to „G”. Wśród ciągu znaków „G” nie występuje żaden prefikso‑sufiks, zatem kolejny element w tablicy T to 0 – prefikso‑sufiks pusty.

RgRUBIjKyXWzY
Źródło: Contentplus.pl Sp. z o.o., licencja: CC BY-SA 3.0.

Krok 2:

Kolejny prefiks wzorca to „GC”. Nie występuje żaden prefikso‑sufiks, zatem jako kolejny element tablicy również wstawiamy 0 – prefikso‑sufiks pusty.

RIpcZTx3g8Zpp
Źródło: Contentplus.pl Sp. z o.o., licencja: CC BY-SA 3.0.

Krok 3:

Kolejny prefiks to „GCA”. Znowu występuje jedynie prefikso‑sufiks pusty, więc do tablicy wstawiamy 0.

R1HiZ7YFxEf1o
Źródło: Contentplus.pl Sp. z o.o., licencja: CC BY-SA 3.0.

Krok 4:

Sytuacja jak poprzednio – wstawiamy 0.

RU4ba4ra2pSPX
Źródło: Contentplus.pl Sp. z o.o., licencja: CC BY-SA 3.0.

Krok 5:

W aktualnie przeszukiwanym fiksie „GCATG” znaleźliśmy pierwszy prefikso‑sufiks. Jego szerokość wynosi 1, zatem w tablicy umieszczamy element 1.

RacgQ4oKNc0fO
Źródło: Contentplus.pl Sp. z o.o., licencja: CC BY-SA 3.0.

Krok 6:

W kolejnym prefiksie największy prefikso‑sufiks, jaki znajdziemy, jest o szerokości 2. Zauważmy, że nie musimy w tym kroku przeszukiwać całego prefiksu „GCATGC”, gdyż możemy kontynuować wyszukiwanie, które rozpoczęliśmy w poprzednim kroku.

RBReEkZM4CYou
Źródło: Contentplus.pl Sp. z o.o., licencja: CC BY-SA 3.0.

Krok 7:

W tym kroku największy prefikso‑sufiks ma szerokość 1. Pamiętając o informacji z poprzedniego kroku, sprawdź czy uda się rozszerzyć poprzednio znaleziony prefikso‑sufiks. Dopiero gdy próba się nie powiedzie, sprawdzamy, czy możemy znaleźć inny prefikso‑sufiks. Zatem w tym kroku są wykonywane tak naprawdę 2 kroki.

RZgJ9fLy6nTDh
Źródło: Contentplus.pl Sp. z o.o., licencja: CC BY-SA 3.0.

Krok 8:

W prefiksie „GCATGCGA” nie ma żadnego prefikso‑sufiksu. Również w tym kroku próbowaliśmy rozszerzyć poprzedni prefikso‑sufiks.

RILM3Vla4JLcB
Źródło: Contentplus.pl Sp. z o.o., licencja: CC BY-SA 3.0.

Krok 9:

Znaleźliśmy prefikso‑sufiks o długości 1 – w tablicy zapisujemy tę wartość.

R1MC5MFyqyWtP
Źródło: Contentplus.pl Sp. z o.o., licencja: CC BY-SA 3.0.

Krok 10:

Możemy rozszerzyć poprzednio znaleziony prefikso‑sufiks. Do tablicy wpisujemy wartość o 1 większą niż w poprzednim kroku.

R1GnQT1SkIlHV
Źródło: Contentplus.pl Sp. z o.o., licencja: CC BY-SA 3.0.

Wszystkie powyżej opisane kroki możemy przedstawić za pomocą poniższego pseudokodu. Jako W oznaczono wzorzec:

Linia 1. funkcja budowaTablicyMP otwórz nawias okrągły W zamknij nawias okrągły. Linia 2. rozmiarT znak równości długość otwórz nawias okrągły W zamknij nawias okrągły plus 1. Linia 3. stwórz tablicę T o długości rozmiarT. Linia 4. poz znak równości minus 1. Linia 5. T otwórz nawias kwadratowy 0 zamknij nawias kwadratowy znak równości minus 1. Linia 7. prawy ukośnik prawy ukośnik dla każdego kolejnego prefiksu wzorca. Linia 8. dla i znak równości 1 przecinek 2 przecinek kropka kropka kropka przecinek rozmiarT minus 1 wykonuj dwukropek. Linia 9. prawy ukośnik prawy ukośnik dopóki możesz znaleźć dłuższy prefikso minus sufiks. Linia 10. dopóki poz zamknij nawias ostrokątny minus 1 i W otwórz nawias kwadratowy poz zamknij nawias kwadratowy wykrzyknik znak równości W otwórz nawias kwadratowy i minus 1 zamknij nawias kwadratowy wykonuj dwukropek. Linia 11. poz znak równości T otwórz nawias kwadratowy poz zamknij nawias kwadratowy. Linia 12. poz plus znak równości 1. Linia 14. prawy ukośnik prawy ukośnik zapisz szerokość znalezionego prefikso minus sufiksu. Linia 15. T otwórz nawias kwadratowy i zamknij nawias kwadratowy znak równości poz. Linia 17. zwróć T.

Usprawnienie

Omówmy dodatkowy, wspominany już przypadek, który ulepsza metodę MP do KMP. Występuje wówczas, gdy znaleźliśmy prefikso‑sufiks (może być pusty) w aktualnie sprawdzanym prefiksie, ale znak następujący po tym prefiksie, jest taki sam jak ten, który występuje po prefikso‑sufiksie (po części prefiksowej). W takiej sytuacji do tablicy wstawiamy wartość, która już została zapisana w tablicy T na pozycji określonej długością obecnego prefikso‑sufiksu. Jeżeli porównywane elementy są różne, do tablicy wpisujemy długość prefikso‑sufiksu. Brzmi to bardzo skomplikowanie. Dlatego też prześledźmy to na konkretnym przykładzie – tym samym, który omówiliśmy przy algorytmie MP.

Krok 0:

Tak jak w przypadku algorytmu MP, na zerowym miejscu tablicy wstawiamy wartownika -1.

RzUoGZ1kXsicH
Źródło: Contentplus.pl Sp. z o.o., licencja: CC BY-SA 3.0.

Krok 1:

Jedyny prefikso‑sufiks, który jesteśmy w stanie znaleźć, to prefikso‑sufiks pusty. Należy jednak porównać zaznaczone na niebiesko elementy (element występujący po znalezionym prefikso‑sufiksie – „G”) oraz element występujący po aktualnie przetwarzanym prefiksie – „C”). Są różne, zatem możemy wpisać do tablicy szerokość znalezionego prefikso‑sufiksu – 0.

R1b1ewZxUERcB
Źródło: Contentplus.pl Sp. z o.o., licencja: CC BY-SA 3.0.

Krok 2:

Podobna sytuacja jak w kroku 1. Znaleźliśmy prefikso‑sufiks pusty. Porównujemy 2 elementy, które występują po znalezionym prefikso‑sufiksie oraz po przetwarzanej części wzorca. Są one różne, zatem możemy wstawić szerokość prefikso‑sufiksu.

R7n66dD3FHgEC
Źródło: Contentplus.pl Sp. z o.o., licencja: CC BY-SA 3.0.

Krok 3:

Sytuacja identyczna jak w poprzednim kroku – wstawiamy 0.

R1Ndp9m3HUT0c
Źródło: Contentplus.pl Sp. z o.o., licencja: CC BY-SA 3.0.

Krok 4:

Znowu znaleźliśmy pusty prefikso‑sufiks. Jest to jednak pierwszy przypadek, w którym porównywane później elementy są takie same. W takiej sytuacji musimy odczytać z tablicy T wartość pod indeksem określonym przez długość obecnego prefikso‑sufiksu. Długość ta wynosi w tym wypadku 0 (prefikso‑sufiks pusty), T[0] = -1, zatem ustawiamy wartownika -1.

R8TvOk4GdVwej
Źródło: Contentplus.pl Sp. z o.o., licencja: CC BY-SA 3.0.

Krok 5:

Znaleźliśmy prefikso‑sufiks o szerokości 1. Elementy, które musimy porównać, są takie same, zatem odczytujemy T[1] = 0, wpisujemy 0.

R11veRqkEJC8u
Źródło: Contentplus.pl Sp. z o.o., licencja: CC BY-SA 3.0.

Krok 6:

Prefikso‑sufiks ma szerokość 2. Ponieważ elementy „A” i „G” są różne, szerokość wpisujemy do tablicy.

R1B0eTw1CCmlq
Źródło: Contentplus.pl Sp. z o.o., licencja: CC BY-SA 3.0.

Krok 7:

W tym kroku znaleziony prefikso‑sufiks ma szerokość 1, elementy, które porównujemy są różne, więc do tablicy wstawiamy 1.

R1C3Zs44Ib4kN
Źródło: Contentplus.pl Sp. z o.o., licencja: CC BY-SA 3.0.

Krok 8:

Prefikso‑sufiks jest pusty – szerokość 0. Następny element tablicy będzie zatem równy elementowy T[0], czyli -1.

RmVc8QKbm7sBU
Źródło: Contentplus.pl Sp. z o.o., licencja: CC BY-SA 3.0.

Krok 9:

Udało się rozszerzyć znaleziony w poprzednim kroku prefikso‑sufiks pusty do szerokości 1. Porównywane elementy są takie same, zatem wstawiamy wartość z komórki o indeksie równym szerokości. T[1] = 0, więc kolejnym elementem będzie 0.

RWFr7HrkAitzk
Źródło: Contentplus.pl Sp. z o.o., licencja: CC BY-SA 3.0.

Krok 10:

Szerokość znalezionego prefikso‑sufiksu to 2. Jako że osiągnęliśmy koniec wzorca i nie mamy z czym porównać elementu „A”, możemy założyć, że zawsze jest on inny od elementu, z którym byśmy go porównywali. W rzeczywistości algorytm sprawdza moment, w którym dochodzimy do końca wzorca. W takiej sytuacji do tablicy wstawiamy szerokość prefikso‑sufiksu.

R1YYdNUvIqzfn
Źródło: Contentplus.pl Sp. z o.o., licencja: CC BY-SA 3.0.

Ostateczny algorytm może być zapisany za pomocą następującego pseudokodu. Jako W oznaczono wzorzec, T to tablica częściowych dopasowań. Zwróćmy uwagę, że różni się od metody MP tylko jedną dodatkową konstrukcją warunkową.

Linia 1. funkcja budowaTablicyKMP otwórz nawias okrągły W zamknij nawias okrągły. Linia 2. rozmiarT znak równości długość otwórz nawias okrągły W zamknij nawias okrągły plus 1. Linia 3. stwórz tablicę T o długości rozmiarT. Linia 4. poz znak równości minus 1. Linia 5. T otwórz nawias kwadratowy 0 zamknij nawias kwadratowy znak równości minus 1. Linia 7. prawy ukośnik prawy ukośnik dla każdego kolejnego prefiksu wzorca. Linia 8. dla i znak równości 1 przecinek 2 przecinek kropka kropka kropka przecinek rozmiarT minus 1 wykonuj dwukropek. Linia 9. prawy ukośnik prawy ukośnik dopóki możesz znaleźć dłuższy prefikso minus sufiks. Linia 10. dopóki poz zamknij nawias ostrokątny minus 1 i W otwórz nawias kwadratowy poz zamknij nawias kwadratowy wykrzyknik znak równości W otwórz nawias kwadratowy i minus 1 zamknij nawias kwadratowy wykonuj dwukropek. Linia 11. poz znak równości T otwórz nawias kwadratowy poz zamknij nawias kwadratowy. Linia 12. poz plus znak równości 1. Linia 14. prawy ukośnik prawy ukośnik usprawnienie. Linia 16. prawy ukośnik prawy ukośnik jeżeli element występujący po znalezionym. Linia 17. prawy ukośnik prawy ukośnik prefikso minus sufiksie jest taki sam jak po prefiksie. Linia 18. jeżeli i znak równości znak równości rozmiarT minus 1 LUB W otwórz nawias kwadratowy i zamknij nawias kwadratowy wykrzyknik znak równości W otwórz nawias kwadratowy poz zamknij nawias kwadratowy. Linia 19. prawy ukośnik prawy ukośnik zapisz szerokość znalezionego prefikso minus sufiksu. Linia 20. T otwórz nawias kwadratowy i zamknij nawias kwadratowy znak równości poz. Linia 21. w przeciwnym wypadku. Linia 22. prawy ukośnik prawy ukośnik kolejny element tablicy będzie tym samym przecinek. Linia 23. prawy ukośnik prawy ukośnik co element na pozycji równej szerokości. Linia 24. prawy ukośnik prawy ukośnik prefikso minus sufiksu. Linia 25. T otwórz nawias kwadratowy i zamknij nawias kwadratowy znak równości T otwórz nawias kwadratowy poz zamknij nawias kwadratowy. Linia 27. zwróć T.

Wykorzystanie tablicy częściowych dopasowań

Dlaczego budujemy tablicę częściowych dopasowań? Wyobraźmy sobie prosty przykład, w którym metodą naiwną próbujemy znaleźć dopasowanie wzorca (AAB) w tekście (AAAAAAB). Dopasowania zaznaczamy na zielono. Czerwonym kolorem oznaczamy te próby, które kończą się niepowodzeniem.

R1Tz3dbWp8KYW
Źródło: Contentplus.pl Sp. z o.o., licencja: CC BY-SA 3.0.

Już po pierwszej próbie dopasowania widoczne jest, że kolejną próbę dopasowania możemy rozpocząć, pomijając sprawdzenie pierwszego znaku. Wiemy bowiem, że drugi znak jest taki sam jak pierwszy, a drugi w tym miejscu już pasował. Optymalne dopasowywanie wyglądałoby w następujący sposób:

R1I5GNluWq20I
Źródło: Contentplus.pl Sp. z o.o., licencja: CC BY-SA 3.0.

Porównując inteligentne przeszukiwanie z metodą naiwną, zaoszczędziliśmy cztery porównania. Być może nie jest to dużo, jednak różnica zwiększa się wraz ze wzrostem długości tekstu oraz wzorca.

Algorytm wyszukiwania wzorca w tekście z wykorzystaniem tablicy dopasowań wygląda następująco. Warto zaznaczyć, że działa on zarówno dla tablicy dopasowań metody MP, jak i KMP, jednak tablica znaleziona algorytmem KMP jest po prostu bardziej wydajna. Jako W oznaczono wzorzec, a jako S – przeszukiwany tekst.

Linia 1. funkcja szukajAlgorytmemKMP otwórz nawias okrągły W przecinek S zamknij nawias okrągły. Linia 2. T znak równości budowaTablicyKMP otwórz nawias okrągły W zamknij nawias okrągły. Linia 3. b znak równości 0. Linia 4. dla i znak równości 0 przecinek 1 przecinek kropka kropka kropka przecinek długość otwórz nawias okrągły S zamknij nawias okrągły minus 1 wykonuj dwukropek. Linia 5. dopóki b zamknij nawias ostrokątny minus 1 i W otwórz nawias kwadratowy b zamknij nawias kwadratowy wykrzyknik znak równości S otwórz nawias kwadratowy i zamknij nawias kwadratowy wykonuj dwukropek. Linia 6. b znak równości T otwórz nawias kwadratowy b zamknij nawias kwadratowy. Linia 7. b plus znak równości 1. Linia 8. jeżeli b znak równości znak równości długość otwórz nawias okrągły W zamknij nawias okrągły dwukropek. Linia 9. zwróć i minus długość otwórz nawias okrągły W zamknij nawias okrągły plus 1. Linia 10. zwróć minus 1.

Słownik

brute force
brute force

(z ang. brutalna siła); metoda polegająca na sprawdzeniu wszystkich możliwych przypadków w celu znalezienia rozwiązania danego problemu; terminu tego często używa się w kontekście prób łamania haseł, które polegają na sprawdzaniu wszystkich możliwych kombinacji znaków

wartownik
wartownik

specjalny rodzaj obiektu, używany w celu oznaczenia wartości o innej interpretacji niż reszta danych w strukturze