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 is placed at the left end of the text string , with their first characters aligned.
We make the following definitions and will not repeat them later:
The length of is . In particular, for a zero-indexed string, is defined as the position of the last character of ;
the length of is , and .
Suppose we know the -th character of (aligned with the last character of ). What information can we obtain?
Observation 1
If we know that the character does not occur in , we do not need to consider occurrences of starting at the st, nd, ..., or -th character of ; we can move forward by characters at once.
Observation 2
More generally, if the rightmost occurrence of in (that is, the position closest to the end) is characters away from the end,
then we can move backward by characters without matching. If the shift distance were less than , the character alone could not be matched, so the pattern string could not match either.
Therefore, unless can match the last character of , must skip characters (equivalently, moves backward by characters). We can define a function to calculate :
Note that, obviously, this table only needs to be computed through position .
Now suppose that matches the last character of . We then check whether the character before matches the second-to-last character of :
If it does, continue moving backward until the entire pattern has been matched (we have then found a successful match of in );
alternatively, after matching the -th character from the end of , we may encounter a mismatch at the -th character from the end. We then want to move 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 characters of , a mismatch occurs at the -th character from the end. To align the mismatching character in with the corresponding character in ,
we need to shift by characters, which means that we should focus on the characters afterward (the character in aligned with the end of after shifting by k).
Since ,
our attention should move characters forward along .
However, we may be able to skip more characters. Continue reading.
Observation 3(b)
Suppose we know that the next characters of match the last characters of ; call this substring .
We also know that the substring after the mismatching character in matches . If occurs before the corresponding mismatching character in , we can shift by some distance
so that the occurring before the character corresponding to the mismatching in (a plausible reoccurrence, abbreviated PR below) aligns with the in . If occurs multiple times in , take the first one in the right-to-left suffix-matching order (the rightmost plausible reoccurrence, abbreviated RPR below).
Suppose is shifted by characters (that is, the distance between the terminal of and its rightmost plausible reoccurrence). Then our attention should move characters forward along . We call this distance :
Let be the position of the rightmost plausible reoccurrence of when the mismatch occurs at , with (this is only a simple definition; the algorithm-design section below gives a more precise discussion). Then clearly .
Therefore:
Thus, on a mismatch, we can skip characters of .
Process
The arrow points to the mismatching character :
does not occur in . By Observation 1, move forward by characters, namely 7 characters:
By Observation 2, we need to move forward by 4 characters so that the hyphen aligns:
Now char: matches. Move the pointer on one step to the left and continue matching:
By Observation 3(a), mismatches. Since does not occur in , move by characters, and move the pointer on forward by characters:
Now again matches the last character of . The pointer on moves left and matches ; continuing left, it encounters a mismatch at :
Intuitively, by Observation 3(b), we now shift by characters so that the suffix aligns. This shift gives the maximum movement of the pointer. Here , so the pointer on moves forward by 7 characters.
From the formal perspective, , which formally supports the jump from Observation 3(b):
We now find that every character in equals the corresponding character in ; we have found a match of in . We used only 14 references to : 7 comparisons were necessary to complete a successful match (), and the other 7 let us skip 22 characters.
Algorithm Design
Initial Matching Algorithm
Explanation
Consider the following algorithm for string matching using and :
If the algorithm above returns , does not occur in ; if it returns a number, that number is the position of the first occurrence of from the left in .
Let us now describe more precisely how to calculate the function on which relies.
By the definition above, is the position associated with a mismatch at : it is the rightmost plausible reoccurrence position of the substring at .
In other words, we need to find the best such that , while also considering two special cases:
- When , this is equivalent to adding a virtual prefix before , and it is consistent with the principle of the jump.
- When , if , then cannot be a plausible reoccurrence of .
The reason is that is itself the mismatching character, so after shifting by characters, suffix matching would still fail at .
There are also two restrictions:
- . When , we have , so the character that mismatched at would also mismatch at .
- Since , define .
Process
Because understanding is central to implementing the Boyer–Moore algorithm, we use the following two examples for a detailed explanation:
For , is . The rightmost plausible reoccurrence before can only be , so its position is -5, that is, .
For , is . The rightmost plausible reoccurrence before is , so .
For , is . The rightmost plausible reoccurrence before is , so .
For , is . The rightmost plausible reoccurrence before is , so .
For , is . The rightmost plausible reoccurrence before is , so .
For , is . The rightmost plausible reoccurrence before is , so .
For , is . Since , that is, equals the mismatching character , is not a plausible reoccurrence of . Thus the rightmost plausible reoccurrence is , so .
For , is . Similarly, since , is not a plausible reoccurrence of ; the rightmost plausible reoccurrence is , so .
For , by the definition of , , so .
Now consider another example:
For , is . The rightmost plausible reoccurrence before can only be , so its position is -8, that is, .
For , is . The only rightmost plausible reoccurrence before is , so .
For , is . The only rightmost plausible reoccurrence before is , so .
For , is . The only rightmost plausible reoccurrence before is , so .
For , is . The only rightmost plausible reoccurrence before is , so .
For , is . The only rightmost plausible reoccurrence before is , so .
For , is . Since and , the rightmost plausible reoccurrence before is , so .
For , is . Although , , so the rightmost plausible reoccurrence before is , so ;
For , by the definition of , , so .
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: does not match , so we jump the entire and begin the next match.
This can be specially optimized:
Define :
Replacing with gives the improved matching algorithm:
Here, 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 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 table was described,
whereas the discussion of the concrete construction of 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 , the definition gives a naive algorithm suitable for small problems:
- For every position
[0, patlen)in the intervali, determine the range of possible reoccurrence positions from the length ofsubpat, namely[-subpatlen, i]. - Compare possible reoccurrence positions character by character from right to left, looking for the rightmost reoccurrence of satisfying the requirements of .
- Finally, remember to set .
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:
usizeandisizeare unsigned and signed integers with the same number of bytes as a memory pointer. On a 32-bit machine they correspond tou32andi32, and on a 64-bit machine tou64andi64.- 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 requiresisize; indexing then requiresusize, which is why theaskeyword is used for an explicit conversion between the two. impl PartialEqis used only as a generic parameter, so it supports bothUnicodevalues encoded incharand binaryu8values.
Obviously, the time complexity of this brute-force algorithm is .
Efficient Algorithm
Next we introduce an efficient algorithm with time complexity and an additional space complexity of .
Although Knuth proposed this construction in 1977, his original construction algorithm has a flaw: for some , it cannot produce a satisfying the definition.
Rytter proposed a correction in a 1980 article3 in SIAM Journal on Computing. The following is the construction algorithm for :
Since the definition of is complicated, first classify cases by the reoccurrence position of 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:
-
The entire reoccurrence of is completely to the left of , for example . In this case, .
-
Part of the reoccurrence of is to the left of , while the rest is at the head of , for example . In this case, .
The boundary case where the entire is at the head of is also placed here (though it may be classified below in an implementation), for example , where . -
The reoccurrence of is entirely inside , for example . In this case, .
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 along the way.
Second Case
When does a reoccurrence of have one part to the left of and one part at the head of ? It occurs when a suffix of equals a prefix of ,
as in the earlier example:
has the reoccurrence ; the suffix of and the prefix of pat share the equal part .
In fact, computing both the second and third cases relies on computing and applying the prefix function.
As long as is chosen so that contains this equal suffix, we obtain the reoccurrence of in the second case. In the example, we only need ,
and is the boundary case in which is entirely at the head of .
We can calculate in this case:
Let the length of this equal prefix and suffix be . Since , the length of the part to the left of is ,
and , so .
There may be multiple pairs of equal prefixes and suffixes afterward, for example:
For , there is ; for , there is ; and for , there is .
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 equals a prefix of , which is equivalent to computing all equal proper suffixes and proper prefixes of , and partitioning the range of to calculate different values by decreasing length.
Using the prefix function and the transition equation in reverse, , obtain all lengths of equal proper prefixes and proper suffixes of . Starting from , 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 for the second case.
Third Case
The reoccurrence of lies exactly inside (not including the head of ); that is, search for in 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 .
Moreover, by the definition of , the character immediately before the found reoccurrence of (the one to its left) must differ from the character immediately before as a suffix of .
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 , 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 in . Finding all matches of in 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 forward by and restarting suffix matching can bring the worst-case time complexity back to (by convention, is and is below).
For example, consider the extreme case : and : .
One method proposed by Knuth records the -length characters in a set of “finite” states. This algorithm guarantees that each character in is compared at most once, but the finite state set may be quite large: for a whose characters are pairwise distinct, it needs states.
Next we introduce the Galil algorithm4, whose idea is simple and requires no additional preprocessing overhead.
Galil Rule
Suppose is a prefix of a string formed by repeating some substring n times, . We call a period of .
For example, is a prefix of the repetition of represented by , so the period length of is for ; equivalently, satisfies .
has at least one period whose length equals its own length. Let the shortest period be , with .
During the search, suppose has successfully matched once. By the properties of the period, we only need to move forward by characters and compare whether these characters correspond, which directly determines whether another match of exists.
To compute the length of this shortest period, suppose we know an equal prefix and suffix of , both of length . Then , giving a period of length .
When we know the longest equal prefix and suffix of , we obtain the shortest period of .
The length of the longest equal prefix and suffix, , is already available from the computation of . 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 and pairs, such as the example : and : , 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 table (the good-suffix table). In practice, matching performance on ordinary alphabets mainly depends on the table (the bad-character table), so a simplified BM algorithm that uses only the 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 to the character aligned with the end of . 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 .
Implementing it only requires a slight modification to the table, equivalent to building it for a of length .
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:
-
Following the suffix-matching approach, first compare the character at position . If they are equal, compare the characters at positions ; if these also match, a match has been found.
-
If a mismatch occurs at any stage, enter the jumping phase.
-
In the jumping phase, first check whether the character immediately after position occurs in . If it does not, shift right by , making maximal use of the Sunday algorithm.
If the character does occur in , apply to the character at 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 , greatly reducing space:
-
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
Setthat 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 has length 50 bytes, the FP probability of the implementation above is approximately 0.5, and when 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
u128integers can already build a character table for the entire character set. -
Use instead of the whole .
Looking at , 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
skipcharacters. On a second-stage mismatch, however, lacking the information from the whole 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
u64integers.
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.
Performance on a small alphabet (a DNA base-pair sequence {A, C, T, G}):
In summary, on a large alphabet, such as in everyday search, the strong performance of the Boyer–Moore family mainly relies on the table to perform character jumps;
on the other hand, on a small alphabet, the role of decreases while the role of becomes apparent.
If some extra space is available, the full Boyer–Moore algorithm with space complexity is more general and has the best overall performance.