mirror of
				https://github.com/python/cpython.git
				synced 2025-11-03 19:34:08 +00:00 
			
		
		
		
	
		
			
				
	
	
		
			431 lines
		
	
	
	
		
			16 KiB
		
	
	
	
		
			Text
		
	
	
	
	
	
			
		
		
	
	
			431 lines
		
	
	
	
		
			16 KiB
		
	
	
	
		
			Text
		
	
	
	
	
	
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.
 |