Formulating the problem
In the previous lesson, we considered the algorithm for finding a pattern in a text. We also found out that the running time of the algorithm is , where is the length of the pattern and is the length of the text. One may wonder: is there an algorithm that can solve the problem more efficiently? The answer is “yes”, and in this lesson, we will consider a more efficient approach to the problem, namely the Rabin-Karp algorithm. The main idea is to use hashing of strings and compare hash values instead of a symbol-by-symbol comparison.
A polynomial hash function
One of the ways to hash a string is to use a so-called polynomial hash function. For a string it can be defined as follows:
Here is a value associated with the i-th symbol of the string. For example, it may be the sequence number of a symbol in the alphabet: 1 for , 2 for and so on. is a constant, usually a prime number approximately equal to the number of symbols in the alphabet, is a constant as well, usually chosen as a big prime number. Let’s consider how we can calculate a polynomial hash for a string . For simplicity, we will consider and to be equal 3 and 11 respectively.
Recall that if two strings have different hash values, it follows that they are different. But if two hash values are the same, it does not mean that the corresponding strings are equal. For instance, and are the different strings, but their hash values are the same (this situation is called a collision):
,
.
The described hash function has one important property: If we know a hash value for a substring of some string, we can calculate a hash value for a left neighbor substring using a constant number of operations. Consider how it works for a string . At first, let’s calculate a hash value for a suffix of having the length 3:
Then, to calculate a hash value for the neighbor substring of length 3, we need to compute the following:
Now we can note that in order to get we need to subtract the last term of , multiply the resulting expression by 3, add the first term of and finally perform a modulo division. So,
For an arbitrary substring of :
A function that has the described property is called a rolling hash. The property is crucial for the Rabin-Karp algorithm.
The Rabin-Karp algorithm
For a pattern p, a text t and the described hash function the Rabin-Karp algorithm can be formulated as follows:
- Calculate a hash value for the pattern .
- Moving along the text from the right to the left, calculate a hash value for the current substring using the formula (2).
- If , move to the next substring. If the hash values are equal, perform a symbol-by-symbol comparison. If the strings are indeed equal, add the starting index of the current substring to a list of occurrences.
- Repeat steps 2 and 3 until all the text is processed. Then, return the list of all occurrences.
An example
Consider how the algorithm works for a pattern , a text and the hash function described above.
At first, we calculate a hash value for the pattern:
Then, we calculate a hash value for the first substring starting from the right of the text:
We can see that the hash values are not equal, so we move to the next substring.
We see that h_p is equal to h_c, but having performed a symbol-by-symbol comparison we find out that actually, the strings are not equal. So, we move to the next step.
The hash values are not equal, move to the next step.
We see that and the strings are equal as well, so we have found an occurrence. Since all the text has been processed, we return a list consisting of the only founded index 0.
Note that in the first step we calculate a hash value for a substring using the formula (1) directly. The remaining substrings are calculated using the formula (2).
Complexity Analysis
Let’s estimate the running time of the algorithm for a pattern and a text (assuming that ).
At first, we calculate a hash value for the pattern and the first substring of the text using the formula (1). It takes 2·|p| operations. Then, we calculate a hash value for the remaining |t|−|p| substrings of the text using the formula (2). Each of these calculations requires only a constant number of operations. When the hash values of the pattern and the current substring are equal, we perform a symbol-by-symbol comparison which requires |p| operations in the worst case. So, the overall running time of the algorithm can be estimated as
where is the number of times the pattern occurs in the text.
There is one more detail to be taken into account. It may happen that a hash value of the pattern and the current substring are the same, but the strings are not equal. If this happens often, it may result in running time.
For example, if a pattern , a text , the Rabin-Karp algorithm will perform a symbol-by-symbol comparison of the pattern with each substring of the text, so the overall running time will be .
However, one may hardly encounter with such examples in real problems. Although even on “real” data collisions are possible, the probability of this event is very low () if is a big prime number. So, the Rabin-Karp algorithm is a good choice for one who needs to solve the problem of finding a substring in a string.