字符串匹配问题是算法领域的经典问题,C/C++中常用的strstr函数就是这个问题的定义:

const char* strstr( const char* str, const char* target ); char* strstr( char* str, const char* target );

Finds the first occurrence of the byte string target in the byte string pointed to by str. The terminating null characters are not compared.

在目标字符串str中寻找是否存在子串target,字符串str的长度为\(n\),target的长度为\(m\)。这个问题最为人所熟知的算法应该是KMP(Knuth-Morris-Pratt)算法,其时间复杂度为\(O(n)\),想法非常酷。

但是,Can we do better?

如我们所知的,KMP算法是一个前缀匹配算法,因为从前向后匹配比较自然也比较保守,所以很难突破\(O(n)\)的天花板,但后缀匹配算法没有这样的限制,比如BM(Boyer-Moore)算法就实现了\(O(n/m)\)的最好情况时间复杂度,最差情况时间复杂度为\(O(n)\),虽然与KMP算法相比BM算法的最差情况具有更大的常数,但是在实际使用中(自然语言)BM算法往往具有优于KMP的性能表现,并且模式串越长性能表现越好。我不想浪费时间搬运现有的算法资料,也不认为我可以讲的更好,所以在本文中会直接给出相应算法的链接,比如BM算法

在BM算法横空出世以后,人们惊讶的发现原来这个问题还可以这么玩!于是非常多的算法研究人员对BM算法进行了研究并提出了各种各样的改进方案,Horspool在1980年提出的Boyer-Moore-Horspool算法就是一种基于BM的改进(或者应该说是简化更合适):Horspool只使用了BM算法中的坏字符规则(基于窗口的最右字符),并且在实验中验证了其实际使用中的性能甚至要优于BM算法。需要注意的是这并不能代表BM算法中的好字符规则是毫无意义的,好字符规则保证了BM算法具有\(O(n)\)的最差情况时间复杂度,而Horspool的最差情况时间复杂度为\(O(nm)\),只是在实际使用中很难出现而已。

下图是对一些字符串匹配算法进行测试的结果:

算法性能对比
算法性能对比

可以看到,抛弃了好字符规则的Horspool具有很好的运行效果。需要注意的是,这些实验是在自然语言(ASCII)中测试得到的,在小字符集的字符串(比如DNA序列)可能会得到不同的结果。

Sunday在1990年提出了另一个基于Boyer-Moore算法的改进(简化)的算法,通常称为Sunday算法,同样只使用坏字符规则,和Horspool算法的思路一脉相承,不同点在于:Horspool算法使用窗口的最右字符,而Sunday更激进,使用窗口后的第一个字符来计算偏移量,获得了更快的窗口移动速度,代码如下:

#include <string.h>

const int CSIZE = 256; // size of whole character set

/**
 * preprocessing step for sunday algorithm
 * calculate bad-character shift table
 */
static void prepare(const char *target, int m, int bad_table[])
{
    for (int i = 0; i < CSIZE; ++i) {
        bad_table[i] = m + 1;
    }

    for (int i = 0; i < m; ++i) {
        bad_table[target[i]] = m - i;
    }
}

/**
 * strstr function implemented using sunday algorithm
 */
const char* strstr_sunday(const char* str, const char* target)
{
    if (NULL == str || NULL == target) {
        return NULL;
    }

    int n, m;
    n = strlen(str);
    m = strlen(target);

    if (!m) return str;
    if (!n) return NULL;

    /* preprocessing */
    int bad_table[CSIZE];
    prepare(target, m, bad_table);

    /* searching */
    int i = 0;
    while (i <= n - m) {
        if (memcmp(target, str + i, m) == 0) {
            return str + i;
        }

        i += bad_table[str[i + m]];
    }

    return NULL;
}

参考资料

Categories:

Updated:

Comments