算法描述

使用文本编辑的时候,常常会用到搜索功能,比如在一个文本"abcabcabcabcabcd"里查找"abcabcd"第一次出现的位置,这个功能如何用代码来解决呢?

最直观的办法就是暴力比较,从文本第一个字符开始逐个和要匹配的字符一一比较,遇到不相等的字符,则倒退从第二个字符重复上面的步骤:

 一共比较了六次。通过观察可以发现,有些比较是可以省掉的:

 真正的有效比较一共只需要四次。

如何能高效的搜索,最好是遍历文本的次数只有一次。也就是假如有两个指针分别指向文本和匹配字符串,文本的指针最好是逐个前移,前面用的暴力匹配法就是当不匹配时把文本指针回退了,导致性能变低。所以需要考虑如何让被匹配字符串的指针能正确回退。比如下面这个过程:

 

 

当字母D与文本不匹配时,如何让被匹配字符串指针回退到C的位置重新开始比较。kmp算法就是解决这个问题的。

首先,要了解两个概念:"前缀"和"后缀"。 "前缀"指除了最后一个字符以外,一个字符串的全部头部组合;"后缀"指除了第一个字符以外,一个字符串的全部尾部组合。

"公共前后缀"就是"前缀"和"后缀"的最长的共有元素的长度,可以用一个数组来记录这个长度。kmp算法中这个数组叫部分匹配表。

假设部分匹配表用int *next表示,文本指针用idx1表示,被匹配的字符串指针用idx2表示,当在某一个位置出现不匹配时,就可以通过idx2=next[idx2-1]来重定位被匹配的字符串指针。而idx1一直是前进不用回退的。

 

代码实现

 1 // s1是文本 s2是要匹配的内容
 2 void KMPSearch(string s1, string s2)
 3 {
 4     int len1 = s1.length();
 5     int len2 = s2.length();
 6 
 7     // 计算公共前后缀
 8     int *next = new int[len2];
 9     next[0] = 0;
10     for(int prefix = 0, suffix = 1; suffix < len2; ++suffix)
11     {
12         while(prefix > 0 && s2[prefix] != s2[suffix])
13         {
14             prefix = next[prefix-1];
15         }
16         if(s2[prefix] == s2[suffix])
17         {
18             ++prefix;
19         }
20         next[suffix] = prefix;
21     }
22 
23     // 开始匹配
24     for(int idx1 = 0, idx2 = 0; idx1 < len1; ++idx1)
25     {
26         while(idx2 > 0 && s1[idx1] != s2[idx2])
27         {
28             idx2 = next[idx2 - 1];
29         }
30         if(s1[idx1] == s2[idx2])
31         {
32             ++idx2;
33         }
34         if(idx2 == len2)
35         {
36             std::cout<<"find result pos:"<<idx1 - len2 + 1<<std::endl;
37             return;
38         }
39     }
40     std::cout<<"find no result "<<std::endl;
41     return;
42 }

 

技术参考

1. C/C++Linux服务器开发/后台架构师

2. 字符串匹配的KMP算法