| This document explains Crochemore and Perrin's Two-Way string matching | |
| algorithm, in which a smaller string (the "pattern" or "needle") | |
| is searched for in a longer string (the "text" or "haystack"), | |
| determining whether the needle is a substring of the haystack, and if | |
| so, at what index(es). It is to be used by Python's string | |
| (and bytes-like) objects when calling `find`, `index`, `__contains__`, | |
| or implicitly in methods like `replace` or `partition`. | |
| This is essentially a re-telling of the paper | |
| Crochemore M., Perrin D., 1991, Two-way string-matching, | |
| Journal of the ACM 38(3):651-675. | |
| focused more on understanding and examples than on rigor. See also | |
| the code sample here: | |
| http://www-igm.univ-mlv.fr/~lecroq/string/node26.html#SECTION00260 | |
| The algorithm runs in O(len(needle) + len(haystack)) time and with | |
| O(1) space. However, since there is a larger preprocessing cost than | |
| simpler algorithms, this Two-Way algorithm is to be used only when the | |
| needle and haystack lengths meet certain thresholds. | |
| These are the basic steps of the algorithm: | |
| * "Very carefully" cut the needle in two. | |
| * For each alignment attempted: | |
| 1. match the right part | |
| * On failure, jump by the amount matched + 1 | |
| 2. then match the left part. | |
| * On failure jump by max(len(left), len(right)) + 1 | |
| * If the needle is periodic, don't re-do comparisons; maintain | |
| a "memory" of how many characters you already know match. | |
| -------- Matching the right part -------- | |
| We first scan the right part of the needle to check if it matches the | |
| the aligned characters in the haystack. We scan left-to-right, | |
| and if a mismatch occurs, we jump ahead by the amount matched plus 1. | |
| Example: | |
| text: ........EFGX................... | |
| pattern: ....abcdEFGH.... | |
| cut: <<<<>>>> | |
| Matched 3, so jump ahead by 4: | |
| text: ........EFGX................... | |
| pattern: ....abcdEFGH.... | |
| cut: <<<<>>>> | |
| Why are we allowed to do this? Because we cut the needle very | |
| carefully, in such a way that if the cut is ...abcd + EFGH... then | |
| we have | |
| d != E | |
| cd != EF | |
| bcd != EFG | |
| abcd != EFGH | |
| ... and so on. | |
| If this is true for every pair of equal-length substrings around the | |
| cut, then the following alignments do not work, so we can skip them: | |
| text: ........EFG.................... | |
| pattern: ....abcdEFGH.... | |
| ^ (Bad because d != E) | |
| text: ........EFG.................... | |
| pattern: ....abcdEFGH.... | |
| ^^ (Bad because cd != EF) | |
| text: ........EFG.................... | |
| pattern: ....abcdEFGH.... | |
| ^^^ (Bad because bcd != EFG) | |
| Skip 3 alignments => increment alignment by 4. | |
| -------- If len(left_part) < len(right_part) -------- | |
| Above is the core idea, and it begins to suggest how the algorithm can | |
| be linear-time. There is one bit of subtlety involving what to do | |
| around the end of the needle: if the left half is shorter than the | |
| right, then we could run into something like this: | |
| text: .....EFG...... | |
| pattern: cdEFGH | |
| The same argument holds that we can skip ahead by 4, so long as | |
| d != E | |
| cd != EF | |
| ?cd != EFG | |
| ??cd != EFGH | |
| etc. | |
| The question marks represent "wildcards" that always match; they're | |
| outside the limits of the needle, so there's no way for them to | |
| invalidate a match. To ensure that the inequalities above are always | |
| true, we need them to be true for all possible '?' values. We thus | |
| need cd != FG and cd != GH, etc. | |
| -------- Matching the left part -------- | |
| Once we have ensured the right part matches, we scan the left part | |
| (order doesn't matter, but traditionally right-to-left), and if we | |
| find a mismatch, we jump ahead by | |
| max(len(left_part), len(right_part)) + 1. That we can jump by | |
| at least len(right_part) + 1 we have already seen: | |
| text: .....EFG..... | |
| pattern: abcdEFG | |
| Matched 3, so jump by 4, | |
| using the fact that d != E, cd != EF, and bcd != EFG. | |
| But we can also jump by at least len(left_part) + 1: | |
| text: ....cdEF..... | |
| pattern: abcdEF | |
| Jump by len('abcd') + 1 = 5. | |
| Skip the alignments: | |
| text: ....cdEF..... | |
| pattern: abcdEF | |
| text: ....cdEF..... | |
| pattern: abcdEF | |
| text: ....cdEF..... | |
| pattern: abcdEF | |
| text: ....cdEF..... | |
| pattern: abcdEF | |
| This requires the following facts: | |
| d != E | |
| cd != EF | |
| bcd != EF? | |
| abcd != EF?? | |
| etc., for all values of ?s, as above. | |
| If we have both sets of inequalities, then we can indeed jump by | |
| max(len(left_part), len(right_part)) + 1. Under the assumption of such | |
| a nice splitting of the needle, we now have enough to prove linear | |
| time for the search: consider the forward-progress/comparisons ratio | |
| at each alignment position. If a mismatch occurs in the right part, | |
| the ratio is 1 position forward per comparison. On the other hand, | |
| if a mismatch occurs in the left half, we advance by more than | |
| len(needle)//2 positions for at most len(needle) comparisons, | |
| so this ratio is more than 1/2. This average "movement speed" is | |
| bounded below by the constant "1 position per 2 comparisons", so we | |
| have linear time. | |
| -------- The periodic case -------- | |
| The sets of inequalities listed so far seem too good to be true in | |
| the general case. Indeed, they fail when a needle is periodic: | |
| there's no way to split 'AAbAAbAAbA' in two such that | |
| (the stuff n characters to the left of the split) | |
| cannot equal | |
| (the stuff n characters to the right of the split) | |
| for all n. | |
| This is because no matter how you cut it, you'll get | |
| s[cut-3:cut] == s[cut:cut+3]. So what do we do? We still cut the | |
| needle in two so that n can be as big as possible. If we were to | |
| split it as | |
| AAbA + AbAAbA | |
| then A == A at the split, so this is bad (we failed at length 1), but | |
| if we split it as | |
| AA + bAAbAAbA | |
| we at least have A != b and AA != bA, and we fail at length 3 | |
| since ?AA == bAA. We already knew that a cut to make length-3 | |
| mismatch was impossible due to the period, but we now see that the | |
| bound is sharp; we can get length-1 and length-2 to mismatch. | |
| This is exactly the content of the *critical factorization theorem*: | |
| that no matter the period of the original needle, you can cut it in | |
| such a way that (with the appropriate question marks), | |
| needle[cut-k:cut] mismatches needle[cut:cut+k] for all k < the period. | |
| Even "non-periodic" strings are periodic with a period equal to | |
| their length, so for such needles, the CFT already guarantees that | |
| the algorithm described so far will work, since we can cut the needle | |
| so that the length-k chunks on either side of the cut mismatch for all | |
| k < len(needle). Looking closer at the algorithm, we only actually | |
| require that k go up to max(len(left_part), len(right_part)). | |
| So long as the period exceeds that, we're good. | |
| The more general shorter-period case is a bit harder. The essentials | |
| are the same, except we use the periodicity to our advantage by | |
| "remembering" periods that we've already compared. In our running | |
| example, say we're computing | |
| "AAbAAbAAbA" in "bbbAbbAAbAAbAAbbbAAbAAbAAbAA". | |
| We cut as AA + bAAbAAbA, and then the algorithm runs as follows: | |
| First alignment: | |
| bbbAbbAAbAAbAAbbbAAbAAbAAbAA | |
| AAbAAbAAbA | |
| ^^X | |
| - Mismatch at third position, so jump by 3. | |
| - This requires that A!=b and AA != bA. | |
| Second alignment: | |
| bbbAbbAAbAAbAAbbbAAbAAbAAbAA | |
| AAbAAbAAbA | |
| ^^^^^^^^ | |
| X | |
| - Matched entire right part | |
| - Mismatch at left part. | |
| - Jump forward a period, remembering the existing comparisons | |
| Third alignment: | |
| bbbAbbAAbAAbAAbbbAAbAAbAAbAA | |
| AAbAAbAAbA | |
| mmmmmmm^^X | |
| - There's "memory": a bunch of characters were already matched. | |
| - Two more characters match beyond that. | |
| - The 8th character of the right part mismatched, so jump by 8 | |
| - The above rule is more complicated than usual: we don't have | |
| the right inequalities for lengths 1 through 7, but we do have | |
| shifted copies of the length-1 and length-2 inequalities, | |
| along with knowledge of the mismatch. We can skip all of these | |
| alignments at once: | |
| bbbAbbAAbAAbAAbbbAAbAAbAAbAA | |
| AAbAAbAAbA | |
| ~ A != b at the cut | |
| bbbAbbAAbAAbAAbbbAAbAAbAAbAA | |
| AAbAAbAAbA | |
| ~~ AA != bA at the cut | |
| bbbAbbAAbAAbAAbbbAAbAAbAAbAA | |
| AAbAAbAAbA | |
| ^^^^X 7-3=4 match, and the 5th misses. | |
| bbbAbbAAbAAbAAbbbAAbAAbAAbAA | |
| AAbAAbAAbA | |
| ~ A != b at the cut | |
| bbbAbbAAbAAbAAbbbAAbAAbAAbAA | |
| AAbAAbAAbA | |
| ~~ AA != bA at the cut | |
| bbbAbbAAbAAbAAbbbAAbAAbAAbAA | |
| AAbAAbAAbA | |
| ^X 7-3-3=1 match and the 2nd misses. | |
| bbbAbbAAbAAbAAbbbAAbAAbAAbAA | |
| AAbAAbAAbA | |
| ~ A != b at the cut | |
| Fourth alignment: | |
| bbbAbbAAbAAbAAbbbAAbAAbAAbAA | |
| AAbAAbAAbA | |
| ^X | |
| - Second character mismatches, so jump by 2. | |
| Fifth alignment: | |
| bbbAbbAAbAAbAAbbbAAbAAbAAbAA | |
| AAbAAbAAbA | |
| ^^^^^^^^ | |
| X | |
| - Right half matches, so use memory and skip ahead by period=3 | |
| Sixth alignment: | |
| bbbAbbAAbAAbAAbbbAAbAAbAAbAA | |
| AAbAAbAAbA | |
| mmmmmmmm^^ | |
| - Right part matches, left part is remembered, found a match! | |
| The one tricky skip by 8 here generalizes: if we have a period of p, | |
| then the CFT says we can ensure the cut has the inequality property | |
| for lengths 1 through p-1, and jumping by p would line up the | |
| matching characters and mismatched character one period earlier. | |
| Inductively, this proves that we can skip by the number of characters | |
| matched in the right half, plus 1, just as in the original algorithm. | |
| To make it explicit, the memory is set whenever the entire right part | |
| is matched and is then used as a starting point in the next alignment. | |
| In such a case, the alignment jumps forward one period, and the right | |
| half matches all except possibly the last period. Additionally, | |
| if we cut so that the left part has a length strictly less than the | |
| period (we always can!), then we can know that the left part already | |
| matches. The memory is reset to 0 whenever there is a mismatch in the | |
| right part. | |
| To prove linearity for the periodic case, note that if a right-part | |
| character mismatches, then we advance forward 1 unit per comparison. | |
| On the other hand, if the entire right part matches, then the skipping | |
| forward by one period "defers" some of the comparisons to the next | |
| alignment, where they will then be spent at the usual rate of | |
| one comparison per step forward. Even if left-half comparisons | |
| are always "wasted", they constitute less than half of all | |
| comparisons, so the average rate is certainly at least 1 move forward | |
| per 2 comparisons. | |
| -------- When to choose the periodic algorithm --------- | |
| The periodic algorithm is always valid but has an overhead of one | |
| more "memory" register and some memory computation steps, so the | |
| here-described-first non-periodic/long-period algorithm -- skipping by | |
| max(len(left_part), len(right_part)) + 1 rather than the period -- | |
| should be preferred when possible. | |
| Interestingly, the long-period algorithm does not require an exact | |
| computation of the period; it works even with some long-period, but | |
| undeniably "periodic" needles: | |
| Cut: AbcdefAbc == Abcde + fAbc | |
| This cut gives these inequalities: | |
| e != f | |
| de != fA | |
| cde != fAb | |
| bcde != fAbc | |
| Abcde != fAbc? | |
| The first failure is a period long, per the CFT: | |
| ?Abcde == fAbc?? | |
| A sufficient condition for using the long-period algorithm is having | |
| the period of the needle be greater than | |
| max(len(left_part), len(right_part)). This way, after choosing a good | |
| split, we get all of the max(len(left_part), len(right_part)) | |
| inequalities around the cut that were required in the long-period | |
| version of the algorithm. | |
| With all of this in mind, here's how we choose: | |
| (1) Choose a "critical factorization" of the needle -- a cut | |
| where we have period minus 1 inequalities in a row. | |
| More specifically, choose a cut so that the left_part | |
| is less than one period long. | |
| (2) Determine the period P_r of the right_part. | |
| (3) Check if the left part is just an extension of the pattern of | |
| the right part, so that the whole needle has period P_r. | |
| Explicitly, check if | |
| needle[0:cut] == needle[0+P_r:cut+P_r] | |
| If so, we use the periodic algorithm. If not equal, we use the | |
| long-period algorithm. | |
| Note that if equality holds in (3), then the period of the whole | |
| string is P_r. On the other hand, suppose equality does not hold. | |
| The period of the needle is then strictly greater than P_r. Here's | |
| a general fact: | |
| If p is a substring of s and p has period r, then the period | |
| of s is either equal to r or greater than len(p). | |
| We know that needle_period != P_r, | |
| and therefore needle_period > len(right_part). | |
| Additionally, we'll choose the cut (see below) | |
| so that len(left_part) < needle_period. | |
| Thus, in the case where equality does not hold, we have that | |
| needle_period >= max(len(left_part), len(right_part)) + 1, | |
| so the long-period algorithm works, but otherwise, we know the period | |
| of the needle. | |
| Note that this decision process doesn't always require an exact | |
| computation of the period -- we can get away with only computing P_r! | |
| -------- Computing the cut -------- | |
| Our remaining tasks are now to compute a cut of the needle with as | |
| many inequalities as possible, ensuring that cut < needle_period. | |
| Meanwhile, we must also compute the period P_r of the right_part. | |
| The computation is relatively simple, essentially doing this: | |
| suffix1 = max(needle[i:] for i in range(len(needle))) | |
| suffix2 = ... # the same as above, but invert the alphabet | |
| cut1 = len(needle) - len(suffix1) | |
| cut2 = len(needle) - len(suffix2) | |
| cut = max(cut1, cut2) # the later cut | |
| For cut2, "invert the alphabet" is different than saying min(...), | |
| since in lexicographic order, we still put "py" < "python", even | |
| if the alphabet is inverted. Computing these, along with the method | |
| of computing the period of the right half, is easiest to read directly | |
| from the source code in fastsearch.h, in which these are computed | |
| in linear time. | |
| Crochemore & Perrin's Theorem 3.1 give that "cut" above is a | |
| critical factorization less than the period, but a very brief sketch | |
| of their proof goes something like this (this is far from complete): | |
| * If this cut splits the needle as some | |
| needle == (a + w) + (w + b), meaning there's a bad equality | |
| w == w, it's impossible for w + b to be bigger than both | |
| b and w + w + b, so this can't happen. We thus have all of | |
| the inequalities with no question marks. | |
| * By maximality, the right part is not a substring of the left | |
| part. Thus, we have all of the inequalities involving no | |
| left-side question marks. | |
| * If you have all of the inequalities without right-side question | |
| marks, we have a critical factorization. | |
| * If one such inequality fails, then there's a smaller period, | |
| but the factorization is nonetheless critical. Here's where | |
| you need the redundancy coming from computing both cuts and | |
| choosing the later one. | |
| -------- Some more Bells and Whistles -------- | |
| Beyond Crochemore & Perrin's original algorithm, we can use a couple | |
| more tricks for speed in fastsearch.h: | |
| 1. Even though C&P has a best-case O(n/m) time, this doesn't occur | |
| very often, so we add a Boyer-Moore bad character table to | |
| achieve sublinear time in more cases. | |
| 2. The prework of computing the cut/period is expensive per | |
| needle character, so we shouldn't do it if it won't pay off. | |
| For this reason, if the needle and haystack are long enough, | |
| only automatically start with two-way if the needle's length | |
| is a small percentage of the length of the haystack. | |
| 3. In cases where the needle and haystack are large but the needle | |
| makes up a significant percentage of the length of the | |
| haystack, don't pay the expensive two-way preprocessing cost | |
| if you don't need to. Instead, keep track of how many | |
| character comparisons are equal, and if that exceeds | |
| O(len(needle)), then pay that cost, since the simpler algorithm | |
| isn't doing very well. |