字符串匹配算法是一种应用很广泛的算法,在入侵检测、关键词匹配等技术中都需要使用到。我专门研究了几个字符串匹配算法,如果想了解更多更全的算法可以去看《柔性字符串匹配》这本书。
下面依次来说说朴素算法,Rabin-Karp算法,Knuth-Morris-Pratt算法,Boyer-Moore算法,sunday算法,以及Aho-Corasick等算法。
朴素算法
朴素算法是一种非常简单的算法,时间复杂度是O(nm)(n为文本串长度,m为模式串长度,下同),不需要预处理。
int naive_matcher(const char * txt, const char * pat,
int offset[])
{
int i, j, n, m, c = 0;
n = strlen(txt);
m = strlen(pat);
for (i = 0; i <= n - m; i++) {
for (j = 0; j < m; j++)
if (pat[j] != txt[i + j])
break;
if (j == m)
offset1 = i;
}
return c;
}
从代码中可以看出,它暴力地依次比较,算法简单但时间复杂度太高,不适合在数据量大或注重效率的情况下使用。
Rabin-Karp算法
朴素算法之所以复杂度达到O(nm),是因为它比较的过程没有优化,没有利用已经比较过的信息。在Rabin-Karp算法中,使用模余的办法来减少比较的次数。先看代码。int rk_matcher(const char * txt, const char * pat,
int offset[])
{
unsigned int i, j, p, t, h, n, m, c = 0;
const unsigned int d = 256, q = 16777213;
n = strlen(txt);
m = strlen(pat);
p = (unsigned char)pat[0];
t = (unsigned char)txt[0];
for (i = 1, h = 1; i < m; i++) {
p = (d * p + (unsigned char)pat[i]) % q;
t = (d * t + (unsigned char)txt[i]) % q;
h = (h * d) % q;
}
for (i = 0; i <= n - m; i++) {
if (p == t) {
for (j = 0; j < m; j++)
if (txt[i + j] != pat[j])
break;
if (j == m)
offset1 = i;
}
t = (t + q - (unsigned char)txt[i] * h % q) % q;
t = (d * t % q + (unsigned char)txt[i + m]) % q;
}
return c;
}
RK算法的关键在于,比较字符串时先比较模余值,如果一致再进行一字符一字符地比较,这样避免了多数的无用比较。对于文本串“235902314526731”和模式串“31452”,取模13作为HASH函数,对于23590首先计算模13的值为8,不等于31452模13的值5,故不用再做比较。第一次计算23590模13的值花费的时间为O(m),而第二次计算35902的值只需要花费O(1)的时间,35902 mod 13 = (((23590 mod 13) – (2 × 10000 mod 13)) × 10 + 2) mod 13。RK算法正是通过模余这个运算减少了比较的次数,使得算法在一般情况下能达到O(n+m)的时间复杂度,最坏情况下时间复杂度依然是O(nm)。
Knuth-Morris-Pratt算法
KMP算法是一个非常经典的算法,它完美地解决了重复比较的问题,算法的时间复杂度下降到O(n),预处理时间为O(m)。int kmp_matcher(const char * txt, const char * pat,
int offset[])
{
int i, j, n, m, c = 0;
n = strlen(txt);
m = strlen(pat);
int * next = new int[m];
for (i = 1, j = 0, next[0] = 0; i < m; i++){
j = next[i - 1];
while (j > 0 && pat[j] != pat[i])
j = next[j - 1];
if (pat[j] == pat[i])
j++;
next[i] = j;
}
for(i = 0, j = 0; i < n; i++) {
while (j > 0 && txt[i] != pat[j])
j = next[j - 1];
if(txt[i] == pat[j])
j++;
if (j == m)
offset1 = i - j + 1;
}
delete [] next;
return c;
}
next数组是KMP算法中最重要的部分,它包含了匹配不成功后该移动的偏移信息。举个例子,对于模式串“abcfeabcd”,如果当前匹配到了模式串的第8个字符“c”时,文本串中下一个字符与模式串中下一个字符“d”不同,说明这次匹配失败,但是不需要从模式串开始进行匹配,而可以从第四个字符“f”开始匹配。
Boyer-Moore算法
BM算法很类似KMP算法,它与KMP的不同体现在模式串比较顺序和遇到不匹配字符右移的距离所计算的方法不同,在一般情况下,它的效率要比KMP算法高几倍,但是在文本串和模式串具有周期性的情况下却会退化到O(n^2),例如,当文本为ababababa,模式串为ababa时。此外,它需要的更多的存储空间。对原始的BM算法进行优化,可以使它在最坏情况下的时间复杂度也为O(n)。int bm_matcher(const char * txt, const char * pat,
int offset[])
{
int i, j, n, m, a, b, c = 0;
n = strlen(txt);
m = strlen(pat);
int * bc = new int[256];
for(i = 0; i < 256; i++)
bc[i] = m;
for (i = 0; i <= m - 1; i++)
bc[(unsigned char)pat[i]] = m - i - 1;
int * sfx = new int[m];
sfx[m - 1] = m;
for (i = m - 2; i >= 0; i--) {
j = i;
while(pat[j] == pat[m - i - 1 + j] && --j >= 0);
sfx[i] = i - j;
}
int * gc = new int[m];
for (i = 0; i < m; i++)
gc[i] = m;
for (i = m - 1; i >= 0; i--)
if (sfx[i] == i + 1)
for (j = 0; j < m - i - 1; ++j)
if (gc[j] == m)
gc[j] = m - i - 1;
for (i = 0; i <= m - 2; ++i)
gc[m - 1 - sfx[i]] = m - i - 1;
for (i = 0; i <= n - m; i += a > b ? a : b) {
j = m - 1;
while(j >= 0 && pat[j] == txt[j + i]) j--;
if (j < 0) {
offset1 = i;
a = b = 1;
} else {
a = gc[j];
b = bc[(unsigned char)txt[j + i]] - m + j + 1;
}
}
delete [] sfx; delete [] gc; delete [] bc;
return c;
}
Sunday算法
Sunday算法也是类似于KMP、BM的一种算法,它在一般情况下的效率要比KMP和BM好,但是在极端情况下同BM算法一样会退化到朴素算法的级别,这跟该算法的具体实现有关。对原始Sunday算法进行优化,也可以使它在最坏情况下的时间复杂度降为O(n)。
int sunday_matcher(const char * txt, const char * pat,
int offset[])
{
int i, j, n, m, c = 0;
n = strlen(txt);
m = strlen(pat);
int * next = new int[256];
for (i = 0; i < 256; i++)
next[i] = m + 1;
for (i = 0; i < m; i++)
next[(unsigned char)pat[i]] = m - i;
for (i = 0, j = 0; i <= n - m;) {
for (j = 0; j < m; j++)
if (txt[i + j] != pat[j])
break;
if (j == m)
offset1 = i;
i += next[(unsigned char)txt[i + m]];
}
return c;
}
Aho-Corasick算法
Aho-Corasick算法是基于有限自动机的多模式串匹配算法,这种算法是基于Trie的,它可以解决在一段文本中查找指定的多个模式出现的位置,该算法了构造一个有限自动机,从而对文本串只需要进行一次遍历即可。AC算法的时间复杂度与构造有限自动机的时间和遍历文本串的时间相关。
typedef struct TrieNode{
char c;
int father;
int son[26];
int next;
int flag;
int count;
}TrieNode;
int trie_cnt = 0;
TrieNode trie[1000] = {0, {0}, 0, 0};
inline unsigned int ac_char2index(char c)
{
return c - 'a';
}
void ac_add_string(const char * sub)
{
int i, j, k, l;
for (i = 0, j = 0; sub[j] != 0; j++)
{
k = ac_char2index(sub[j]);
if (trie[i].son[k] > 0)
{
i = trie[i].son[k];
}
else
{
l = ++trie_cnt;
trie[i].son[k] = l;
trie[l].father = i;
trie[l].c = sub[j];
i = l;
}
}
trie[i].flag = 1;
}
void ac_build_automation()
{
int i, j, k, l;
for (i = 1; i <= trie_cnt; i++)
{
j = trie[i].father;
k = trie[j].next;
l = ac_char2index(trie[i].c);
if (j > 0 && trie[k].son[l] > 0)
trie[i].next = trie[k].son[l];
}
}
int ac_matcher(const char * txt, int offset[])
{
int i, j, k, l, n, c;
n = strlen(txt);
for (i = 0, j = 0, c = 0; i < n; i++) {
l = ac_char2index(txt[i]);
while (!trie[j].son[l] && j)
j = trie[j].next;
j = trie[j].son[l];
for (k = j; k; k = trie[k].next)
if (trie[k].flag)
offset1 = i;
}
return c;