Advanced searching via Z-Algorithm
I’m actually learning some stuff related to algorithms on sequences. The naive search for a pattern in a long string is of course very slow and comes with a lot of unintelligent compares. The Z-Algorithm improves the searching by preprocessing the pattern.
Naive searching
A simple search algorithm written in java may look like
This code reliably finds any existence of needle in haystack in \(O(m \cdot n)\), with \(m=\) length of needle and \(n=\) length of haystack. That screams for improvements ;)
Definitions
The first algorithm that I want to present in this series is called Z-Algorithm. First of all we need some definitions.
Definition 1: In the following we denote \(S[i\dots j]\) as the substring of \(S\) beginning at position \(i\) and ending at position \(j\). We can also leave one of the limits clear, so that \(S[i\dots]\) is the substring \(S[i\dots |S|]\) and \(S[\dots j]\) means \(S[1\dots j]\).
Definition 2: \(Z_i(S) := \max \{p | S[i \dots i+p-1] = S[1 \dots p]\}\) So \(Z_i(S)\) is the length of the longest prefix of the suffix \(S[i\dots]\) that is also prefix of \(S\) itself. To abbreviate \(Z_i(S)\) is further on mentioned as \(Z_i\).
Definition 3: The set \([i,i+Z_i-1]\) for a \(Z_i > 0\) is called Z-Box at position \(i\).
Definition 4: \(V_i := \{[a_j, b_j] | [a_j, b_j] \text{ is Z-Box at } a_j \wedge a_j < i\}\) \(V_i\) is the set of limits of all Z-Box’es that start at the left-handed side of \(i\). Consider \(i<j \Rightarrow V_i \subseteq V_j\).
Definition 5: \([l_i,r_i] := \begin{cases} \underset{b_j}{\arg\max} \ [a_j,b_j] \in V_i, & \text{if } V_i \ne \varnothing\\ [0,0] & \text{else}\end{cases}\) If \(l_i>0\) and \(r_i>0\), \([l_i,r_i]\) defines the rightest Z-Box that starts before respectively at position \(i\). Consider \(i<j \Rightarrow r_i\le r_j\).
Algorithm
In the following \(i\) will denote the actual position we are looking for, \(l\) and \(r\) describe the current respectively last found of a Z-Box. First of all we set the values \(l\) and \(r\) to zero because we haven’t found any Z-Box yet. \(Z_2\) of our text \(S\) is according to Definition 2 the length of the longest prefix of \(S[2\dots]\) that is also prefix of \(S\) itself. If \(Z_2>0\) we found a first Z-Box and update the limits to \(l=2\) and \(r=2+Z_2-1\).
Now we have to run through the word \(S\), so \(i=3\dots \|S\|\) with \(\|S\|\) defines the length of \(S\).
Case 1: Let’s assume position \(i\) is outside of the last found Z-Box or we didn’t find any Z-Box yet (\(i>r\)). We find \(Z_i\) by comparing the prefixes of \(S\) and \(S[i\dots]\). If \(Z_i>0\) we’ve found a new Z-Box and need to update the limits to \(l=i\) and \(r=i+Z_i-1\).
Case 2: If the current position \(i\) is inside of a current Z-Box (\(i\le r\)) we try to find the equivalent position at the beginning of \(S\). The position we are searching for is \(k=i-l+1\) steps off the beginning of \(S\) (we are \(i-l+1\) steps behind \(l\) and \(S[l\dots]\) has the same prefix as \(S\)). Case 2a: If we don’t break out of the current Z-Box by creating another Z-Box with the length of the box at position \(k\) (\(Z_k<r-i+1\), so position \(i+Z_k\) is not behind position \(r\)), we can simply apply this Z-Box to the current position and \(Z_i=Z_k\). Case 2b: Otherwise, if we would leave the actual Z-Box (\(i + Z_k>r\)) we have to recheck the prefix conditions of \(S[i\dots]\) and \(S\). We know that \(S[i\dots r]\) equals \(S[1\dots r-i+1]\), so we only have to find the length of the longest prefix \(p\) of \(S[r-i+2\dots]\) that equals the prefix of \(S[r+1\dots]\). Now we can apply the new Z-Box such that \(Z_i=r-i+1+p\) and of course we update the Z-Box limits to \(l=i\) and \(r=i+Z_i-1\).
If we reached the end of \(S\) all Z-Boxes are found in \(\Theta(\|S\|)\).
Pseudo code
Example
Let me demonstrate the algorithm with a small example. Let’s take the word \(S=aabaaab\). First we start with \(l=0\) and \(r=0\) at position 2. \(Z_2\) is the length of the shared prefix of \(S\) (\(aabaaab\)) and \(S[2\dots]\) (\(abaaab\)). Easy to see the prefix is \(a\) with a length of 1. So \(Z_2=1\), \(l=2\) and \(r=2\). At the beginning of our for-loop the program’s status is:
$$T$$ | a | a | b | a | a | a | b |
---|---|---|---|---|---|---|---|
$$i$$ | 1 | 2 | |||||
$$Z_i$$ | 1 | ||||||
$$l$$ | 2 | ||||||
$$r$$ | 2 |
At the first round in the loop \(i=3\), so \(i>r\) because \(r=2\). So we meet case 1 and have to find the length of the prefix of \(S\) (\(aabaaab\)) and \(S[3\dots]\) (\(baaab\)). Of course it’s zero, nothing to do.
$$T$$ | a | a | b | a | a | a | b |
---|---|---|---|---|---|---|---|
$$i$$ | 1 | 2 | 3 | ||||
$$Z_i$$ | 1 | 0 | |||||
$$l$$ | 2 | 2 | |||||
$$r$$ | 2 | 2 |
Next round, we’re at position 4 and again \(i>r\) (case 1). So we have to compare \(aabaaab\) and \(aaab\). The longest prefix of both words is \(aa\) with a length of 2. So we start a new Z-Box at 4 with a size of 2, so \(l=4\) and \(r=5\).
$$T$$ | a | a | b | a | a | a | b |
---|---|---|---|---|---|---|---|
$$i$$ | 1 | 2 | 3 | 4 | |||
$$Z_i$$ | 1 | 0 | 2 | ||||
$$l$$ | 2 | 2 | 4 | ||||
$$r$$ | 2 | 2 | 5 |
With \(i=5\) and \(r=5\) we reach case 2 for the first time. \(k=i-l+1=2\) so our similar position at the beginning of \(S\) is position 2. \(Z_2=1\) and \(r-i+1=1\) so we are in case 2b and have to find the shared prefix of \(S[2 ..]\) (\(abaaab\)) and \(S[6 ..]\) (\(ab\)). It’s \(ab\), so \(p=2\) and \(Z_5=r-i+1+p=3\). \(l=5\) and \(r=7\).
$$T$$ | a | a | b | a | a | a | b |
---|---|---|---|---|---|---|---|
$$i$$ | 1 | 2 | 3 | 4 | 5 | ||
$$Z_i$$ | 1 | 0 | 2 | 3 | |||
$$l$$ | 2 | 2 | 4 | 5 | |||
$$r$$ | 2 | 2 | 5 | 7 |
Next round brings us \(i=6<r\), therefor we’re in case 2. Equivalent position is again \(k=i-l+1=2\), but now \(Z_2=1<r-i+1=2\) and we’re in case 2a and can just set \(Z_6=1\).
$$T$$ | a | a | b | a | a | a | b |
---|---|---|---|---|---|---|---|
$$i$$ | 1 | 2 | 3 | 4 | 5 | 6 | |
$$Z_i$$ | 1 | 0 | 2 | 3 | 1 | ||
$$l$$ | 2 | 2 | 4 | 5 | 5 | ||
$$r$$ | 2 | 2 | 5 | 7 | 7 |
The last round we have to process is \(i=7<r\), case 2. Equivalent position is \(k=i-l+1=3\) and \(Z_3=0<r-i+1=1\), so case 2a and \(Z_7 = 0\).
$$T$$ | a | a | b | a | a | a | b |
---|---|---|---|---|---|---|---|
$$i$$ | 1 | 2 | 3 | 4 | 5 | 6 | 7 |
$$Z_i$$ | 1 | 0 | 2 | 3 | 1 | 0 | |
$$l$$ | 2 | 2 | 4 | 5 | 5 | 5 | |
$$r$$ | 2 | 2 | 5 | 7 | 7 | 7 |
That’s it. The Z-Box’es we’ve found are visualized in the image.
Searching
To search for a pattern \(P \in A^*\) in a text \(T \in A^*\) just calculate the Z-Boxes of \(P\$T\) with \(\$\notin A\). These calculations are done in \(\Theta(|T|)\). For any \(i>|P|\): If \(Z_i=|P|\) means \(P\$T[i\dots i+|P|-1]\) is prefix of \(P\$T\), so \(P\) is found at position \(i-(|P|+1)\) in \(T\).
Code
Of course I’m providing an implementation, see attachment.
- bioinformatics (22) ,
- explained (43) ,
- java (22) ,
- pattern (3) ,
- programming (75) ,
- search (5) ,
- university (42)
Leave a comment
There are multiple options to leave a comment:
- send me an email
- submit a comment through the feedback page (anonymously via TOR)
- Fork this repo at GitHub, add your comment to the _data/comments directory and send me a pull request
- Fill the following form and Staticman will automagically create a pull request for you:
1 comment
Suffix trees…
Suffix trees are an application to particularly fast implement many important string operations like searching for a pattern or finding the longest common substring…….