字符串搜索的 Horspool 算法
当你要手写一个字符串搜索算法,你会写什么?
本文将要介绍的 Horspool 算法简单又不慢,一看就懂,一学就会,一写就过!
假设我们要在一个叫 haystack 的字符串中搜索另一个叫 needle 的字符串。先对齐,从后往前搜到某个不匹配的字符:
abcbabababab cbabab
我们可以把 needle 右移一位然后重新从最后一个开始匹配,这是最朴素的算法。但是我们可不可以多移几位呢?我们看 haystack 中的第三个 b,一个策略是移动 needle 使得这个 b 和 needle 里倒数第二个 b 对齐来,这样 needle 移了两个位置。
abcbabababab cbabab
needle 里第一个 b 不行,因为会错过解;最后一个 b 也不行,因为 needle 没有动。所以除了 needle 里最后一个位置之外,倒数第一个配对的字符才是正确的选择。
所以我们知道如何预处理一下 needle,记录 needle 每个字符的倒数第一次出现的位置(末尾除外)离 needle 末尾的距离,来作为跳转步数。不在 needle 里出现的字符对应的跳转步数则为 |needle|。哎呀这个岂不很好写的:
int jmp_table[UCHAR_MAX+1];
for(int i = 0; i <= UCHAR_MAX; i++)
jmp_table[i] = nlen;
int last = nlen - 1;
for(int i = 0; i < last; i++)
jmp_table[needle[i]] = last - i;
好了现在我们知道,每次出现不匹配时,找到 haystack 里和 needle 对应的最后一个字符,再在 needle 里找它倒数第一次出现的位置(末尾除外),然后把它们对齐。就是这么简单。
完整的 C++ 代码:
int horspool(char *haystack, char *needle) {
int hlen = strlen(haystack), nlen = strlen(needle);
int jmp_table[UCHAR_MAX+1];
for(int i = 0; i <= UCHAR_MAX; i++)
jmp_table[i] = nlen;
int last = nlen - 1;
for(int i = 0; i < last; i++)
jmp_table[needle[i]] = last - i;
int j = 0;
while(j <= hlen - nlen){
char c = haystack[j + last];
int i = last;
while(i >= 0 && haystack[j + i] == needle[i]) --i;
if(i == -1) return j;
j += jmp_table[c];
}
return -1;
}
简单到 Haskell 的也很好写哇:
{-# LANGUAGE OverloadedStrings #-}
import qualified Data.Map as M (findWithDefault, fromList)
import qualified Data.ByteString as BS
horspool :: BS.ByteString -> BS.ByteString -> Maybe Int
horspool s p = f (lenp - 1) (lenp - 1) (lenp - 1)
where lenp = BS.length p
lens = BS.length s
jmptbl = M.fromList $ zip (BS.unpack p) [lenp - 1, lenp - 2 .. 1]
jmp x = M.findWithDefault lenp x jmptbl
f i j k
| i >= lens = Nothing
| j == -1 = Just (i + 1)
| s `BS.index` i == p `BS.index` j = f (i-1) (j-1) k
| otherwise = let k' = k + jmp (s `BS.index` k)
in f k' (lenp - 1) k'
main :: IO ()
main = print $ horspool "abcbabababab" "cbabab"