4种字符串匹配算法:KMP(下)
回顾:4种字符串匹配算法:BS朴素 Rabin-karp(上)
1、图解
KMP算法是一种改进的字符串匹配算法,由D.E.Knuth,J.H.Morris和V.R.Pratt同时发现,因此人们称它为克努特——莫里斯——普拉特操作(简称KMP算法)。KMP算法的关键是利用匹配失败后的信息,尽量减少模式串与主串的匹配次数以达到快速匹配的目的。具体实现就是实现一个next()函数,函数本身包含了模式串的局部匹配信息。我不喜欢叫他“看毛片”算法。但我不得不说,能联想到这个的人,确实很有才。
原理如果文字理解起来非常复杂,而且有点难懂。因此,画图来讲解是最好的方式啦,下图非常容易理解算法的执行原理。
我之前也是看了这幅图理解的。所以我觉得把这个图用来讲解最好不过(抱歉,我搬了这图,但是这个图是我至今觉得讲的最好的图,不得不搬),当然我已经全部重新画过。网络上KMP的讲解,讲的好的寥寥无几,发现一些博客,都是转载,或者讲的不够清楚,很难理解。我觉得有必要重新整理整理,自己来梳理一下知识点,为了让自己更理解深刻一些。
首先模式串逐一对比文本串,如上图,直到遇到相同的元素,如下图:
模式串,逐一对比,直到发现蓝色框框内的字符不相同,下图。这时候怎么办?
BS算法,就是把模式串向前移动一位,从头继续比较,所以他的时间复杂度最差才是o(m*n)。而KMP呢,不再从头比较啦,这样大大减少了时间复杂度。我们即将引出next数组概念。
既然,不保存,那他是怎么跳的呢?
我们发现,ABCDAB,AB**AB, 这个字符串首尾相同,因此直接跳4格,如下图。
也就是说,next数组保存的数和跳几格是有关系的呗。那我们怎么来看呢?这个字符串的匹配值有关。我们只要数,字符串首尾有几个是匹配的即可,通过这样来初始化。我们来看一下这个表格。
A = 0 AB = 0 ABC = 0 ABCD = 0 ABCDA = 1 ABCDAB = 2 ABCDABD = 0
公式:
移动位数 = 已匹配的字符数 - 表格内的匹配值
我们继续看,即使跳转了4格,还是有蓝色的部分不匹配,又因为AB = 0 所以移动位数 = 已匹配的字符数(2) - 表格内的匹配值(0) = 2,依次类推,直到匹配到下图,则成功。
该算法,最重要的是next数组上。理解这个,我们觉得其他就迎刃而解了。
2、代码实现
主要代码(c++版):
1 std::map<int,int> compute_prefix(const std::string &pattern) 2 { 3 int i = 1; 4 int p = 0; 5 std::map<int, int> pi; 6 int length = pattern.length(); 7 pi.insert(std::make_pair(1, 0)); 8 while (i < length) 9 { 10 if (p > 0 && pattern[i] != pattern[p]) 11 { 12 p = 0; 13 } 14 if (pattern[i] == pattern[p]) 15 { 16 ++p; 17 } 18 pi.insert(std::make_pair(i + 1, p)); 19 i++; 20 } 21 return pi; 22 } 23 24 bool kmp_match(const std::string &text,const std::string &pattern) 25 { 26 std::map<int, int> pos; 27 pos = compute_prefix(pattern); 28 int q = 0; 29 for (int i = 0; i < text.length(); i++) 30 { 31 if (q > 0 && text[i] != pattern[q]) 32 { 33 q = pos.at(q); 34 } 35 if (text[i] == pattern[q]) 36 { 37 q++; 38 } 39 if (q == pattern.length()) 40 { 41 return true; 42 } 43 } 44 }
测试代码:
int main() { char a[] = "bbc abcdab abcdabcdabde"; char b[] = "abcdabd"; bool iftrue = kmp_match(a, b);; if (iftrue == true) { std::cout << "找到了" << std::endl; } else { std::cout << "没有" << std::endl; } }
注:你也可以返回文本串的地址下标,稍加改动即可。
1 int kmp_match(const std::string &text,const std::string &pattern) 2 { 3 std::map<int, int> pos; 4 pos = compute_prefix(pattern); 5 int q = 0; 6 for (int i = 0; i < text.length(); i++) 7 { 8 if (q > 0 && text[i] != pattern[q]) 9 { 10 q = pos.at(q); 11 } 12 if (text[i] == pattern[q]) 13 { 14 q++; 15 } 16 if (q == pattern.length()) 17 { 18 return (i+1)-q+1; 19 } 20 } 21 return -1; 22 }
另外,从代码中可以看出,他的时间复杂度为o(n),预处理时间o(m)
资料:
特别感谢:阮一峰的网络日志
1 #include <iostream> 2 #include <map> 3 #include <string> 4 #include <utility> 5 #include <stdlib.h> 6 7 std::map<int,int> compute_prefix(const std::string &pattern) 8 { 9 int i = 1; 10 int p = 0; 11 std::map<int, int> pi; 12 int length = pattern.length(); 13 pi.insert(std::make_pair(1, 0)); 14 while (i < length) 15 { 16 if (p > 0 && pattern[i] != pattern[p]) 17 { 18 p = 0; 19 } 20 if (pattern[i] == pattern[p]) 21 { 22 ++p; 23 } 24 pi.insert(std::make_pair(i + 1, p)); 25 i++; 26 } 27 return pi; 28 } 29 30 int kmp_match(const std::string &text,const std::string &pattern) 31 { 32 std::map<int, int> pos; 33 pos = compute_prefix(pattern); 34 int q = 0; 35 for (int i = 0; i < text.length(); i++) 36 { 37 if (q > 0 && text[i] != pattern[q]) 38 { 39 q = pos.at(q); 40 } 41 if (text[i] == pattern[q]) 42 { 43 q++; 44 } 45 if (q == pattern.length()) 46 { 47 return (i+1)-q+1; 48 } 49 } 50 return -1; 51 } 52 53 int main() 54 { 55 char a[] = "bbc abcdab abcdabcdabde"; 56 char b[] = "abcdabd"; 57 58 int iftrue = kmp_match(a, b);; 59 if (iftrue >=0) 60 { 61 std::cout << "找到了" << " "<< iftrue <<std::endl; 62 } 63 else 64 { 65 std::cout << "没有" << std::endl; 66 } 67 system("pause"); 68 }