KMP算法分析
一、先来看看传统的暴力解法
1.1 BF算法设计思想
1、主串和模式串逐个字符进行比较
2、当出现**「字符串不相同」时,也就是「失配」**时,主串的比较位置重置为起始位置的下一个字符位置,模式串的比较位置重置为起始位置
1.2 BF算法的设计缺陷及解决方案
在BF算法中,每次失配都需要回溯指向上次比较起始字符的下一个字符。通过观察发现:在回溯的时候,已匹配似乎**「有一部分」没必要继续比较了,这样可以降低算法的「时间复杂度」**
2.1 KMP算法设计思路
在匹配过程中出现字符比较不相等时,**「主串 S」已比较的位置不回溯,「模式串 T」**比较的位置进行移动
在匹配过程中有一个难题需要解决:如何计算**「模式串 T」失配时的移动位数?经过三位牛人的研究,设计出了「部分匹配函数」**
2.2 部分匹配函数
部分匹配函数是KMP算法中最难以理解的部分。首先需要理解**「前缀」、「后缀」**的概念。
· 前缀:指除了最后一个字符以外,一个字符串的全部头部组合
· 后缀:指除了第一个字符以外,一个字符串的全部尾部组合
下图分别是字符 c 的前缀和字符 a 的后缀
如何求出一个字符串的**[最大共有长度]**,下面以 **ababc **这个字符串举例
求出 c 这个字符的前缀与后缀
前缀 | 后缀 |
---|---|
a | b |
ab | ab |
aba | bab |
那么由此可知 ababc这个字符串最大的 共有长度 就是 2 --ab
因此就可以知道 ababc 这个字符串所有字符的最大共有长度,可以得出一个next数组 [-1,0,0,1,2]
说明 数组的前两位是固定的 因为 a 这个字符无前缀与后缀,字符 b 虽然有,但是前缀与后缀不能达到最大长度1,这个前缀与后缀不能达到最大长度适应于所有字符。这就是为什么上面的表格没有将 前缀 abab与后缀abab列出来的原因。
那么来看一下 KMP算法的匹配过程
当发现a 与c不相等的时候,T串该如何移动,由上可知,T字符串的 next数组为[-1,0,1,2,3],发现字符c的最大共有长度为3,而此时 c 的后缀为 aaa, 前缀也是aaa,那是不是可以将T向前移动一个字符就可以了,就可以得到下图
红线框出的部分恰好就是失配时已匹配部分,“aaaa” 的最大共有元素为 “aaa”,这一部分字符就是不需要再重复进行比较直接跳过的字符
这里next数组代表当S串与T串匹配到的字符不相等时,T串该移动到的位置
- 当下标值是 -1时,代表 S串向前移动,T串在开头位置
- 当下标值>=0 表示 T串该跳到的位置,S串保持不动
说明next数组会因为语言下标的不同(比如lua和c),会有不一样的设定,后续看代码设定。
//求出 字符串 s2的 next
static int* getNextArray(string s2){
int *next = NULL;
int len = s2.length();
if(len == 1){
next = (int*)malloc(sizeof(int)*1);
next[0] = -1;
return next;
}
next = (int*)malloc(sizeof(int) * len);
next[0] = -1;
next[1] = 0;
int i = 2; // 目前在哪个位置上求next数组的值
int cn = 0;// 当前是哪个位置的值再和i-1位置的字符比较
while(i < len){
if(s2[i -1] == s2[cn]){// 配成功的时候
next[i++] = ++cn;
}else if(cn > 0){
cn = next[cn];
}else{
next[i++] = 0;
}
}
return next;
}
//获取s2串在s1串中第一次出现的位置 没找到返回 -1
static int getIndexOf(string s1,string s2){
if(s2.length() < 1 || s1.length() < s2.length()){
return -1;
}
int x = 0;
int y = 0;
int* next = getNextArray(s2);
while(x < s1.length() && y <s2.length()){
if(s1[x] == s2[y]){
x++;
y++;
}else if(next[y] == -1){ //y == 0
x++;
}else{
y = next[y];
}
}
free(next);
return y == s2.length() ? x - y : -1;
}
2.3 lua string.find() 寻找子串实现原理
//摘抄子 lua-5.1.4 lstrlib.c 文件
static int lmemfind (const char *s1, size_t l1,
const char *s2, size_t l2) {
const char *start = s1;
if (l2 == 0) return -1; /* empty strings are everywhere */
else if (l2 > l1) return -1; /* avoids a negative `l1' */
else {
const char *init; /* to search for a `*s2' inside `s1' */
l2--; /* 1st char will be checked by `memchr' */
l1 = l1-l2; /* `s2' cannot be found after that */
while (l1 > 0 && (init = (const char *)memchr(s1, *s2, l1)) != NULL) { //寻找到第一个相等子串的问题
init++; /* 1st char is already checked */
if (memcmp(init, s2+1, l2) == 0) //然后比较后续字符是否相等
return (init-1) - start;
else { /* correct `l1' and `s1' to try again */
l1 -= init-s1;
s1 = init;
}
}
return -1; /* not found */
}
}
2.4 比较 kmp 与 lua string.find()的效率
//g++ kmp.cpp -o kmp
#include<iostream>
#include<stdio.h>
#include<stdlib.h>
#include<string>
#include<string.h>
#include<cstring>
#include <sys/time.h>
#include<vector>
using namespace std;
static vector<string> allStr;
static vector<string> allMatch;
static int* getNextArray(string s2);
long long getCurrentTime()
{
struct timeval tv;
gettimeofday(&tv,NULL);
return tv.tv_sec * 1000 + tv.tv_usec / 1000;
}
static int getIndexOf(string s1,string s2){
if(s2.length() < 1 || s1.length() < s2.length()){
return -1;
}
int x = 0;
int y = 0;
int* next = getNextArray(s2);
while(x < s1.length() && y <s2.length()){
if(s1[x] == s2[y]){
x++;
y++;
}else if(next[y] == -1){ //y == 0
x++;
}else{
y = next[y];
}
}
free(next);
return y == s2.length() ? x - y : -1;
}
//求出 字符串 s2的 next
static int* getNextArray(string s2){
int *next = NULL;
int len = s2.length();
if(len == 1){
next = (int*)malloc(sizeof(int)*1);
next[0] = -1;
return next;
}
next = (int*)malloc(sizeof(int) * len);
next[0] = -1;
next[1] = 0;
int i = 2; // 目前在哪个位置上求next数组的值
int cn = 0;// 当前是哪个位置的值再和i-1位置的字符比较
while(i < len){
if(s2[i -1] == s2[cn]){// 配成功的时候
next[i++] = ++cn;
}else if(cn > 0){
cn = next[cn];
}else{
next[i++] = 0;
}
}
return next;
}
static string getRandomString(int possibilities, int size) {
int ranLen = (rand() % size) + 1;
// char* ans = (char*)malloc(ranLen);
string res(ranLen,'\0');
for(int i = 0;i < ranLen;i++){
res[i] = (char)(int)(rand() % possibilities + 'a');
}
res[ranLen] = '\0';
return res;
}
static int lmemfind (const char *s1, size_t l1,
const char *s2, size_t l2) {
const char *start = s1;
if (l2 == 0) return -1; /* empty strings are everywhere */
else if (l2 > l1) return -1; /* avoids a negative `l1' */
else {
const char *init; /* to search for a `*s2' inside `s1' */
l2--; /* 1st char will be checked by `memchr' */
l1 = l1-l2; /* `s2' cannot be found after that */
while (l1 > 0 && (init = (const char *)memchr(s1, *s2, l1)) != NULL) {
init++; /* 1st char is already checked */
if (memcmp(init, s2+1, l2) == 0)
return (init-1) - start;
else { /* correct `l1' and `s1' to try again */
l1 -= init-s1;
s1 = init;
}
}
return -1; /* not found */
}
}
static void testTime(){
vector<int> memFindVec;
vector<int> kmpFindVec;
long long startTime = getCurrentTime();
for(int i = 0;i < allStr.size();i++){
string& str = allStr[i];
string& match = allMatch[i];
memFindVec.push_back(lmemfind(str.c_str(),str.length(),match.c_str(),match.length()));
}
cout << "memFindTime:"<< getCurrentTime() - startTime<<endl;
startTime = getCurrentTime();
for(int i = 0;i < allStr.size();i++){
string& str = allStr[i];
string& match = allMatch[i];
kmpFindVec.push_back(getIndexOf(str, match));
}
cout << "kmpFindTime:"<< getCurrentTime() - startTime<<endl;
for(int j = 0;j < memFindVec.size();j++){
if(memFindVec[j] != kmpFindVec[j]){
cout << allStr[j] << " " << allMatch[j]<<endl;
cout << "error:"<< " j: " << j << " " << memFindVec[j] << " " << kmpFindVec[j]<<endl;
break;
}
}
cout << "test success"<<endl;
}
static void randomString(){
int possibilities = 5;
int strSize = 20;
int matchSize = 5;
int testTimes = 5000000;
for(int i = 0; i < testTimes;i++){
allStr.push_back(getRandomString(possibilities,strSize));
allMatch.push_back(getRandomString(possibilities,matchSize));
}
}
int main(){
srand((unsigned)time(NULL));
randomString();
testTime();
// {
// string str = "bacebaccdcbedacca";
// string match = "cd";
// cout << lmemfind(str.c_str(),str.length(),match.c_str(),match.length())<<endl;
// }
return 0;
}
2.5 结论
由上述案例代码可知, 基于特定的样本, string.find() 的效率还是远远高于kmp的,只能说 c的库函数还是厉害,kmp虽然号称O(N),但是会省去很多常数项的东西,具体怎么达到O(N),可以看看算法导论 32.4章节。