• Keine Ergebnisse gefunden

Haskell-Code

Im Dokument Institut für Informatik (Seite 35-41)

5. Implementierung in Haskell

5.1 Haskell-Code

5.1.1 LCP-Array (Algorithmus 3.5)

Der Algorithmus 3.5 kann in zwei Abschnitte unterteilt werden. Im ersten Abschnitt wird das Inverse Suffix Array konstruiert und im zweiten das LCP-Array. Als Datenstruktur für S, SA, ISA und LCP wurde Data.Map21 (Map hat den Typ Map k a) verwendet, da der Zugriff und andere Operationen darauf entweder in O(log(n)) oder in konstanter Zeit erfolgen, da die Einträge in einer Baumstruktur gespeichert werden.

Code 5.1: Inverses Suffix Array ISA

create_ISA :: Ord k => Map k Integer -> Map Integer k -> Integer ->

Map k Integer

create_ISA isa sa i =

if i > (toInteger ((size sa))) then isa

else create_ISA (insert (sa!i) i isa) sa (i+1)

Code 5.1 zeigt die Funktion create_ISA die als Eingabe eine leere Map (isa), einen Suffix Array (sa) und einen Wert i erwartet, der am Anfang den Wert 1 hat und bei jedem rekursiven Aufruf um 1 erhöht wird. Dieser Wert dient einerseits als Abbruchbedingung der Rekursion, da im Fall i > (toInteger ((size sa))) das Inverse Suffix Array ausgegeben wird. Andererseits ist i der Wert, der mit dem Index

(sa!i) in ISA abgespeichert wird.

Die Funktion insert erwartet einen Index, einen Wert (i) und eine Map, in der der Wert zusammen mit dem Index gespeichert wird. Der Typ von insert ist

insert :: Ord k => k -> a -> Map k a -> Map k a.

Beispielaufrufe: Aus SA = [3,6,4,7,1,9,2,5,8] wird ISA = [5,7,1,3,8,2,4,9,6]

konstruiert. empty ist eine leere Map die der Funktion create_ISA übergeben wird.

*Main> sa

fromList [(1,3),(2,6),(3,4),(4,7),(5,1),(6,9),(7,2),(8,5),(9,8)]

*Main> create_ISA empty sa 1

fromList [(1,5),(2,7),(3,1),(4,3),(5,8),(6,2),(7,4),(8,9),(9,6)]

20 http://haskell.org (zuletzt besucht 2.5.2014)

21 https://hackage.haskell.org/package/containers-0.4.0.0/docs/Data-Map.html (zul. besucht 2.5.2014)

Im zweiten Abschnitt wird das LCP-Array konstruiert. Die Funktion create_LCP in Code 5.2 erwartet als Eingabe zwei Variablen i und l, den String S (s), SA (sa) und ISA (isa) und ein leeres LCP-Array (lcp). Wie bei create_ISA gibt es auch hier ein i, welches als Abbruchbedingung dient, da bei i=0 die Rekursion stoppt und das LCP-Array ausgeben wird. l ist eine Zahl, die angibt wie viele Zeichen der aktuelle Suffix mit seinem lexikographischen Vorgänger gemeinsam hat.

create_LCP verwendet zwei Hilfsfunktionen comp und maxi. comp vergleicht den String s ab Position (k+l) und (i+l) miteinander. comp ruft sich rekursiv auf und erhöht l um 1, wenn auch die nächsten Zeichen identisch sind. Die Ausgabe von comp

ist l. maxi wird eine Zahl l übergeben und gibt die größere Zahl aus, entweder (l-1) aktuelle Suffix mit dem lexikographischen Vorgänger verglichen und das Ergebnis wird mit dem Index i in lcp gespeichert.

then insert j (comp s (sa!(j-1)) l ineu) lcp else lcp

Beispielaufruf: Das LCP-Array vom String S=ctaataatg$ wird zunächst mit dem Wert (-1) an Position 1 und n+1 (mit n=|S|) erzeugt. Danach wird create_LCP aufgerufen.

*Main> let lcp_empty = fromList [((toInteger 1),-1),((toInteger (size sa)+1),-1)]

*Main> lcp_empty fromList [(1,-1),(10,-1)]

*Main> let s = fromList (zip [1..] "ctaataatg$") *Main> s

fromList [(1,'c'),(2,'t'),(3,'a'),(4,'a'),(5,'t'),(6,'a'),(7,'a'),(8,'t'),(9,'g'),(10,'$')]

*Main> create_LCP 9 s sa isa lcp_empty 0

fromList [(1,-1),(2,3),(3,1),(4,2),(5,0),(6,0),(7,0),(8,4),(9,1),(10,-1)]

5.1.2 SASEARCH und CMP (Algorithmen 4.4)

if (c < toInteger ( minimum [size w, size v])) then

cmp ist eine rekursive Funktion und vergleicht zwei Strings w und v miteinander ab der Position c. Als Ausgabe wird ein Tupel (c,a), mit a ∈ {-1,0,1} ausgegeben, wobei c mitzählt, wie viele Zeichen bei beiden Strings identisch sind, d.h. c erhöht sich bei jedem positiven Zeichenvergleich um 1. Der Wert a hängt davon ab, ob das zuletzt verglichene Zeichen von w kleiner, größer als oder gleich das Zeichen von v

ist. Bei einem Miss-Match wird die Rekursion abgebrochen und das Ergebnis ausgegeben.

Beispielaufruf: Gegeben sind w = aababc, v = aabacb und c = 2. Wir gehen davon aus, dass bereits bekannt ist, dass die ersten beiden Zeichen von w und v identisch sind, daher wurde c = 2 gewählt.

*Main> let w=fromList (zip [1..] "aababc")

findrightmost wurde analog zu findleftmost implementiert. Daher wird nur der Code von findleftmost erklärt (Code 5.4).

findleftmost benötigt als Eingabe S, P und SA. Im „where“-Block werden die Variablen r, l, h_l, f_r und h_r, f_r berechnet. Die Werte f_l und f_r geben an, ob das gesuchte Pattern P in S vorkommt oder nicht. Ist eine der beide Bedingungen

(f_l <= 0) und (f_r > 0) erfüllt wird direkt 1 oder r+1 ausgegeben. Ist dies nicht der Fall, wird binsearchleft aufgerufen, das die eigentliche binäre Suche im Suffix Array SA durchführt. binsearchleft entspricht der while-Schleife im Algorithmus 4.3 und ist eine rekursive Funktion. Bei jedem Durchlauf wird ein neuer mid-Wert berechnet und das Pattern mit dem Suffix SSA[mid] verglichen ((c,f_c)=

cmp p (suffix s (sa!mid)) (minimum[h_l,h_r])). Das Ergebnis des Vergleichs

(c,f_c) wird genutzt um zu entscheiden ob die binäre Suche im linken oder rechten Teil des SA fortgesetzt wird. D.h. ist f_c <= 0 wird binsearchleft mit neuen

(h_r,r)-Werten aufgerufen. Die neuen Werte sind genau (c,mid). Das bedeutet, dass der rechte Teil von SA nicht mehr betrachtet wird und im linken Teil nach dem Pattern gesucht wird. Falls die Bedingung (f_c <= 0) nicht erfüllt ist, wird im rechten Teil weitergesucht und (h_l,l) mit (c,mid) ersetzt und binsearchleft

aufgerufen. Die Rekursion endet, wenn (r-l)>1 nicht mehr wahr ist, d.h. r hat l

überholt. binsearchleft gibt den Wert r zurück.

Code 5.4: FINDLEFTMOST (Algorithmus 4.3)

findleftmost :: (Ord a, Ord a1) => Map a1 a -> Map Integer a ->

binsearchleft :: (Integral a2, Ord a, Ord a1) => (Integer, a2) ->

(Integer, a2) -> Map a1 a -> Map Integer a -> Map a2 a1 -> a2 binsearchleft (h_l,l) (h_r,r) s p sa =

if (r-l) > 1 then if f_c <= 0

then binsearchleft (h_l,l) (c,mid) s p sa else binsearchleft (c,mid) (h_r,r) s p sa else r

where

mid = div (l+r) 2

(c,f_c) = cmp p (suffix s (sa!mid)) (minimum[h_l,h_r])

5.1.4 FINDLEFTMOST (Algorithmus 4.5)

findrightmost wurde analog zu findleftmost implementiert. Daher wird folgenden nur findleftmost erklärt. Im Gegensatz zur vorigen findleftmost-Funktion arbeitet diese mit weniger Zeichenvergleiche und hat eine Eingabe mehr: das LCP-Array, dessen Implementierung in Kapitel 5.1.1 vorgestellt wurde. Abgesehen davon haben beide findleftmost identische Funktion.

Code 5.5: FINDLEFTMOST (Algorithmus 4.5)

binsearchleft (h_l,l) (h_r,r) s p sa lcp where

r = toInteger (size s) - 1 l = 1

(h_l,f_l) = cmp p (suffix s (sa!l)) 0 (h_r,f_r) = cmp p (suffix s (sa!r)) 0

binsearchleft :: (Integral a1, Ord a, Ord a2) => (Integer, a1) ->

(Integer, a1) -> Map a a2 -> Map Integer a2 -> Map a1 a -> SegTree -> a1

then binsearchleft (h_l,l) (c,mid) s p sa lcp else binsearchleft (c,mid) (h_r,r) s p sa lcp rmqLinear i j = minimum [lcp!k | k <- [i+1..j]]

suffix :: (Enum k, Num k, Ord k, Ord a1) => Map a1 a -> a1 -> Map k a suffix s i =

fromList (zip [1..] (snd (unzip (toList (filterWithKey (\k _ -> k >= i) s)))))

binsearchleft (Code 5.5) führt die binäre Suche im Suffix Array aus. Die Hilfsfunktion hf übernimmt die Entscheidung ob im rechten oder linken Teil des

Suffix Arrays weitergesucht wird. rmqLinear gibt den kleinsten Wert vom LCP-Array LCP[i+1, ... ,j] aus. suffix s i ist eine Hilfsfunktion die einen String s und eine Zahl

i erwartet und den Suffix Si von S ausgibt. Hierbei muss der Index neu eingefügt werden, da dieser sonst ab dem Wert i aufsteigt. filterWithKey filtert alle Einträge, die einen Index (key) kleiner oder gleich i haben. toList ändert Map zu einer Liste um, d.h. aus der Form fromList [(1,a1),(2,a2),...,(n,an)] wird [(1,a1),(2,a2),...,(n,an)].

Dies ist nötig, da die folgenden Funktionen nur auf Listen arbeiten und nicht auf Map angewendet werden können. unzip teilt die Tupel in dieser Liste in zwei Listen. Eine Liste enthält die Indizes und eine die Werte, d.h. aus [(1,a1),(2,a2),...,(n,an)] wird ([1,2,...,n],[a1,a2,...,an]). snd gibt das zweite Element aus und mit zip [1..] wird die Liste mit den neuen Indizes ab 1 aufsteigend verknüpft. Anschließenden wird fromList an dieser Liste drangehängt, damit der Typ Map vollständig wird und anschließend ausgegeben wird.

binsearchleft arbeitet genau wie der Algorithmus 4.5. Die while-Schleife wurde durch die rekursive Funktion binsearchleft umgesetzt und bricht ab, sobald die Bedingung (r-l) > 1 eintrifft. Dann wird der Wert r ausgegeben.

5.1.5 Weitere Implementierungen

Die Funktion rmqLinear die in Code 5.5 vorgestellt wurde benötigt im Worst-Case lineare Zeit, wodurch sich die Gesamt-Laufzeit des Algorithmus verschlechtert.

Gemäß dem Theorem 4.1 kann RMQ auch in konstanter Zeit beantwortet werden, wenn das Array dementsprechend vorverarbeitet wurde. Daher wurde in der eigentlichen Implementierung das LCP-Array nach der Konstruktion zu einer Baumstruktur umgewandelt und SASEARCH übergeben. Dafür wurde im eigentlichen Code eine andere RMQ-Funktion aus [9] übernommen. Auch für die Konstruktion des Suffix Arrays wurde der Haskell-Code aus dem Buch „Pearl“ [3] übernommen, der das Suffix Array in O(n*log(n)) Zeit berechnet. Der übernommene Code berechnet normalerweise das Inverse Suffix Array. Durch die Veränderung der ranktails -Funktion konnte auch das Suffix Array (ranktail) ausgegeben werden.

Code 5.6: Haskell-Code für die SA-Konstruktion wurde aus Pearl... übernommen.

ranktail wurde zu ranktails verändert, damit das Suffix Array ausgegeben wird.

-- SA

ranktail :: Ord a => [a] -> [Int]

ranktail xs =

((concat.applyUntil (all single) (repartitions (n+1)).psort.zip[1..]) xs) where n = length xs

-- ISA

ranktails :: Ord a => [a] -> [Int]

ranktails xs =

(resort n.concat.label.applyUntil (all single) (repartitions n).psort.zip [0..]) xs

where n = length xs

Die Gesamtfunktion stringmatch erwartet als Eingabe einen String s_str' und ein Pattern p_str. stringmatch ist für die Vorverarbeitung der Eingabestrings verantwortlich. Zuerst wird s_str' ein Dollar-Symbol bzw. irgendein Symbol

drangehängt, welches kleiner als alle anderen Zeichen sein muss. Danach werden

s_str und p_str in eine Map umgewandelt und das dazugehörige Suffix Array und LCP-Array konstruiert, die dann der Funktion sasearch übergeben werden. Die Ausgabe von stringmatch ist eine Liste von Zahlen, die die Positionen angeben, ab dem das p_str im String s_str' vorkommt.

Code 5.7: stringmatch für den Algorithmus 4.5

stringmatch :: [Char] -> [Char] -> [Integer]

stringmatch s_str' p_str = sasearch s p sa lcp where

---Vorverarbeitung

s_str = s_str' ++ [dollar_symbol]

s = fromList (zip [1..] s_str) p = fromList (zip [1..] p_str) -- SA

sa_list = tail (Prelude.map toInteger (ranktail s_str)) sa = fromList (zip [1..] sa_list)

-- ISA

i = toInteger (size sa) isa = create_ISA empty sa 1 --- > LCP-Array erstellen:

lcp_empty = fromList [((toInteger 1),-1),((toInteger (size sa) +1),-1)]

lcp = create_LCP i s sa isa lcp_empty 0

Die Funktion stringmatch für den Algorithmus 4.3 berechnet das Inverse Suffix Array und das LCP-Array nicht. Der Funktion sasearch werden nur s, p und sa

übergeben.

Beispielaufruf: Der Funktion stringmatch werden zwei Strings S und P übergeben.

Die Ausgabe ist eine Liste von Zahlen, die die Position angeben, ab dem das Pattern P im String S vorkommt.

*Main> stringmatch "Hallo Welt!" "ll"

[3]

*Main> stringmatch "Hallo Welt! Hallo Welt? Hallo Welt!" "ll"

[27,3,15]

Im Dokument Institut für Informatik (Seite 35-41)