Two-way string-matching algorithm

ClassString-searching algorithm
Data structureAny string with an ordered alphabet
Worst-case performanceO(n)
Best-case performanceO(n)
Worst-case space complexityO(1)

In computer science, the two-way string-matching algorithm (or Crochemore-Perrin string-matching algorithm) is a string-searching algorithm, discovered by Maxime Crochemore and Dominique Perrin in 1991. It searches for copies of a "needle" of length m in a "haystack" of length n. After preprocessing the needle in time O(m), the actual search takes time O(n) linear in the length of the haystack.

The two-way algorithm can be viewed as a combination of the forward-going Knuth–Morris–Pratt algorithm (KMP) and the backward-running Boyer–Moore string-search algorithm (BM). Like those two, the two-way algorithm preprocesses the needle to find partially repeating periods and computes “shifts” based on them, indicating what offset to “jump” to in the haystack when a given character is encountered.

Unlike BM and KMP, it uses only O(1) additional space to store information about those partial repeats (assuming the transdichotomous model, where an index of ⌈log2(m)⌉ bits is counted as a single word). The preprocessing computes the "period" of the needle, the minimum offset between two consecutive matches (a value between 1 and m), and a "critical factorization" of the needle into a prefix and suffix with particular properties, specified by the length of the prefix (also less than m).

The actual matching operation performs at most 2n − m comparisons.

Breslauer later published two improved variants performing fewer comparisons, at the cost of storing additional data about the preprocessed needle:

The algorithm is considered fairly efficient in practice, being cache-friendly and using several operations that can be implemented in well-optimized subroutines. It is used by the C standard libraries glibc, newlib, and musl, to implement the memmem and strstr family of substring functions. As with most advanced string-search algorithms, the naïve implementation may be more efficient on small-enough instances; this is especially so if the needle isn't searched in multiple haystacks, which would amortize the preprocessing cost.

Critical factorization

Before we define critical factorization, we should define:

Finally, a critical factorization is a factorization ⁠⁠ of x such that ⁠⁠. The existence of a critical factorization is provably guaranteed. For a needle of length m in an ordered alphabet, it can be computed in 2m comparisons, by computing the lexicographically larger of two ordered maximal suffixes, defined for order ≤ and ≥.

The algorithm

The algorithm starts by computing a critical factorization of the needle n as the preprocessing step. This step produces the index (starting point) of the periodic right-half, and the period of this stretch. The suffix computation here follows the authors' formulation. It can alternatively be computed using the Duval's algorithm, which is simpler and still linear time but slower in practice.

Shorthand for inversion.
function cmp(a, b)
    if a > b return 1
    if a = b return 0
    if a < b return -1
function maxsuf(n, rev)
    length ← len(n)
    cur_period ← 1       currently known period.
    period_test_idx ← 1  index for period testing, 0 < period_test_idx <= cur_period.
    maxsuf_test_idx ← 0  index for maxsuf testing. greater than maxs.
    maxsuf_idx ← -1      the proposed starting index of maxsuf

    while maxsuf_test_idx + period_test_idx < length
        cmp_val ← cmp(
              n[maxsuf_test_idx + period_test_idx],
              n[maxsuf_idx      + period_test_idx]
        )
        if rev
            cmp_val *= -1
        if cmp_val < 0
            Suffix (maxsuf_test_idx + period_test_idx) is smaller. Period is the entire prefix so far.
            maxsuf_test_idx += period_test_idx
            period_test_idx ← 1
            cur_period ← maxsuf_test_idx - maxsuf_idx
        else if cmp_val == 0
            They are the same - we should go on.
            if period_test_idx == cur_period
                We are done checking this stretch of cur_period. reset period_test_idx.
                maxsuf_test_idx += cur_period
                period_test_idx ← 1
            else
                period_test_idx += 1
        else
            Suffix is larger. Start over from here.
            maxsuf_idx ← maxsuf_test_idx
            maxsuf_test_idx += 1
            cur_period ← 1
            period_test_idx ← 1
   return [maxsuf_idx, cur_period]
function crit_fact(n)
    [idx1, per1] ← maxsuf(n, false)
    [idx2, per2] ← maxsuf(n, true)
    if idx1 > idx2
        return [idx1, per1]
    else
        return [idx2, per2]

The comparison proceeds by first matching for the right-hand-side, and then for the left-hand-side if it matches. Linear-time skipping is done using the period.

function match(needle, haystack)
    needle_len   ← len(needle)
    haystack_len ← len(haystack)
    [length, cur_period] ← crit_fact(needle)
    Matches ← {}                             set of matches.

    Match the suffix.
    Use a library function like memcmp, or write your own loop.
    if needle[0] ... needle[length] == needle[length + 1] ... needle[length + cur_period]
        Matches ← {}
        pos ← 0
        s ← 0

    TODO. At least put the skip in.

References

  1. 1 2 3 Crochemore, Maxime; Perrin, Dominique (1 July 1991). "Two-way string-matching" (PDF). Journal of the ACM. 38 (3): 650–674. doi:10.1145/116825.116845. S2CID 15055316. Note that pseudocode in this paper uses one-based indexing.
  2. 1 2 Charras, Christian; Lecroq, Thierry (14 January 1997). "Two Way algorithm". Exact String Matching Algorithms. Institut Gaspard Monge.
  3. ↑ Breslauer, Dany (May 1996). "Saving comparisons in the Crochemore-Perrin string-matching algorithm" (PDF). Theoretical Computer Science. 158 (1–2): 177–192. doi:10.1016/0304-3975(95)00068-2. Archived from the original (PDF) on 2019-03-16.
  4. ↑ "musl/src/string/memmem.c". Retrieved 23 November 2019.
  5. ↑ "newlib/libc/string/memmem.c". Retrieved 23 November 2019.
  6. 1 2 "glibc/string/str-two-way.h".
  7. ↑ "Eric Blake - Re: PATCH] Improve performance of memmem". Newlib mailing list.
  8. ↑ Adamczyk, Zbigniew; Rytter, Wojciech (May 2013). "A note on a simple computation of the maximal suffix of a string" (PDF). Journal of Discrete Algorithms. 20: 61–64. doi:10.1016/j.jda.2013.03.002.