Recent from talks
Contribute something
Nothing was collected or created yet.
Rabin–Karp algorithm
View on WikipediaThis article includes a list of references, related reading, or external links, but its sources remain unclear because it lacks inline citations. (September 2018) |
| Class | String searching |
|---|---|
| Worst-case performance | plus preprocessing time |
| Average performance | |
| Worst-case space complexity |
In computer science, the Rabin–Karp algorithm or Karp–Rabin algorithm is a string-searching algorithm created by Richard M. Karp and Michael O. Rabin (1987) that uses hashing to find an exact match of a pattern string in a text. It uses a rolling hash to quickly filter out positions of the text that cannot match the pattern, and then checks for a match at the remaining positions. Generalizations of the same idea can be used to find more than one match of a single pattern, or to find matches for more than one pattern.
To find a single match of a single pattern, the expected time of the algorithm is linear in the combined length of the pattern and text, although its worst-case time complexity is the product of the two lengths. To find multiple matches, the expected time is linear in the input lengths, plus the combined length of all the matches, which could be greater than linear. In contrast, the Aho–Corasick algorithm can find all matches of multiple patterns in worst-case time and space linear in the input length and the number of matches (instead of the total length of the matches).
A practical application of the algorithm is detecting plagiarism. Given source material, the algorithm can rapidly search through a paper for instances of sentences from the source material, ignoring details such as case and punctuation. Because of the abundance of the sought strings, single-string searching algorithms are impractical.
Overview
[edit]A naive string matching algorithm compares the given pattern against all positions in the given text. Each comparison takes time proportional to the length of the pattern, and the number of positions is proportional to the length of the text. Therefore, the worst-case time for such a method is proportional to the product of the two lengths. In many practical cases, this time can be significantly reduced by cutting short the comparison at each position as soon as a mismatch is found, but this idea cannot guarantee any speedup.
Several string-matching algorithms, including the Knuth–Morris–Pratt algorithm and the Boyer–Moore string-search algorithm, reduce the worst-case time for string matching by extracting more information from each mismatch, allowing them to skip over positions of the text that are guaranteed not to match the pattern. The Rabin–Karp algorithm instead achieves its speedup by using a hash function to quickly perform an approximate check for each position, and then only performing an exact comparison at the positions that pass this approximate check.
A hash function is a function which converts every string into a numeric value, called its hash value; for example, we might have hash("hello")=5. If two strings are equal, their hash values are also equal. For a well-designed hash function, the inverse is true, in an approximate sense: strings that are unequal are very unlikely to have equal hash values. The Rabin–Karp algorithm proceeds by computing, at each position of the text, the hash value of a string starting at that position with the same length as the pattern. If this hash value equals the hash value of the pattern, it performs a full comparison at that position.
In order for this to work well, the hash function should be selected randomly from a family of hash functions that are unlikely to produce many false positives, that is, positions of the text which have the same hash value as the pattern but do not actually match the pattern. These positions contribute to the running time of the algorithm unnecessarily, without producing a match. Additionally, the hash function used should be a rolling hash, a hash function whose value can be quickly updated from each position of the text to the next. Recomputing the hash function from scratch at each position would be too slow.
The algorithm
[edit]The algorithm is as shown:
function RabinKarp(string s[1..n], string pattern[1..m])
hpattern := hash(pattern[1..m]);
for i from 1 to n-m+1
hs := hash(s[i..i+m-1])
if hs = hpattern
if s[i..i+m-1] = pattern[1..m]
return i
return not found
Lines 2, 4, and 6 each require O(m) time. However, line 2 is only executed once, and line 6 is only executed if the hash values match, which is unlikely to happen more than a few times. Line 5 is executed O(n) times, but each comparison only requires constant time, so its impact is O(n). The issue is line 4.
Naively computing the hash value for the substring s[i+1..i+m] requires O(m) time because each character is examined. Since the hash computation is done on each loop, the algorithm with a naive hash computation requires O(mn) time, the same complexity as a straightforward string matching algorithm. For speed, the hash must be computed in constant time. The trick is the variable hs already contains the previous hash value of s[i..i+m-1]. If that value can be used to compute the next hash value in constant time, then computing successive hash values will be fast.
The trick can be exploited using a rolling hash. A rolling hash is a hash function specially designed to enable this operation. A trivial (but not very good) rolling hash function just adds the values of each character in the substring. This rolling hash formula can compute the next hash value from the previous value in constant time:
s[i+1..i+m] = s[i..i+m-1] - s[i] + s[i+m]
This simple function works, but will result in statement 5 being executed more often than other more sophisticated rolling hash functions such as those discussed in the next section.
Good performance requires a good hashing function for the encountered data. If the hashing is poor (such as producing the same hash value for every input), then line 6 would be executed O(n) times (i.e. on every iteration of the loop). Because character-by-character comparison of strings with length m takes O(m) time, the whole algorithm then takes a worst-case O(mn) time.
Hash function used
[edit]The key to the Rabin–Karp algorithm's performance is the efficient computation of hash values of the successive substrings of the text. The Rabin fingerprint is a popular and effective rolling hash function. The hash function described here is not a Rabin fingerprint, but it works equally well. It treats every substring as a number in some base, the base being usually the size of the character set.
For example, if the substring is "hi", the base is 256, and prime modulus is 101, then the hash value would be
[(104 × 256 ) %[a] 101 + 105] % 101 = 65 (ASCII of 'h' is 104 and of 'i' is 105)
Technically, this algorithm is only similar to the true number in a non-decimal system representation, since for example we could have the "base" less than one of the "digits". See hash function for a much more detailed discussion. The essential benefit achieved by using a rolling hash such as the Rabin fingerprint is that it is possible to compute the hash value of the next substring from the previous one by doing only a constant number of operations, independent of the substrings' lengths.
For example, if we have text "abracadabra" and we are searching for a pattern of length 3, the hash of the first substring, "abr", using 256 as the base, and 101 as the prime modulus is:
// ASCII a = 97, b = 98, r = 114.
hash("abr") = [ ( [ ( [ (97 × 256) % 101 + 98 ] % 101 ) × 256 ] % 101 ) + 114 ] % 101 = 4
We can then compute the hash of the next substring, "bra", from the hash of "abr" by subtracting the number added for the first 'a' of "abr", i.e. 97 × 2562, multiplying by the base and adding for the last a of "bra", i.e. 97 × 2560. Like so:
// old hash (-ve avoider)[b] old 'a' left base offset base shift new 'a' prime modulus hash("bra") = [ ( 4 + 101 - 97 * [(256%101)*256] % 101[c] ) * 256[d] + 97 ] % 101 = 30
If we are matching the search string "bra", using similar calculation of hash("abr"),
hash'("bra") = [ ( [ ( [ ( 98 × 256) %101 + 114] % 101 ) × 256 ] % 101) + 97 ] % 101 = 30
If the substrings in question are long, this algorithm achieves great savings compared with many other hashing schemes.
Theoretically, there exist other algorithms that could provide convenient recomputation, e.g. multiplying together ASCII values of all characters so that shifting substring would only entail dividing the previous hash by the first character value, then multiplying by the new last character's value. The limitation, however, is the limited size of the integer data type and the necessity of using modular arithmetic to scale down the hash results.[e] Meanwhile, naive hash functions do not produce large numbers quickly, but, just like adding ASCII values, are likely to cause many hash collisions and hence slow down the algorithm. Hence the described hash function is typically the preferred one in the Rabin–Karp algorithm.
Multiple pattern search
[edit]The Rabin–Karp algorithm is inferior for single pattern searching to Knuth–Morris–Pratt algorithm, Boyer–Moore string-search algorithm and other faster single pattern string searching algorithms because of its slow worst case behavior. However, it is a useful algorithm for multiple pattern search.
To find any of a large number, say k, fixed length patterns in a text, a simple variant of the Rabin–Karp algorithm uses a Bloom filter or a set data structure to check whether the hash of a given string belongs to a set of hash values of patterns we are looking for:
function RabinKarpSet(string s[1..n], set of string subs, m):
set hsubs := emptySet
foreach sub in subs
insert hash(sub[1..m]) into hsubs
hs := hash(s[1..m])
for i from 1 to n-m+1
if hs ∈ hsubs and s[i..i+m-1] ∈ subs
return i
hs := hash(s[i+1..i+m])
return not found
We assume all the substrings have a fixed length m.
A naïve way to search for k patterns is to repeat a single-pattern search taking O(n+m) time, totaling in O((n+m)k) time. In contrast, the above algorithm can find all k patterns in O(n+km) expected time, assuming that a hash table check works in O(1) expected time.
Notes
[edit]- ^ % is 'mod' or modulo, or remainder after integer division, operator.
- ^ (-ve avoider) = "underflow avoider". Necessary if using unsigned integers for calculations. Because we know all hashes for prime modulus p, we can ensure no underflow by adding p to the old hash before subtracting the value corresponding to the old 'a' (mod p).
- ^ although
((256%101)*256)%101is the same as 2562 mod 101, to avoid overflowing integer maximums when the pattern string is longer (e.g. 'Rabin-Karp' is 10 characters, 2569 is the offset without modulation ), the pattern length base offset is pre-calculated in a loop, modulating the result each iteration. - ^ the last
* 256is the shift of the subtracted hash to the left. - ^ See hash function article.
References
[edit]Sources
[edit]- Candan, K. Selçuk; Sapino, Maria Luisa (2010). Data Management for Multimedia Retrieval. Cambridge University Press. pp. 205–206. ISBN 978-0-521-88739-7. (for the Bloom filter extension)
- Cormen, Thomas H.; Leiserson, Charles E.; Rivest, Ronald L.; Stein, Clifford (2001-09-01) [1990]. "The Rabin–Karp algorithm". Introduction to Algorithms (2nd ed.). Cambridge, Massachusetts: MIT Press. pp. 911–916. ISBN 978-0-262-03293-3.
- Karp, Richard M.; Rabin, Michael O. (March 1987). "Efficient randomized pattern-matching algorithms". IBM Journal of Research and Development. 31 (2): 249–260. CiteSeerX 10.1.1.86.9502. doi:10.1147/rd.312.0249.
External links
[edit]- "Rabin–Karp Algorithm/Rolling Hash" (PDF). MIT 6.006: Introduction to Algorithms 2011- Lecture Notes. MIT.
Rabin–Karp algorithm
View on GrokipediaIntroduction
Overview
The Rabin–Karp algorithm is a string-searching algorithm that employs hashing techniques to locate all occurrences of a given pattern string within a larger text string. Developed by Michael O. Rabin and Richard M. Karp, it was first published in 1987 as part of a broader exploration of randomized pattern-matching methods.[6] At its core, the algorithm computes a hash value for the pattern and corresponding hash values for overlapping substrings of the text, using these to rapidly identify candidate matches that require exact character-by-character verification to confirm or rule out false positives. This approach leverages a rolling hash mechanism to update substring hashes incrementally without recomputing from scratch each time. The algorithm demonstrates expected linear time performance of in practice, where is the text length and is the pattern length, making it particularly efficient for processing large texts or searching multiple patterns simultaneously. However, it carries a potential worst-case time complexity of if hash collisions occur frequently, necessitating robust hash functions to mitigate this risk.History
The Rabin–Karp algorithm was invented by Michael O. Rabin and Richard M. Karp in 1987 as a probabilistic approach to string matching.[7] Their work introduced randomized techniques to achieve expected linear-time performance for finding occurrences of a pattern within a larger text, addressing limitations of deterministic methods prevalent at the time.[7] The algorithm was detailed in their seminal paper, "Efficient Randomized Pattern-Matching Algorithms," published in the IBM Journal of Research and Development.[7] This publication built upon the broader context of 1980s advancements in string algorithms, including the Knuth–Morris–Pratt algorithm (1977) and the Boyer–Moore algorithm (1977), which had established efficient deterministic searching but often required complex preprocessing.[8] Rabin and Karp's contribution leveraged earlier ideas in universal hashing from Carter and Wegman (1977), adapting them to enable rapid substring comparisons via rolling hashes.[9] The initial motivation stemmed from the need to surpass the quadratic time complexity of naive string matching, which was inefficient for growing text corpora in early computing environments such as database systems and text processing tools at IBM.[7] By employing hashing to filter potential matches probabilistically, the algorithm offered practical speedups with low error probabilities, tunable through hash function choices.[7] By the late 1980s and into the 1990s, the Rabin–Karp method gained traction in algorithmic literature and software implementations, influencing tools for text search and pattern recognition; for instance, Robert Sedgewick's 1988 textbook on algorithms highlighted it as a key technique for string processing. Its adoption extended to open-source libraries and search utilities, where it supported efficient handling of multiple patterns and large datasets, paving the way for applications in information retrieval.Core Concepts
Rolling Hash Functions
A rolling hash function is a technique for efficiently computing hash values of substrings in a string by representing the string as a polynomial evaluated at a chosen base , allowing incremental updates as the substring window slides across the text. This approach treats each character as a coefficient in the polynomial, where the alphabet symbols are mapped to numerical values (e.g., using base for ASCII characters). The method enables time updates for consecutive substrings, making it suitable for pattern matching tasks like those in the Rabin–Karp algorithm.[2][10] For a pattern string of length , the initial hash value is computed as where is the numerical value of the -th character, is the base, and is a large prime modulus chosen to reduce the likelihood of collisions. This polynomial evaluation captures the entire substring in a single integer modulo , serving as a fingerprint that approximates the string's identity.[2][10] To compute the hash for the next sliding window in the text , starting from position , the update formula is This removes the contribution of the outgoing character (shifted by the highest power), multiplies by to adjust positions, and adds the incoming character , all modulo to keep values manageable. The operation exploits the polynomial structure to avoid recomputing the entire sum, ensuring constant-time transitions.[2][11] Key parameters include the modulus , typically a large prime such as or leveraging 64-bit unsigned integers for natural overflow (equivalent to mod ), and the base , selected as a prime number larger than the alphabet size to enhance uniformity (e.g., or for extended ASCII). These choices balance computational efficiency with collision resistance, as a good distributes hashes evenly across the range, minimizing spurious matches.[11][2] The primary property of this rolling hash is its use in fingerprinting, where the modulo operation produces a compact signature with a low probability of collisions for distinct strings, theoretically bounded by for random inputs. To further lower the false positive rate—especially for long texts—double hashing employs two independent hash functions with distinct primes and , requiring both hashes to match for a candidate position, reducing collision probability to approximately . This enhances reliability without significantly increasing computation.[10][11]Collision Resolution
In the Rabin–Karp algorithm, potential matches identified by equal hash values between the pattern and a sliding window of the text are verified through an exact character-by-character comparison to resolve collisions, ensuring that only true matches are reported.[10] This verification step is essential because hash collisions can occur when distinct strings produce the same hash value, leading to false positives if not checked.[12] The probability of a collision at any given position is approximately 1/p for random strings, where p is the chosen modulus in the hash function, assuming a uniform distribution over the hash space.[10] In the worst case, however, adversarial inputs can cause collisions at all n - m + 1 positions, forcing verification at every step and degrading performance.[13] To mitigate this, the algorithm can employ multiple independent rolling hash functions, such as two with distinct primes p and q, reducing the collision probability to roughly 1/(p * q) and making false positives exponentially unlikely.[10] This verification process adds O(m) time complexity per candidate match, where m is the pattern length, resulting in an expected overall runtime of O(n + m) for the algorithm under random inputs, but potentially O(nm) in the worst case due to frequent collisions.[12] In practice, using 64-bit hash functions renders collisions negligible for most texts shorter than 10^18 characters, as the per-position collision probability falls below 10^{-18}, ensuring high reliability without excessive verification overhead.[13]Algorithm Mechanics
Preprocessing
In the preprocessing phase of the Rabin–Karp algorithm, the hash value of the entire pattern string of length is computed to enable efficient comparisons during the search. This hash, denoted , is derived from a polynomial rolling hash function: where represents the numeric value of the -th character (e.g., ASCII code), is the base (often 256 for ASCII characters), and is a large prime modulus chosen to minimize collisions. The computation is typically performed iteratively to avoid overflow: initialize , then for each from 0 to , update . This step takes time. Additionally, a scaling factor is precomputed, which facilitates the efficient update of substring hashes in the text by allowing the removal of the leading character's contribution and the addition of the new trailing character in constant time per window shift. This precomputation also requires time using efficient exponentiation methods, but since it is done once, the overall preprocessing remains linear. The additional space used is beyond storing the input pattern. For illustration, consider the pattern "abc" where , , , , and . The hash is The scaling factor is . These values prepare the algorithm for sliding window comparisons without recomputing full hashes.Searching Process
The searching process of the Rabin–Karp algorithm begins after the pattern hash has been precomputed during preprocessing.[10] For a text of length and pattern of length , the algorithm first initializes the hash value for the initial window consisting of the first characters of , denoted as . This initial hash, , is calculated using the same hashing function applied to the pattern.[11] The core of the searching phase involves an iterative loop over possible starting positions in the text. For each position ranging from 0 to , the algorithm compares the current window hash (representing ) with . If the hashes match, an exact character-by-character verification is performed on the substring against to confirm a true match and avoid false positives due to hash collisions. If the verification succeeds, the starting index is recorded in a list of matches. The window is then slid forward by one position, updating to via the rolling hash mechanism, and the process repeats until all positions are checked.[10][11] This iterative approach efficiently scans the text by leveraging hash comparisons to skip unnecessary exact checks in most cases. The output of the searching process is a list of all starting indices where occurs in ; if no matches are found, the list is empty.[11] Edge cases are handled straightforwardly: if , no windows of size exist in , so the algorithm immediately returns an empty list of matches without entering the loop.[11] The following pseudocode outlines the searching process:function search(T, n, P, m, h_P):
if n < m:
return empty list
matches = empty list
h = hash of T[0..m-1] // Initialize first window hash
for j = 0 to n - m:
if h == h_P:
if T[j..j+m-1] == P: // Exact verification
append j to matches
if j < n - m:
h = update_hash(h, T[j], T[j+m], m) // Slide window
return matches
function search(T, n, P, m, h_P):
if n < m:
return empty list
matches = empty list
h = hash of T[0..m-1] // Initialize first window hash
for j = 0 to n - m:
if h == h_P:
if T[j..j+m-1] == P: // Exact verification
append j to matches
if j < n - m:
h = update_hash(h, T[j], T[j+m], m) // Slide window
return matches
