The Boyer–Moore Algorithm

Prerequisite: The Prefix Function and KMP Algorithm.

The KMP algorithm makes maximal use of prefix-matching information,

whereas the basic idea behind the BM algorithm is to obtain more information through suffix matching than through prefix matching, enabling faster character jumps.

Introduction

Imagine that our pattern string patpat is placed at the left end of the text string stringstring, with their first characters aligned.

pat:EXAMPLEstring:HEREISASIMPLEEXAMPLE…     ⇑\begin{aligned} \textit{pat}:\qquad\qquad &\mathtt{EXAMPLE} \\ \textit{string}:\qquad\quad &\mathtt{HERE IS A SIMPLE EXAMPLE} \dots \\ &\qquad\ \ \ \, \, \Uparrow \end{aligned}

We make the following definitions and will not repeat them later:

The length of patpat is patlenpatlen. In particular, for a zero-indexed string, patlastpos=patlen−1patlastpos=patlen-1 is defined as the position of the last character of patpat;

the length of stringstring is stringlenstringlen, and stringlastpos=stringlen−1stringlastpos = stringlen-1.

Suppose we know the stringstring-th character patlenpatlen of charchar (aligned with the last character of patpat). What information can we obtain?

Observation 1

If we know that the character charchar does not occur in patpat, we do not need to consider occurrences of patpat starting at the stringstringst, 11nd, ..., or 22-th character of patlenpatlen; we can move patpat forward by patlenpatlen characters at once.

Observation 2

More generally, if the rightmost occurrence of patpat in charchar (that is, the position closest to the end) is delta1delta_1 characters away from the end,

then we can move patpat backward by delta1delta_1 characters without matching. If the shift distance were less than delta1delta_1, the character charchar alone could not be matched, so the pattern string patpat could not match either.

Therefore, unless charchar can match the last character of patpat, stringstring must skip delta1delta_1 characters (equivalently, patpat moves backward by delta1delta_1 characters). We can define a function delta1delta_1 to calculate delta1(char)delta_1(char):

int delta1(char char)if char不在pat中 || char是pat上最后一个字符return patlenelsereturn patlastpos−i// i为出现在pat最末尾的那一个char出现的位置,即pat[i]=char\begin{array}{ll} \textbf{int}\ delta1(\textbf{char}\ char) \\ \qquad \textbf{if}\ \text{char不在pat中 || char是pat上最后一个字符} \\ \qquad\qquad\textbf{return}\ patlen \\ \qquad \textbf{else} \\ \qquad\qquad\textbf{return}\ patlastpos-i\quad\textbf{//}\ \text{i为出现在pat最末尾的那一个char出现的位置,即pat[i]=char} \end{array}

Note that, obviously, this table only needs to be computed through position patlastpos−1patlastpos-1.

Now suppose that charchar matches the last character of patpat. We then check whether the character before charchar matches the second-to-last character of patpat:

If it does, continue moving backward until the entire pattern patpat has been matched (we have then found a successful match of stringstring in patpat);

alternatively, after matching the patpat-th character from the end of mm, we may encounter a mismatch at the m+1m+1-th character from the end. We then want to move patpat forward to the next position where a match may be possible, and naturally we want to shift it as far as possible.

Observation 3(a)

In Observation 2, suppose that after matching the last patpat characters of mm, a mismatch occurs at the m+1m+1-th character from the end. To align the mismatching character in stringstring with the corresponding character in patpat,

we need to shift patpat by kk characters, which means that we should focus on the k+mk+m characters afterward (the character in patpat aligned with the end of stringstring after shifting by k).

Since k=delta1−mk=delta_1-m,

our attention should move stringstring characters forward along delta1−m+m=delta1delta_1-m+m = delta_1.

However, we may be able to skip more characters. Continue reading.

Observation 3(b)

Suppose we know that the next stringstring characters of mm match the last patpat characters of mm; call this substring subpatsubpat.

We also know that the substring after the mismatching character stringstring in charchar matches subpatsubpat. If patpat occurs before the corresponding mismatching character in subpatsubpat, we can shift patpat by some distance

so that the charchar occurring before the character corresponding to the mismatching patpat in subpatsubpat (a plausible reoccurrence, abbreviated PR below) aligns with the stringstring in subpatsubpat. If patpat occurs multiple times in subpatsubpat, take the first one in the right-to-left suffix-matching order (the rightmost plausible reoccurrence, abbreviated RPR below).

Suppose patpat is shifted by kk characters (that is, the distance between the terminal patpat of subpatsubpat and its rightmost plausible reoccurrence). Then our attention should move stringstring characters forward along k+mk+m. We call this distance delta2(j)delta_2(j):

Let rpr(j)rpr(j) be the position of the rightmost plausible reoccurrence of subpat=pat[j+1…patlastpos]subpat=pat[j+1\dots patlastpos] when the mismatch occurs at pat[j]pat[j], with rpr(j)<jrpr(j) < j (this is only a simple definition; the algorithm-design section below gives a more precise discussion). Then clearly k=j−rpr(j), m=patlastpos−jk=j-rpr(j),\ m=patlastpos-j.

Therefore:

int delta2(int j)// j为失配字符在pat上对应字符的位置return patlastpos−rpr(j)\begin{array}{ll} \textbf{int}\ delta2(\textbf{int}\ j) \quad\textbf{//}\ \text{j为失配字符在pat上对应字符的位置} \\ \qquad\qquad\textbf{return}\ patlastpos-rpr(j) \\ \end{array}

Thus, on a mismatch, we can skip stringstring characters of max⁡(delta1,delta2)\max(delta_1,delta_2).

Process

The arrow points to the mismatching character charchar:

pat:AT−THATstring:   … WHICH−FINALLY−HALTS.−−AT−THAT−POINT…     ⇑\begin{aligned} \textit{pat}:\qquad\qquad &\mathtt{AT-THAT} \\ \textit{string}:\ \ \ \dots\ &\mathtt{WHICH-FINALLY-HALTS.--AT-THAT-POINT} \dots \\ &\qquad\ \ \ \, \, \Uparrow \end{aligned}

F\mathtt{F} does not occur in patpat. By Observation 1, move patpat forward by patlenpatlen characters, namely 7 characters:

pat:   AT−THATstring:   … WHICH−FINALLY−HALTS.−−AT−THAT−POINT…        ⇑\begin{aligned} \textit{pat}:\qquad\qquad &\qquad\quad\ \ \, \mathtt{AT-THAT} \\ \textit{string}:\ \ \ \dots\ &\mathtt{WHICH-FINALLY-HALTS.--AT-THAT-POINT} \dots \\ &\qquad\ \ \ \, \, \qquad\quad\ \ \ \Uparrow \end{aligned}

By Observation 2, we need to move patpat forward by 4 characters so that the hyphen aligns:

pat:   AT−THATstring:   … WHICH−FINALLY−HALTS.−−AT−THAT−POINT…     ⇑\begin{aligned} \textit{pat}:\qquad\qquad &\qquad\qquad\quad\ \ \, \mathtt{AT-THAT} \\ \textit{string}:\ \ \ \dots\ &\mathtt{WHICH-FINALLY-HALTS.--AT-THAT-POINT} \dots \\ &\qquad\ \ \ \; \qquad\qquad\qquad \Uparrow \end{aligned}

Now char:T\mathtt{T} matches. Move the pointer on stringstring one step to the left and continue matching:

pat:   AT−THATstring:   … WHICH−FINALLY−HALTS.−−AT−THAT−POINT…       ⇑\begin{aligned} \textit{pat}:\qquad\qquad &\qquad\qquad\quad\ \ \, \mathtt{AT-THAT} \\ \textit{string}:\ \ \ \dots\ &\mathtt{WHICH-FINALLY-HALTS.--AT-THAT-POINT} \dots \\ &\qquad\ \ \ \; \qquad\qquad\quad\ \, \Uparrow \end{aligned}

By Observation 3(a), L\mathtt{L} mismatches. Since L\mathtt{L} does not occur in patpat, move patpat by k=delta1−m=7−1=6k=delta_1-m=7-1=6 characters, and move the pointer on stringstring forward by delta1=7delta_1=7 characters:

pat:    AT−THATstring:   … WHICH−FINALLY−HALTS.−−AT−THAT−POINT…          ⇑\begin{aligned} \textit{pat}:\qquad\qquad &\qquad\qquad\qquad\qquad\ \ \,\, \mathtt{AT-THAT} \\ \textit{string}:\ \ \ \dots\ &\mathtt{WHICH-FINALLY-HALTS.--AT-THAT-POINT} \dots \\ &\qquad\ \ \ \; \qquad\qquad\qquad\qquad\ \ \ \, \, \Uparrow \end{aligned}

Now charchar again matches the last character patpat of T\mathtt{T}. The pointer on stringstring moves left and matches A\mathtt{A}; continuing left, it encounters a mismatch at −\mathtt{-}:

pat:    AT−THATstring:   … WHICH−FINALLY−HALTS.−−AT−THAT−POINT…          ⇑\begin{aligned} \textit{pat}:\qquad\qquad &\qquad\qquad\qquad\qquad\ \ \,\, \mathtt{AT-THAT} \\ \textit{string}:\ \ \ \dots\ &\mathtt{WHICH-FINALLY-HALTS.--AT-THAT-POINT} \dots \\ &\qquad\ \ \ \; \qquad\qquad\qquad\quad\ \ \ \,\, \Uparrow \end{aligned}

Intuitively, by Observation 3(b), we now shift patpat by k=5k=5 characters so that the suffix AT\mathtt{AT} aligns. This shift gives the maximum movement of the stringstring pointer. Here delta2=k+patlastpos−j=5+6−4=7delta_2=k+patlastpos-j=5+6-4=7, so the pointer on stringstring moves forward by 7 characters.

From the formal perspective, delta1=7−1−2=4, delta2=7,max⁡(delta1,delta2)=7delta_1=7-1-2=4,\ delta_2=7, \max(delta_1,delta_2)= 7, which formally supports the jump from Observation 3(b):

pat:   AT−THATstring:   … WHICH−FINALLY−HALTS.−−AT−THAT−POINT…         ⇑\begin{aligned} \textit{pat}:\qquad\qquad &\qquad\qquad\qquad\qquad\qquad\quad \;\, \mathtt{AT-THAT} \\ \textit{string}:\ \ \ \dots\ &\mathtt{WHICH-FINALLY-HALTS.--AT-THAT-POINT} \dots \\ &\qquad\ \ \ \; \qquad\qquad\qquad\qquad\qquad\quad \ \ \; \Uparrow \end{aligned}

We now find that every character in patpat equals the corresponding character in stringstring; we have found a match of stringstring in patpat. We used only 14 references to stringstring: 7 comparisons were necessary to complete a successful match (patlen=7patlen=7), and the other 7 let us skip 22 characters.

Algorithm Design

Initial Matching Algorithm

Explanation

Consider the following algorithm for string matching using delta1delta_1 and delta2delta_2:

i←patlastpos.j←patlastpos.loopif j<0return i+1if string[i]=pat[j]j←j−1i←i−1continuei←i+max(delta1(string[i]),delta2(j))if i>stringlastposreturn falsej←patlastpos\begin{array}{ll} i \gets patlastpos. \\ j \gets patlastpos. \\ \textbf{loop}\\ \qquad \textbf{if}\ j < 0 \\ \qquad \qquad \textbf{return}\ i+1 \\ \\ \qquad \textbf{if}\ string[i]=pat[j] \\ \qquad \qquad j \gets j-1 \\ \qquad \qquad i \gets i-1 \\ \qquad \qquad \textbf{continue} \\ \\ \qquad i \gets i+max(delta_1(string[i]), delta_2(j)) \\ \\ \qquad \textbf{if}\ i > stringlastpos \\ \qquad \qquad \textbf{return}\ false \\ \qquad j \gets patlastpos \\ \end{array}

If the algorithm above returns return false\textbf{return}\ false, patpat does not occur in stringstring; if it returns a number, that number is the position of the first occurrence of patpat from the left in stringstring.

Let us now describe more precisely how to calculate the function delta2delta_2 on which rpr(j)rpr(j) relies.

By the definition above, rpr(j)rpr(j) is the position associated with a mismatch at pat(j)pat(j): it is the rightmost plausible reoccurrence position of the substring subpat=pat[j+1…patlastpos]subpat=pat[j+1\dots patlastpos] at pat[j]pat[j].

In other words, we need to find the best kk such that pat[k…k+patlastpos−j−1]=pat[j+1…patlastpos]pat[k\dots k+patlastpos-j-1]=pat[j+1\dots patlastpos], while also considering two special cases:

  1. When k<0k<0, this is equivalent to adding a virtual prefix before patpat, and it is consistent with the principle of the delta2delta_2 jump.
  2. When k>0k>0, if pat[k−1]=pat[j]pat[k-1]=pat[j], then pat[k…k+patlastpos−j−1]pat[k\dots k+patlastpos-j-1] cannot be a plausible reoccurrence of subpatsubpat.
    The reason is that pat[j]pat[j] is itself the mismatching character, so after shifting patpat by kk characters, suffix matching would still fail at pat[k−1]pat[k-1].

There are also two restrictions:

  1. k<jk < j. When k=jk=j, we have pat[k]=pat[j]pat[k]=pat[j], so the character that mismatched at pat[j]pat[j] would also mismatch at pat[k]pat[k].
  2. Since delta2(patlastpos)=0delta_2(patlastpos)= 0, define rpr(patlastpos)=patlastposrpr(patlastpos) = patlastpos.

Process

Because understanding rpr(j)rpr(j) is central to implementing the Boyer–Moore algorithm, we use the following two examples for a detailed explanation:

j:  012345678pat:  ABCXXXABCrpr(j):  543210218sgn:  −−−−−−−−+\begin{aligned} \textit{j}:\qquad\qquad\quad\ \ &\mathtt{0 1 2 3 4 5 6 7 8} \\ \textit{pat}:\qquad\qquad\ \ &\mathtt{A B C X X X A B C} \\ \textit{rpr(j)}:\qquad\quad\ \ &\mathtt{5 4 3 2 1 0 2 1 8} \\ \textit{sgn}:\qquad\qquad\ \ &\mathtt{- - - - - - - - +} \end{aligned}

For rpr(0)rpr(0), subpatsubpat is BCXXXABC\mathtt{BCXXXABC}. The rightmost plausible reoccurrence before pat[0]pat[0] can only be [(BCXXX)ABC]XXXABC\mathtt{[(BCXXX)ABC]XXXABC}, so its position is -5, that is, rpr(j)=−5rpr(j)=-5.

For rpr(1)rpr(1), subpatsubpat is CXXXABC\mathtt{CXXXABC}. The rightmost plausible reoccurrence before pat[1]pat[1] is [(CXXX)ABC]XXXABC\mathtt{[(CXXX)ABC]XXXABC}, so rpr(j)=−4rpr(j)=-4.

For rpr(2)rpr(2), subpatsubpat is XXXABC\mathtt{XXXABC}. The rightmost plausible reoccurrence before pat[2]pat[2] is [(XXX)ABC]XXXABC\mathtt{[(XXX)ABC]XXXABC}, so rpr(j)=−3rpr(j)=-3.

For rpr(3)rpr(3), subpatsubpat is XXABC\mathtt{XXABC}. The rightmost plausible reoccurrence before pat[3]pat[3] is [(XX)ABC]XXXABC\mathtt{[(XX)ABC]XXXABC}, so rpr(j)=−2rpr(j)=-2.

For rpr(4)rpr(4), subpatsubpat is XABC\mathtt{XABC}. The rightmost plausible reoccurrence before pat[4]pat[4] is [(X)ABC]XXXABC\mathtt{[(X)ABC]XXXABC}, so rpr(j)=−1rpr(j)=-1.

For rpr(5)rpr(5), subpatsubpat is ABC\mathtt{ABC}. The rightmost plausible reoccurrence before pat[5]pat[5] is [ABC]XXXABC\mathtt{[ABC]XXXABC}, so rpr(j)=0rpr(j)=0.

For rpr(6)rpr(6), subpatsubpat is BC\mathtt{BC}. Since string[0]=string[6]string[0]=string[6], that is, string[0]string[0] equals the mismatching character string[6]string[6], string[0…2]string[0\dots 2] is not a plausible reoccurrence of subpatsubpat. Thus the rightmost plausible reoccurrence is [(BC)]ABCXXXABC\mathtt{[(BC)]ABCXXXABC}, so rpr(j)=−2rpr(j)=-2.

For rpr(7)rpr(7), subpatsubpat is C\mathtt{C}. Similarly, since string[7]=string[1]string[7]=string[1], string[1…2]string[1\dots 2] is not a plausible reoccurrence of subpatsubpat; the rightmost plausible reoccurrence is [(C)]ABCXXXABC\mathtt{[(C)]ABCXXXABC}, so rpr(j)=−1rpr(j)=-1.

For rpr(8)rpr(8), by the definition of delta2delta_2, rpr(patlastpos)=patlastposrpr(patlastpos)=patlastpos, so rpr(8)=8rpr(8)=8.

Now consider another example:

j:  012345678pat:  ABYXCDEYXrpr(j):  876543218sgn:  −−−−−−+−+\begin{aligned} \textit{j}:\qquad\qquad\quad\ \ &\mathtt{0 1 2 3 4 5 6 7 8} \\ \textit{pat}:\qquad\qquad\ \ &\mathtt{A B Y X C D E Y X} \\ \textit{rpr(j)}:\qquad\quad\ \ &\mathtt{8 7 6 5 4 3 2 1 8} \\ \textit{sgn}:\qquad\qquad\ \ &\mathtt{- - - - - - + - +} \end{aligned}

For rpr(0)rpr(0), subpatsubpat is BYXCDEYX\mathtt{BYXCDEYX}. The rightmost plausible reoccurrence before pat[0]pat[0] can only be [(BYXCDEYX)]ABYXCDEYX\mathtt{[(BYXCDEYX)]ABYXCDEYX}, so its position is -8, that is, rpr(j)=−8rpr(j)=-8.

For rpr(1)rpr(1), subpatsubpat is YXCDEYX\mathtt{YXCDEYX}. The only rightmost plausible reoccurrence before pat[1]pat[1] is [(YXCDEYX)]ABYXCDEYX\mathtt{[(YXCDEYX)]ABYXCDEYX}, so rpr(j)=−7rpr(j)=-7.

For rpr(2)rpr(2), subpatsubpat is XCDEYX\mathtt{XCDEYX}. The only rightmost plausible reoccurrence before pat[2]pat[2] is [(XCDEYX)]ABYXCDEYX\mathtt{[(XCDEYX)]ABYXCDEYX}, so rpr(j)=−6rpr(j)=-6.

For rpr(3)rpr(3), subpatsubpat is CDEYX\mathtt{CDEYX}. The only rightmost plausible reoccurrence before pat[3]pat[3] is [(CDEYX)]ABYXCDEYX\mathtt{[(CDEYX)]ABYXCDEYX}, so rpr(j)=−5rpr(j)=-5.

For rpr(4)rpr(4), subpatsubpat is DEYX\mathtt{DEYX}. The only rightmost plausible reoccurrence before pat[4]pat[4] is [(DEYX)]ABYXCDEYX\mathtt{[(DEYX)]ABYXCDEYX}, so rpr(j)=−4rpr(j)=-4.

For rpr(5)rpr(5), subpatsubpat is EYX\mathtt{EYX}. The only rightmost plausible reoccurrence before pat[5]pat[5] is [(EYX)]ABYXCDEYX\mathtt{[(EYX)]ABYXCDEYX}, so rpr(j)=−3rpr(j)=-3.

For rpr(6)rpr(6), subpatsubpat is YX\mathtt{YX}. Since string[2…3]=string[7…8]string[2\dots 3]=string[7\dots 8] and string[6]≠string[1]string[6]\neq string[1], the rightmost plausible reoccurrence before pat[6]pat[6] is AB[YX]CDEYX\mathtt{AB[YX]CDEYX}, so rpr(j)=2rpr(j)=2.

For rpr(7)rpr(7), subpatsubpat is X\mathtt{X}. Although string[3]=string[8]string[3]=string[8], string[2]=string[7]string[2] = string[7], so the rightmost plausible reoccurrence before pat[7]pat[7] is [X]ABYXCDEYX\mathtt{[X]ABYXCDEYX}, so rpr(j)=−1rpr(j)=-1;

For rpr(8)rpr(8), by the definition of delta2delta_2, rpr(patlastpos)=patlastposrpr(patlastpos)=patlastpos, so rpr(8)=8rpr(8)=8.

An Improvement to the Matching Algorithm

Finally, in practice, about 80% of the search time is estimated to be spent on the jump in Observation 1: string[i]string[i] does not match pat[patlastpos]pat[patlastpos], so we jump the entire patlenpatlen and begin the next match.

This can be specially optimized:

Define delta0delta0:

int delta0(char char)if char=pat[patlastpos]return large  // large为一个整数,需要满足large>stringlastpos+patlenreturn delta1(char)\begin{array}{ll} \textbf{int}\ delta0(\textbf{char}\ char) \\ \qquad \textbf{if}\ char=pat[patlastpos] \\ \qquad\qquad \textbf{return}\ large\ \ \text{// large为一个整数,需要满足large>stringlastpos+patlen} \\ \qquad \textbf{return}\ delta1(char) \end{array}

Replacing delta0delta0 with delta1delta_1 gives the improved matching algorithm:

i←patlastposloopif i>stringlastposreturn falsewhile i<stringleni←i+delta0(string(i))  // 除非string[i]和pat末尾字符匹配,否则至多向下滑动patlen  if i⩽ large//此时表示string上没有一个字符和pat末尾字符匹配 return falsei←i−largej←patlastpos.while j⩾ 0 and string[i]=pat[j]j←j−1i←i−1if j<0return i+1i←i+max(delta1(string[i]),delta2(j))\begin{array}{ll} i \gets patlastpos \\ \textbf{loop} \\ \qquad\textbf{if} \ i > stringlastpos \\ \qquad\qquad\textbf{return}\ false\\ \\ \qquad\textbf{while}\ i < stringlen \\ \qquad\qquad i \gets i+delta0(string(i)) \ \ \text{// 除非string[i]和pat末尾字符匹配,否则至多向下滑动patlen }\\\ \qquad\textbf{if}\ i \leqslant\ large \qquad\qquad\qquad\qquad \text{//此时表示string上没有一个字符和pat末尾字符匹配}\ \\ \qquad\qquad\textbf{return}\ false\\ \\ \qquad i \gets i-large \\ \qquad j \gets patlastpos. \\ \qquad\textbf{while}\ j \geqslant\ 0 \ and \ string[i]=pat[j]\\ \qquad \qquad j \gets j-1 \\ \qquad \qquad i \gets i-1 \\ \\ \qquad \textbf{if}\ j < 0 \\ \qquad \qquad \textbf{return}\ i+1 \\ \qquad i \gets i+max(delta_1(string[i]), delta_2(j)) \\ \\ \end{array}

Here, largelarge has multiple roles: it performs fast bad-character jumps similar to the Horspool algorithm introduced later, and it helps detect when the string search is complete.

Compared with the original algorithm, this improvement avoids the unnecessary computation of delta2delta_2 on every Observation 1 jump, giving a clear performance improvement for searching in usual alphabets.

Details of Building delta2

Introduction

In the Boyer and Moor paper1 on the Boyer–Moore algorithm in the October 1977 issue of Communications of the ACM, only the static delta2delta_2 table was described,

whereas the discussion of the concrete construction of delta2delta_2 appeared in the paper2 on the KMP algorithm jointly published by Knuth, Morris, and Pratt in the June 1977 issue of SIAM Journal on Computing.

Naive Algorithm

Before introducing Knuth's construction algorithm for delta2delta_2, the definition gives a naive algorithm suitable for small problems:

  1. For every position [0, patlen) in the interval i, determine the range of possible reoccurrence positions from the length of subpat, namely [-subpatlen, i].
  2. Compare possible reoccurrence positions character by character from right to left, looking for the rightmost reoccurrence of delta2delta_2 satisfying the requirements of subpatsubpat.
  3. Finally, remember to set delta2(lastpos)=0delta_2(lastpos)= 0.
Implementation
use std::cmp::PartialEq;

pub fn build_delta_2_table_naive(p: &[impl PartialEq]) -> Vec<usize> {
    let patlen = p.len();
    let lastpos = patlen - 1;
    let mut delta_2 = vec![];

    for i in 0..patlen {
        let subpatlen = (lastpos - i) as isize;

        if subpatlen == 0 {
            delta_2.push(0);
            break;
        }

        for j in (-subpatlen..(i + 1) as isize).rev() {
            // Match subpat
            if (j..j + subpatlen)
            .zip(i + 1..patlen)
            .all(|(rpr_index, subpat_index)| {
                if rpr_index < 0 {
                    return true;
                }

                if p[rpr_index as usize] == p[subpat_index] {
                    return true;
                }

                false
            })
            && (j <= 0 || p[(j - 1) as usize] != p[i])
            {
                delta_2.push((lastpos as isize - j) as usize);
                break;
            }
        }
    }

    delta_2
}

In particular, the following explanations of relevant Rust language features are necessary and will not be repeated:

  • usize and isize are unsigned and signed integers with the same number of bytes as a memory pointer. On a 32-bit machine they correspond to u32 and i32, and on a 64-bit machine to u64 and i64.
  • Indices into arrays, vectors, and slices use numbers of type usize (because they perform random memory access and indices cannot be negative). Therefore, handling negative values requires isize; indexing then requires usize, which is why the as keyword is used for an explicit conversion between the two.
  • impl PartialEq is used only as a generic parameter, so it supports both Unicode values encoded in char and binary u8 values.

Obviously, the time complexity of this brute-force algorithm is O(n3)O(n^3).

Efficient Algorithm

Next we introduce an efficient algorithm with time complexity O(n)O(n) and an additional space complexity of O(n)O(n).

Although Knuth proposed this construction in 1977, his original construction algorithm has a flaw: for some patpat, it cannot produce a delta2delta_2 satisfying the definition.

Rytter proposed a correction in a 1980 article3 in SIAM Journal on Computing. The following is the construction algorithm for delta2delta_2:

Since the definition of delta2delta_2 is complicated, first classify cases by the reoccurrence position of subpatsubpat and handle each class separately. This is the key idea for an efficient implementation.

Classify them from farthest to nearest, that is, from larger to smaller offsets:

  1. The entire reoccurrence of subpatsubpat is completely to the left of patpat, for example [(EYX)]ABYXCDEYX\mathtt{[(EYX)]ABYXCDEYX}. In this case, delta2(j)=patlastpos×2−jdelta_2(j) = patlastpos\times 2 - j.

  2. Part of the reoccurrence of subpatsubpat is to the left of patpat, while the rest is at the head of patpat, for example [(XX)ABC]XXXABC\mathtt{[(XX)ABC]XXXABC}. In this case, patlastpos<delta2(j)<patlastpos×2−jpatlastpos < delta_2(j) < patlastpos\times 2 - j.
    The boundary case where the entire subpatsubpat is at the head of patpat is also placed here (though it may be classified below in an implementation), for example [ABC]XXXABC\mathtt{[ABC]XXXABC}, where patlastpos=delta2(j)patlastpos = delta_2(j).

  3. The reoccurrence of subpatsubpat is entirely inside patpat, for example AB[YX]CDEYX\mathtt{AB[YX]CDEYX}. In this case, delta2(j)<patlastposdelta_2(j) < patlastpos.

Now let us discuss how to compute these three cases efficiently:

First Case

This is the simplest case; it only needs one traversal and can also initialize delta2delta_2 along the way.

Second Case

When does a reoccurrence of subpatsubpat have one part to the left of patpat and one part at the head of patpat? It occurs when a suffix of subpatsubpat equals a prefix of patpat,

as in the earlier example:

j:  012345678pat:  ABCXXXABC\begin{aligned} \textit{j}:\qquad\qquad\quad\ \ &\mathtt{0 1 2 3 4 5 6 7 8} \\ \textit{pat}:\qquad\qquad\ \ &\mathtt{A B C X X X A B C} \\ \end{aligned}

delta2(3)delta_2(3) has the reoccurrence [(XX)ABC]XXXABC\mathtt{[(XX)ABC]XXXABC}; the suffix of subpatsubpat XXABC\mathtt{XXABC} and the prefix of pat share the equal part ABC\mathtt{ABC}.

In fact, computing both the second and third cases relies on computing and applying the prefix function.

As long as jj is chosen so that subpatsubpat contains this equal suffix, we obtain the reoccurrence of subpatsubpat in the second case. In the example, we only need j⩽5j \leqslant 5,

and j=5j = 5 is the boundary case in which subpatsubpat is entirely at the head of patpat.

We can calculate delta2(j)delta_2(j) in this case:

Let the length of this equal prefix and suffix be prefixlen\textit{prefixlen}. Since subpatlen=patlastpos−jsubpatlen = patlastpos - j, the length of the part to the left of patpat is subpatlen−prefixlensubpatlen-\textit{prefixlen},

and rpr(j)=−(subpatlen−prefixlen)rpr(j) = -(subpatlen-\textit{prefixlen}), so delta2(j)=patlastpos−rpr(j)=patlastpos×2−j−prefixlendelta_2(j) = patlastpos - rpr(j) = patlastpos \times 2 - j - \textit{prefixlen}.

There may be multiple pairs of equal prefixes and suffixes afterward, for example:

j:  0123456789pat:  ABAABAABAA\begin{aligned} \textit{j}:\qquad\qquad\quad\ \ &\mathtt{0 1 2 3 4 5 6 7 8 9} \\ \textit{pat}:\qquad\qquad\ \ &\mathtt{A B A A B A A B A A} \\ \end{aligned}

For j≤2j\leq2, there is ABAABAA\mathtt{ABAABAA}; for 2<j≤52< j \leq 5, there is ABAA\mathtt{ABAA}; and for 5<j≤85<j\leq8, there is A\mathtt{A}.

The flaw in Knuth's algorithm is that it considers only the longest pair. In fact, we must consider every case in which a suffix of subpatsubpat equals a prefix of patpat, which is equivalent to computing all equal proper suffixes and proper prefixes of patpat, and partitioning the range of jj to calculate different delta2(j)delta_2(j) values by decreasing length.

Using the prefix function and the transition equation in reverse, j(n)=π[j(n−1)−1]j^{(n)} = \pi[j^{(n-1)}-1], obtain all lengths of equal proper prefixes and proper suffixes of patpat. Starting from π[patlastpos]\pi[patlastpos], the length of the longest pair, repeatedly run the transition equation backward to obtain the next shorter equal proper prefix and suffix.

This completes the computation of delta2delta_2 for the second case.

Third Case

The reoccurrence of subpatsubpat lies exactly inside patpat (not including the head of patpat); that is, search for pat[0…patlastpos−1]pat[0\dots patlastpos-1] in subpatsubpat from right to left.

If we solve this with the BM algorithm, we obtain a recursive implementation of the third BM case, with the base condition patlen⩽2patlen \leqslant 2.

Moreover, by the definition of delta2delta_2, the character immediately before the found reoccurrence of subpatsubpat (the one to its left) must differ from the character immediately before patpat as a suffix of subpatsubpat.

This suggests using a process similar to prefix-function computation, but with left and right reversed:

  • Two pointers point to the left endpoint of the substring and the “prefix” position of the longest common prefix and suffix of the substring. Move them from right to left, continuing when the two pointed-to characters are equal; this corresponds to extending the “prefix”.
  • When the characters differ, the part that was equal satisfies the reoccurrence requirement of delta2delta_2, and move the pointer to the “prefix” position backward until a new pair of equal characters is formed or the boundary is crossed.

As with the prefix function, an auxiliary array is needed for fallback. The space of the prefix array generated while computing the second case can be reused.

Implementation

Implementation Above
use std::cmp::PartialEq;
use std::cmp::min;

pub fn build_delta_2_table_improved_minghu6(p: &[impl PartialEq]) -> Vec<usize> {
    let patlen = p.len();
    let lastpos = patlen - 1;
    let mut delta_2 = Vec::with_capacity(patlen);

    // First case
    // delta_2[j] = lastpos * 2 - j
    for i in 0..patlen {
        delta_2.push(lastpos * 2 - i);
    }

    // Second case
    // lastpos <= delata2[j] = lastpos * 2 - j
    let pi = compute_pi(p);  // Compute the prefix function
    let mut i = lastpos;
    let mut last_i = lastpos; // Only for initialization
    while pi[i] > 0 {
        let start;
        let end;

        if i == lastpos {
            start = 0;
        } else {
            start = patlen - pi[last_i];
        }

        end = patlen - pi[i];

        for j in start..end {
            delta_2[j] = lastpos * 2 - j - pi[i];
        }

        last_i = i;
        i = pi[i] - 1;
    }

    // Third case
    // delata2[j] < lastpos
    let mut j = lastpos;
    let mut t = patlen;
    let mut f = pi;
    loop {
        f[j] = t;
        while t < patlen && p[j] != p[t] {
            // Use min to ensure that later fallbacks do not overwrite earlier data
            delta_2[t] = min(delta_2[t], lastpos - 1 - j);
            t = f[t];
        }

        t -= 1;
        if j == 0 {
            break;
        }
        j -= 1;
    }

    // Has no practical meaning; it is included for a complete definition
    delta_2[lastpos] = 0;

    delta_2
}

Improving the Worst Case for Multiple Matches with the Galil Rule

Multiple Matches in Suffix-Matching Algorithms

The search algorithm above only considers finding the first match of stringstring in patpat. Finding all matches of stringstring in patpat involves many different algorithmic ideas; the central question is how to use information from previously successful matches to reduce the worst-case time complexity to linear.

After an original successful match, simply moving the pointer in stringstring forward by patlenpatlen and restarting suffix matching can bring the worst-case time complexity back to O(mn)O(mn) (by convention, mm is patlenpatlen and nn is stringlenstringlen below).

For example, consider the extreme case patpat: AAA\mathtt{AAA} and stringstring: AAAAA…\mathtt{AAAAA}\dots.

One method proposed by Knuth records the patlenpatlen-length characters in a set of “finite” states. This algorithm guarantees that each character in stringstring is compared at most once, but the finite state set may be quite large: for a patpat whose characters are pairwise distinct, it needs 12m2+m\dfrac{1}{2}m^{2}+m states.

Next we introduce the Galil algorithm4, whose idea is simple and requires no additional preprocessing overhead.

Galil Rule

Suppose patpat is a prefix of a string formed by repeating some substring UU n times, UUUU…UUUU\dots. We call UU a period of patpat.

For example, pat:ABCABCABpat: \mathtt{ABCABCAB} is a prefix of the repetition of ABC\mathtt{ABC} represented by ABCABCABC\mathtt{ABCABCABC}, so the period length of ABC\mathtt{ABC} is 33 for patpat; equivalently, patpat satisfies pat[i]=pat[i+3]pat[i] = pat[i+3].

patpat has at least one period whose length equals its own length. Let the shortest period be kk, with k≤patlenk\leq patlen.

During the search, suppose patpat has successfully matched once. By the properties of the period, we only need to move stringstring forward by kk characters and compare whether these kk characters correspond, which directly determines whether another match of patpat exists.

To compute the length of this shortest period, suppose we know an equal prefix and suffix of patpat, both of length prefixlen\textit{prefixlen}. Then pat[i]=pat[i+(patlen−prefixlen)]pat[i] = pat[i+(patlen-\textit{prefixlen})], giving a period of length patlen−prefixlenpatlen-\textit{prefixlen}.

When we know the longest equal prefix and suffix of patpat, we obtain the shortest period of patpat.

The length of the longest equal prefix and suffix, π[patlastpos]\pi[patlastpos], is already available from the computation of delta2delta_2. Therefore, no additional preprocessing time or space is needed to improve the worst-case time complexity of the suffix-matching algorithm to linear.

Final BM Search Algorithm with the Above Optimizations
#[cfg(target_pointer_width = "64")]
const LARGE: usize = 10_000_000_000_000_000_000;

#[cfg(not(target_pointer_width = "64"))]
const LARGE: usize = 2_000_000_000;

pub struct BMPattern<'a> {
    pat_bytes: &'a [u8],
    delta_1: [usize; 256],
    delta_2: Vec<usize>,
    k: usize  // The shortest period length of pat
}

impl<'a> BMPattern<'a> {
    // ...

    pub fn find_all(&self, string: &str) -> Vec<usize> {
        let mut result = vec![];
        let string_bytes = string.as_bytes();
        let stringlen = string_bytes.len();
        let patlen = self.pat_bytes.len();
        let pat_last_pos = patlen - 1;
        let mut string_index = pat_last_pos;
        let mut pat_index;
        let l0 =  patlen - self.k;
        let mut l = 0;

        while string_index < stringlen {
            let old_string_index = string_index;

            while string_index < stringlen {
                string_index += self.delta0(string_bytes[string_index]);
            }
            if string_index < LARGE {
                break;
            }

            string_index -= LARGE;

            // If string_index moved, at least one failed match has occurred since the last successful match.
            // The offset for the Galil rule's second match must be reset to zero.
            if old_string_index < string_index {
                l = 0;
            }

            pat_index = pat_last_pos;

            while pat_index > l && string_bytes[string_index] == self.pat_bytes[pat_index] {
                string_index -= 1;
                pat_index -= 1;
            }

            if pat_index == l && string_bytes[string_index] == self.pat_bytes[pat_index] {
                result.push(string_index - l);

                string_index += pat_last_pos - l + self.k;
                l = l0;
            } else {
                l = 0;
                string_index += max(
                    self.delta_1[string_bytes[string_index] as usize],
                    self.delta_2[pat_index],
                );
            }
        }

        result
    }
}

Practical Performance Impact of the Worst Case

From a practical perspective, the theoretical worst case does not easily affect performance. Even on random text over a very small alphabet of only 4 characters, its impact is difficult to observe.

Consequently, without careful design, using the Galil rule can slightly hurt average performance. For certain extreme patpat and stringstring pairs, such as the example patpat: AAA\mathtt{AAA} and stringstring: AAAAA…\mathtt{AAAAA}\dots, however, applying the Galil rule does improve performance several-fold.

Improved Algorithms

Simplified Boyer–Moore Algorithm

The most complicated part of the BM algorithm is building the delta2delta_2 table (the good-suffix table). In practice, matching performance on ordinary alphabets mainly depends on the delta1delta_1 table (the bad-character table), so a simplified BM algorithm that uses only the delta1delta_1 table has emerged; its performance is usually only slightly different from the original.

Boyer–Moore–Horspool Algorithm

The Horspool algorithm also uses the bad-character rule, applying patpat to the character aligned with the end of delta1delta_1. It is similar to the improvement of the original matching algorithm and usually performs better than the original version.

Implementation
pub struct HorspoolPattern<'a> {
    pat_bytes: &'a [u8],
    bm_bc: [usize; 256],
}

impl<'a> HorspoolPattern<'a> {
    // ...
    pub fn find_all(&self, string: &str) -> Vec<usize> {
        let mut result = vec![];
        let string_bytes = string.as_bytes();
        let stringlen = string_bytes.len();
        let pat_last_pos = self.pat_bytes.len() - 1;
        let mut string_index = pat_last_pos;

        while string_index < stringlen {
            if &string_bytes[string_index-pat_last_pos..string_index+1] == self.pat_bytes {
                result.push(string_index-pat_last_pos);
            }

            string_index += self.bm_bc[string_bytes[string_index] as usize];
        }

        result
    }
}

Boyer–Moore–Sunday Algorithm

The Sunday algorithm also uses the bad-character rule, but compared with Horspool it goes one step further and directly considers the character after the one aligned with the end of patpat.

Implementing it only requires a slight modification to the delta1delta_1 table, equivalent to building it for a patlen+1patlen+1 of length patpat.

The Sunday algorithm is often used as one of the simplest practical algorithms with the best average performance in general cases. It is usually a little faster than Horspool and BM.

Implementation
pub struct SundayPattern<'a> {
    pat_bytes: &'a [u8],
    sunday_bc: [usize; 256],
}

impl<'a> SundayPattern<'a> {
    // ...
    fn build_sunday_bc(p: &'a [u8]) -> [usize; 256] {
        let mut sunday_bc_table = [p.len() + 1; 256];

        for i in 0..p.len() {
            sunday_bc_table[p[i] as usize] = p.len() - i;
        }

        sunday_bc_table
    }

    pub fn find_all(&self, string: &str) -> Vec<usize> {
        let mut result = vec![];
        let string_bytes = string.as_bytes();
        let pat_last_pos = self.pat_bytes.len() - 1;
        let stringlen = string_bytes.len();
        let mut string_index = pat_last_pos;

        while string_index < stringlen {
            if &string_bytes[string_index - pat_last_pos..string_index+1] == self.pat_bytes {
                result.push(string_index - pat_last_pos);
            }

            if string_index + 1 == stringlen {
                break;
            }

            string_index += self.sunday_bc[string_bytes[string_index + 1] as usize];
        }

        result
    }
}

BMHBNFS Algorithm

This algorithm combines Horspool and Sunday. It is the algorithm used by the stringlib function when CPython implements the find module5, and is abbreviated B5S below.

The basic idea of B5S is:

  1. Following the suffix-matching approach, first compare the character at position patlastpospatlastpos. If they are equal, compare the characters at positions 0…patlastpos−10\dots patlastpos-1; if these also match, a match has been found.

  2. If a mismatch occurs at any stage, enter the jumping phase.

  3. In the jumping phase, first check whether the character immediately after position patlastpospatlastpos occurs in patpat. If it does not, shift right by patlen+1patlen+1, making maximal use of the Sunday algorithm.

    If the character does occur in patpat, apply patlastpospatlastpos to the character at delta1delta_1 and perform a Horspool jump.

The implementation differs substantially depending on whether saving time or saving space is the primary goal.

Time-Saving Version

Implementation
pub struct B5STimePattern<'a> {
    pat_bytes: &'a [u8],
    alphabet: [bool;256],
    bm_bc: [usize;256],
    k: usize
}

impl<'a> B5STimePattern<'a> {
    pub fn new(pat: &'a str) -> Self {
        assert_ne!(pat.len(), 0);

        let pat_bytes = pat.as_bytes();
        let (alphabet, bm_bc, k) = B5STimePattern::build(pat_bytes);

        B5STimePattern { pat_bytes, alphabet, bm_bc, k }
    }

    fn build(p: &'a [u8]) -> ([bool;256], [usize;256], usize)  {
        let mut alphabet = [false;256];
        let mut bm_bc = [p.len(); 256];
        let lastpos = p.len() - 1;

        for i in 0..lastpos {
            alphabet[p[i] as usize] = true;
            bm_bc[p[i] as usize] = lastpos - i;
        }

        alphabet[p[lastpos] as usize] = true;

        (alphabet, bm_bc, compute_k(p))
    }

    pub fn find_all(&self, string: &str) -> Vec<usize> {
        let mut result = vec![];
        let string_bytes = string.as_bytes();
        let pat_last_pos = self.pat_bytes.len() - 1;
        let patlen = self.pat_bytes.len();
        let stringlen = string_bytes.len();
        let mut string_index = pat_last_pos;
        let mut offset = pat_last_pos;
        let offset0 = self.k - 1;

        while string_index < stringlen {
            if string_bytes[string_index] == self.pat_bytes[pat_last_pos] {
                if &string_bytes[string_index-offset..string_index] == &self.pat_bytes[pat_last_pos-offset..pat_last_pos] {
                    result.push(string_index-pat_last_pos);

                    offset = offset0;

                    // Galil rule
                    string_index += self.k;
                    continue;
                }
            }

            if string_index + 1 == stringlen {
                break;
            }

            offset = pat_last_pos;

            if !self.alphabet[string_bytes[string_index+1] as usize] {
                string_index += patlen + 1;  // sunday
            } else {
                string_index += self.bm_bc[string_bytes[string_index] as usize];  // horspool
            }
        }

        result
    }
}

This version of B5S performs extremely well; among the suffix-matching algorithms introduced so far, it is usually the fastest in ordinary cases.

Space-Saving Version

Also implemented in CPython's stringlib, this version uses two integers to approximately replace the character table and the role of delta1delta_1, greatly reducing space:

  1. Replace the character table (alphabet) with a simple Bloom filter.

    ???+ note "Implementation" ```rust pub struct BytesBloomFilter { mask: u64, }

    impl BytesBloomFilter {
        pub fn new() -> Self {
            SimpleBloomFilter {
                mask: 0,
            }
        }
        
        fn insert(&mut self, byte: &u8) {
            (self.mask) |= 1u64 << (byte & 63);
        }
        
        fn contains(&self, char: &u8) -> bool {
            (self.mask & (1u64 << (byte & 63))) != 0
        }
    }
    ```
    

    A Bloom filter is a data structure similar to a Set that greatly saves storage by sacrificing accuracy (and, in practice, some running time). It may incorrectly report that an item not in the set is present (False Positives, abbreviated FP), but it never reports that an item in the set is absent (False Negatives, abbreviated FN). Thus, using it may fail to obtain the largest possible character jump because of an FP, but it will not skip a character that should be matched because of an FN.

    Theoretical analysis shows that when patpat has length 50 bytes, the FP probability of the implementation above is approximately 0.5, and when patpat has length 10 bytes, it is approximately 0.15.

    Although this is not a standard Bloom filter, it does not actually use a true hash function; it is simply a character mapping that maps each byte from 0-255 to the number formed by its first six bits.

    However, this simplification is important for character search in memory. Even using the fastest known non-cryptographic hash algorithm, xxHash, the computation still takes an order of magnitude longer.

    In addition, when pat is shorter than 30 bytes, more than one hash function is needed to achieve the best FP probability. This is not very meaningful, however, because an array containing two u128 integers can already build a character table for the entire character set.

  2. Use delta1(pat[patlastpos])delta_1(pat[patlastpos]) instead of the whole delta1delta_1.

    Looking at delta1delta_1, its most frequent use is when the first character checked during suffix matching mismatches, which is the most common mismatch. Therefore, set skip = delta1(pat[patlastpos]).

    On a first-stage mismatch, shift directly by skip characters. On a second-stage mismatch, however, lacking the information from the whole delta1delta_1 table means that we can shift only one character.

    ???+ note "Implementation" ```rust pub struct B5SSpacePattern<'a> { pat_bytes: &'a [u8], alphabet: BytesBloomFilter, skip: usize, }

    impl<'a> B5SSpacePattern<'a> {
        pub fn new(pat: &'a str) -> Self {
            assert_ne!(pat.len(), 0);
            
            let pat_bytes = pat.as_bytes();
            let (alphabet, skip) = B5SSpacePattern::build(pat_bytes);
            
            B5SSpacePattern { pat_bytes, alphabet, skip}
        }
        
        fn build(p: &'a [u8]) -> (BytesBloomFilter, usize)  {
            let mut alphabet = BytesBloomFilter::new();
            let lastpos = p.len() - 1;
            let mut skip = p.len();
            
            for i in 0..p.len()-1 {
                alphabet.insert(&p[i]);
                
                if p[i] == p[lastpos] {
                    skip = lastpos - i;
                }
            }
            
            alphabet.insert(&p[lastpos]);
            
            (alphabet, skip)
        }
        
        pub fn find_all(&self, string: &'a str) -> Vec<usize> {
            let mut result = vec![];
            let string_bytes = string.as_bytes();
            let pat_last_pos = self.pat_bytes.len() - 1;
            let patlen = self.pat_bytes.len();
            let stringlen = string_bytes.len();
            let mut string_index = pat_last_pos;
            
            while string_index < stringlen {
                if string_bytes[string_index] == self.pat_bytes[pat_last_pos] {
                    if &string_bytes[string_index-pat_last_pos..string_index] == &self.pat_bytes[..patlen-1] {
                        result.push(string_index-pat_last_pos);
                    }
                    
                    if string_index + 1 == stringlen {
                        break;
                    }
                    
                    if !self.alphabet.contains(&string_bytes[string_index+1]) {
                        string_index += patlen + 1;  // sunday
                    } else {
                        string_index += self.skip;  // horspool
                    }
                } else {
                    if string_index + 1 == stringlen {
                        break;
                    }
                    
                    if !self.alphabet.contains(&string_bytes[string_index+1]) {
                        string_index += patlen + 1;  // sunday
                    } else {
                        string_index += 1;
                    }
                }
            
            }
            
            result
        }
    }
    ```
    

    Compared with the suffix-matching algorithms above, this version is not as fast, but the gap is small. Its performance is still better than KMP, thanks to the excellent space complexity of at most two u64 integers.

Theoretical Analysis

The following shows the performance of the algorithms for a general alphabet. The vertical axis is similar to execution cost (cost is the cost when a match of m characters is followed by a mismatch, and skip is the probability of shifting k characters after a mismatch); lower is better. The horizontal axis is the length of the pattern string pat.

String-search algorithm performance comparison
String-search algorithm performance comparison

Performance on a small alphabet (a DNA base-pair sequence {A, C, T, G}):

String-search algorithm performance comparison for a small alphabet
String-search algorithm performance comparison for a small alphabet

In summary, on a large alphabet, such as in everyday search, the strong performance of the Boyer–Moore family mainly relies on the delta1delta_1 table to perform character jumps;

on the other hand, on a small alphabet, the role of delta1delta_1 decreases while the role of delta2delta_2 becomes apparent.

If some extra space is available, the full Boyer–Moore algorithm with space complexity O(m)O(m) is more general and has the best overall performance.

References and Notes

Footnotes

  1. The 1977 Boyer–Moore algorithm paper ↩

  2. The 1977 KMP algorithm paper ↩

  3. Rytter's 1980 correction to Knuth's paper ↩

  4. The 1979 paper introducing the Galil algorithm ↩

  5. An introduction to the B5S algorithm ↩

This content comes from OI Wiki: The Boyer–Moore Algorithm. See the original for authors and revision history. AIOJ has adapted the formatting, in-site links, and resource paths.

Content is shared under CC BY-SA 4.0 and SATA license; additional terms are governed by the original notice. Thanks to the OI Wiki authors and contributors; per the SATA terms, please visit the OI Wiki repository and star it to show your support.