3.8.5 · D3String Algorithms

Worked examples — Boyer-Moore — bad character, good suffix heuristics

3,101 words14 min readBack to topic

This is the hands-on companion to the parent note. There we built the two rules; here we drive them through every situation a real text can throw at us. This page is self-contained: every symbol it uses is (re)built below before it is used.

Recall The three numbers you always need
  • ::: the pattern index where the mismatch happened (we compare right to left, so starts at ).
  • ::: the bad character — the text character sitting under the mismatch (recall is the alignment offset defined just above).
  • ::: the rightmost index inside where character appears, or if never appears.

Bad-character shift: . Good-suffix shift: (the array is built just below). Total shift: .


Two building blocks we need first

Before any example we must define the two ideas the good-suffix rule leans on: a border, and the good-suffix shift array . We build them from scratch here so nothing on this page is borrowed.


The scenario matrix

Every mismatch or full match during a search falls into exactly one of the algorithmic cases A–H below. (Cases I and J are not new cases — they are contexts in which the same A–H logic is exercised; we tag examples with them only to show the rules under realistic pressure.)

Case Situation What decides the shift Danger if you get it wrong
A Bad char not in () slide whole pattern past: none — this is the easy big jump
B Bad char left of () forgetting it exists → too slow
C Bad char right of () formula goes clamp to 1 infinite loop / backward shift
D Mismatch at the very first compare () no matched suffix yet, so is small; bad char rules over-trusting good suffix
E Good suffix reoccurs internally (Case 1) align the earlier copy whose preceding char differs picking a copy with same preceding char → repeat mismatch
F Only a border of = suffix of the good suffix (Case 2) align that border, shift forgetting Case 2 → over-shift, miss a match
G Full match found ( falls below ) shift by shifting by wastes work; shifting by may skip overlaps
H Degenerate pattern () worst case ; Galil rule fixes thinking BM is always fast

Context tags used below: I = real-world large-alphabet search (DNA), where Case A fires often → sublinear; J = exam-style "which of the two rules wins the ?".


Example 1 — Case C then Case A (context I: real-world DNA)

Step 1 — line up and read right-to-left (alignment (i)). ; text char is . Counting from 0: G C T T C T G C. Match, move to . Why this step? BM compares right-to-left; a match peels off a matched suffix.

Step 2 — reach the first mismatch (alignment (i)). ✓; ✓; vs mismatch. So , . Why this step? The first disagreement is the decision point.

Step 3 — this is Case C (bad char to the right). in CCTTTTGC = index 7. Since , the naive is illegal (backward). Clamp: . Why this step? Aligning a rightmost copy that lies to the right of the mismatch would drag backward.

Step 4 — a clean Case A (alignment (ii)). Here at . Compare right-to-left: ✓; ✓; ✓; vs mismatch at , . Character A is absent from CCTTTTGC, so and — the Case A jump, skipping characters we never read. Why this step? Case A and Case C are genuinely different; giving each its own concrete text keeps the shift fully reproducible.

Verify: Case C → . Case A → . Both positive and legal. ✓


Example 2 — Case B (bad char to the left)

Step 1 — locate the bad character. . Why? The text char under the mismatch drives the bad-character rule.

Step 2 — compute . In A B C A B, the only C is at index 2. So . Why? We want the rightmost copy of inside to slide under the mismatch position.

Step 3 — apply the formula. Case B. . Why? A leftward occurrence gives a positive, legal shift — here just 1, small but valid.

Verify: . Aligning index-2 C under text position moves right by exactly . ✓


Example 3 — Case C (bad char to the right, clamp)

Step 1 — . In ABCAB, A appears at 0 and 3; rightmost is 3. Why? Standard bad-character lookup.

Step 2 — compare with . Case C. Why? The rightmost A is right of the mismatch, so the naive formula gives .

Step 3 — clamp. . Why? A shift of moves leftward, re-reading the same region forever. The guard prevents this. The good-suffix rule usually supplies a bigger legal jump here.

Verify: . Without the clamp we'd get → non-terminating loop. ✓


Example 4 — Case E (good suffix reoccurs, Case 1)

Figure — Boyer-Moore — bad character, good suffix heuristics

How to read the figure: the top row is ANPANMAN with each character in its own box and its index below. The two red-outlined boxes at indices 6–7 are the matched good suffix AN. The two red boxes drawn below, at indices 3–4, are the earlier internal copy of AN; the black arrow points to its preceding character . The long red arrow shows the pattern sliding right by so the earlier AN lands under the already-matched text.

Step 1 — identify the good suffix . (length ). Why? Everything to the right of the mismatch already matched the text; we want to preserve that.

Step 2 — find internal reoccurrences of . As the figure shows, AN sits at indices 0–1 and 3–4. We want one whose preceding character differs from the mismatched . Why? If the preceding char also equalled M, the very next compare would mismatch identically — no progress.

Step 3 — pick the copy at indices 3–4. Its preceding char is ✓. To bring AN from indices 3–4 to where indices 6–7 sit, shift by . Why? We slide right so the earlier valid AN lands under the already-matched text.

Step 4 — sanity vs shift-by-1. Shifting by 1 would put over a text position known to hold A → guaranteed mismatch. So beats 1.

Verify: Case 1 shift (end index 7) (earlier copy's end index 4) , so ; matched-suffix length . ✓


Example 5 — Case F (Case 2, border = suffix of the good suffix)

Step 1 — good suffix. , length , mismatch at . Why? Matched-suffix length .

Step 2 — Case 1 has nothing usable, so go to Case 2. Why? There is no earlier full copy of B whose preceding character differs in a way that avoids an immediate repeat mismatch; we salvage a partial overlap instead.

Step 3 — find the longest border of . Front reading of ABAB is A,B,…; back reading is B,A,…. The length-2 chunk AB is both the first two and last two characters → border of length 2 (recall the border definition above). Why? The border is the largest overlap between 's start and its own tail, the only alignment that could still match without skipping an occurrence.

Step 4 — shift = . Why? Slide so the length-2 border prefix lands where the length-2 border suffix was.

Verify: longest border of ABAB is 2, so Case-2 shift . A smaller shift only re-examines impossible alignments; a larger one skips the border overlap → could miss a match. ✓


Example 6 — Case D (mismatch on the very first compare)

Step 1 — matched suffix length. . Empty suffix. Why? We mismatched before matching anything, so the good-suffix lookup is , the entry for an empty matched suffix.

Step 2 — compute cleanly. The good-suffix rule can only ask us to preserve a matched suffix; when that suffix is empty there is nothing to preserve, so the rule imposes no constraint beyond the algorithm's minimum legal move of . Concretely : an empty good suffix trivially overlaps at every alignment, so the largest border-of-the-good-suffix that also prefixes has length (the whole pattern minus one step), giving . There is no contradiction — the good-suffix rule simply contributes the neutral value here. Why? An empty matched suffix carries zero information about where may next align, so it cannot legally demand a jump larger than .

Step 3 — bad character. in NEEDLE Case A inside Case D. . Why? K is absent, so we slide the whole pattern past it.

Step 4 — take the max. . Why? Both rules are valid lower bounds; the larger wins. Here the bad-character rule already hits the maximum possible jump , and the neutral good-suffix value cannot beat it.

Verify: ; ; final . ✓


Example 7 — Case G (full match) + context J (which rule wins)

Step 1 — after a full match, use . With every character matched, falls to ; there is no bad character to look up, so the shift comes entirely from the good-suffix rule for the whole pattern, which is stored in . Why? No mismatch to trigger the bad-character rule; tells us the next alignment that could still contain without skipping an overlapping occurrence.

Step 2 — longest border of ABCAB. Front reading A,B,C,…; back reading B,A,C,…. Test lengths from large to small: length 4 ABCA vs BCAB ✗; length 3 ABC vs CAB ✗; length 2 AB vs AB ✓. So the longest proper border has length 2. Why? The next alignment that can match must reuse the longest self-overlap of — that is exactly its longest border.

Step 3 — compute . . After reporting the match we advance by 3. Why? Shifting by 1 or 2 would place AB… where it cannot re-match; shifting by 4 or 5 could skip an overlapping occurrence beginning inside the just-matched region.

Step 4 — context J twist ( of the two rules). During a mismatch elsewhere in the same search we always take . Two illustrative pairs:

  • if and , the algorithm uses (good suffix wins);
  • if and , it uses (bad char wins). Why? Each rule is an independently safe lower bound on a legal jump, so the larger is also safe — always compute both and take the max.

Verify: border of ABCAB is 2, . And , . ✓


Example 8 — Case H (degenerate pattern, worst case)

Step 1 — compare each alignment. At every offset , comparing right-to-left, all of equal the text A. We do comparisons, find a full match, and (Case G) shift by . Why? Nothing ever mismatches, so no bad-character skip is ever triggered.

Step 2 — for AAA. Front reading A,A,…; back reading A,A,…. Longest proper border: length 2 (AA is both first two and last two characters). So — we shift by only 1 each time. Why? AAA overlaps itself at every offset, so the only safe post-match move is a single step; a larger jump would skip an overlapping AAA.

Step 3 — count the work. Number of alignments ; each does comparisons and shifts by 1. Total comparisons . Why? Bad char is useless (every char is A) and good suffix shifts by 1, so BM degrades to the naive bound on this input.

Step 4 — the fix. The Galil Rule remembers the already-matched region so it never re-compares it, restoring guaranteed even here. Related always-linear matchers: Knuth-Morris-Pratt (KMP), Z-Algorithm (both built on Borders and Prefix Function), and hashing-based Rabin-Karp. For many patterns at once, Aho-Corasick.

Verify: alignments ; comparisons ; border of AAA so . ✓


Recall

Recall Quick self-test

What is the alignment offset ? ::: The text position under which currently sits; lines up with . What is a border of a string? ::: A substring that is both a prefix and a suffix, shorter than the whole string (a proper border). Which case has ? ::: Case A — bad char absent, shift . Which case forces a clamp to 1? ::: Case C — . After a full match you shift by? ::: . Case 2 of good suffix fires when? ::: Only a border of equals a suffix of the good suffix; shift . Why take of the two rule shifts? ::: Each is an independently safe lower bound, so the larger is also safe and faster.