Beating Textbook Algorithms in String Search
By: Linas Medžiūnas
Tags: Search, Algorithms, Performance, Scala, Java

1. Knuth-Morris-Pratt (KMP)

This is a well known string search algorithm that dates back to 1970. Since it is well described in the literature, I will not try to explain its details here. KMP is based on Finite State Automaton — during precomputing phase, an array of jump indexes is built based on the needle . During the search, this automaton accepts characters of the haystack one after another, and updates it’s internal state (which is just an index within this jump table).

A link to implementation in Scala.

2. Shifting Bit Mask

Well, I had to come up with some name for it because I don’t remember seeing this algorithm anywhere in the literature. If you have seen a similar algorithm described somewhere — kindly please share a link in the comments below.

Update: I have learned that this algorithm (and its variations) was known since 1964, under various names, such as Bitap, Shift-or, Shift-and, Baeza-Yates–Gonnet. I thank my readers for pointing this out.

This algorithm is based on a very simple idea, and performs great because it is effectively jump-free and is based on just a few primitive binary operations. Because of that, it has a limitation on the length of the needle we are searching for — it cannot be more than 64 bytes long (based on the number of bits of a Long on JVM). This limit is generous enough for many use cases.

Because this is a home made algorithm, I will try to explain it in detail. The first part is precomputing the search context for the given needle :

Precomputing bit masks

We precompute a bitMask (64 bit Long) for every possible byte value (256 bitMasks in total). For some byte value X , its bitbask contains ones in every position where X is present in the needle , for example:

Bit masks for the needle “abracadabra”

Also, we need to precompute a successBitMask which will help us know when we have a full match. It is simply a Long value with bit 1 in the position needle.length — 1 :

Precomputing success bit mask

And the last part is performing the actual search. The only mutable state that we need to track is currentMask (a Long ). For every byte of haystack , we shift currentMask to the left by 1 bit, set its lowest bit to 1, and perform a bitwise and operation of the resulting value with the bitMask precomputed for the current byte value of the haystack being processed (this and resets all the bits in positions of currentMask that do not match the currently processed byte value).

This way, after each byte processed, only the bits that are in matching positions “survive”. And, with each byte processed, all the bits are shifted to the left by one position. If a bit “survives” the process for the number of iterations that is equal to the length of the needle , we have a match (we use successBitMask to check for this):

Shifting Bit Mask search implementation

Note: the method above returns false in case of a match, which might seem somewhat counterintuitive. This is needed because returning true has the meaning of continuing the search process, and false stops it indicating a match (as I mentioned before, this API was designed to plug into Netty framework). See this code for a better understanding of how the search process is being executed.

So, the whole logics boils down to just a few simple CPU instructions. And, sadly, a completely redundant bounds check on bitMasks array access that Java JIT compiler is unable to eliminate (yes, I have checked the assembly generated by multiple JDKs).

A link to the full implementation in Scala.

3. Aho-Corasic

Another well known algorithm, dating back to 1975. Its distinct (and in some cases very useful) feature is the possibility to search for multiple needles simultaneously, while still processing every character of the haystack only once (which I think is brilliant!). The idea behind it is an extension to Knuth Morris Pratt — a Finite State Automaton supported by a trie (built based on the set of needles ) of jump references (versus one-dimensional array of jump indexes of KMP). Based on those references, the internal state of the automaton jumps around the nodes of the trie upon every character processed, and some of the nodes indicate a match on some specific needle . It’s precomputing phase is quite complicated, but the actual search phase is surprisingly simple.

A link to implementation in Scala.