KMP算法
2016-06-19 17:44
351 查看
KMP算法是字符串比较经典的算法,《大话数据结构》感觉有点难看懂,从编程中下标从零开始的习惯,稍微改变了相关的说明,方便大家理解。
1. 前缀:除了最后一个字符以外,一个字符串的全部头部组合
2. 后缀:除了第一个字符以外,一个字符串的全部尾部组合。
3. “部分匹配值”就是”前缀”和”后缀”的最长的共有元素的长度
目标串:”ABCFABD”,原串”ABCFABCDABCFABD”
比较到如图中位置时,已经标绿的两个字符序列相等。
黄色部分的两个字符不相等。若采用传统的搜索方法,只将目标串后移一位,效率很低。如下图所示
实际上对于已经匹配的绿色子串“ABCFAB”,如果我们找出它的部分匹配串(即“AB”)则只需将目标偏移到与原串中第二个“AB”序列对齐,并从第三个字符开始比较即可。
亦即,移动位数 = 已匹配的字符数 - 对应的部分匹配值。
接着比较
因为D与F不相等,目标串需要往后移。这时,已匹配序列为(”ABC”),对应的”部分匹配值”为0。所以,移动位数 为3 - 0=3。
如此一直往下
字符串的索引从零开始
用next数组来表示部分匹配表,数组下标从0开始。next[i]代表结束字符索引为i字串的最大部分匹配长度
串的长度为len,开始索引为begin,结束索引为end。则有
end - begin + 1 = len;
找出字串Sub1(下标为0~next[m - 1] - 1)的最长部分匹配子串(记为Sub3,假设长度为k,k = next[next[m - 1] - 1])。
因为T[m]前边存在Sub1串,而Sub3又是Sub1的部分匹配子串。因此T[m]前边肯定存在Sub3串。只需要比较T[k]和T[m]即可
如果T[k]和T[m]仍旧不等,一直往下。直到找不到部分匹配字串。
- 绿色代表已经匹配的序列
- 蓝色代表待计算的next值,黄色代表求改next值时尾部待比较的字符
- T[4] == T[1],所以 next[4] = 1
- T[5] == T[1],所以next[5] = 2
- T[6] == T[2],所以next[6] = 3
1. T[7] != T[3],因此查找T[7]前的已经遍历过的子串“ABCFABC”的部分匹配字串,查next表知next[6]为3
2. 回溯查看长度为3的子串(“ABC”)的部分匹配序列,也就是查看next[2]的值(为0)
3. 结束,next[7]为0.
1. T[14] != T[6],因此查找T[14]前的已经遍历过的子串“ABCFABCDABCFAB”的部分匹配字串,查next表知next[13]为6.
2. 回溯查看长度为6的子串(“ABCFAB”)的部分匹配序列长度,也就是next[5]的值(为2)
3. 比较T[2] 与T[14] (此时开头部分和结尾部分前面都有子串“AB”)
4. T[2] !=T[14],继续找出字串“AB”的部分匹配长度,为零。终止迭代,因此next[14]为0。
部分匹配表
(Partial Match Table)1. 前缀:除了最后一个字符以外,一个字符串的全部头部组合
2. 后缀:除了第一个字符以外,一个字符串的全部尾部组合。
3. “部分匹配值”就是”前缀”和”后缀”的最长的共有元素的长度
算法搜索的思路
说明:绿色代表已经表过并且相等的部分。黄色代表当前待表的位置目标串:”ABCFABD”,原串”ABCFABCDABCFABD”
比较到如图中位置时,已经标绿的两个字符序列相等。
黄色部分的两个字符不相等。若采用传统的搜索方法,只将目标串后移一位,效率很低。如下图所示
实际上对于已经匹配的绿色子串“ABCFAB”,如果我们找出它的部分匹配串(即“AB”)则只需将目标偏移到与原串中第二个“AB”序列对齐,并从第三个字符开始比较即可。
亦即,移动位数 = 已匹配的字符数 - 对应的部分匹配值。
接着比较
因为D与F不相等,目标串需要往后移。这时,已匹配序列为(”ABC”),对应的”部分匹配值”为0。所以,移动位数 为3 - 0=3。
如此一直往下
部分匹配表的计算
阐述前提
目标字符表示待搜索的字符,记为T。而搜索的串记为O字符串的索引从零开始
用next数组来表示部分匹配表,数组下标从0开始。next[i]代表结束字符索引为i字串的最大部分匹配长度
串的长度为len,开始索引为begin,结束索引为end。则有
end - begin + 1 = len;
部分匹配表递推
已知条件
假设next[m-1]的值已知,根据部分匹配表的定义可知前next[m-1]个字符序列(T[0]~T[next[m-1] - 1])与字符序列(T[m - next[m - 1]]~T[m-1] )是相同的(如下图,记为Sub1)若T[m] == T[next[m-1]]
则T[0]~T[next[m-1]]与字符序列T[m - next[m-1]]~T[m]是相同的。亦即前缀和后缀分别向后扩展一位后仍是相等的两个字符串(如下图,记为Sub2)若T[m] != T[next[m-1]]
由推算前提可知,字符序列(T[m - next[m - 1]]~T[m-1] )与(T[0]~T[next[m-1] - 1])是相同的。只是这两个字序列在向后扩展时遇到了分歧。找出字串Sub1(下标为0~next[m - 1] - 1)的最长部分匹配子串(记为Sub3,假设长度为k,k = next[next[m - 1] - 1])。
因为T[m]前边存在Sub1串,而Sub3又是Sub1的部分匹配子串。因此T[m]前边肯定存在Sub3串。只需要比较T[k]和T[m]即可
如果T[k]和T[m]仍旧不等,一直往下。直到找不到部分匹配字串。
部分匹配表计算示例
- 绿色代表已经匹配的序列
- 蓝色代表待计算的next值,黄色代表求改next值时尾部待比较的字符
初始
因为前四个字符都是各不相同的,所以next值肯定为0。next[4]
- T[4] == T[1],所以 next[4] = 1
next[5]~next[6]
- T[5] == T[1],所以next[5] = 2
- T[6] == T[2],所以next[6] = 3
next[7]
1. T[7] != T[3],因此查找T[7]前的已经遍历过的子串“ABCFABC”的部分匹配字串,查next表知next[6]为3
2. 回溯查看长度为3的子串(“ABC”)的部分匹配序列,也就是查看next[2]的值(为0)
3. 结束,next[7]为0.
next[14]
1. T[14] != T[6],因此查找T[14]前的已经遍历过的子串“ABCFABCDABCFAB”的部分匹配字串,查next表知next[13]为6.
2. 回溯查看长度为6的子串(“ABCFAB”)的部分匹配序列长度,也就是next[5]的值(为2)
3. 比较T[2] 与T[14] (此时开头部分和结尾部分前面都有子串“AB”)
4. T[2] !=T[14],继续找出字串“AB”的部分匹配长度,为零。终止迭代,因此next[14]为0。
程序代码
#include <iostream> #include <string> using namespace std; void Print(int* array,int length) { for(int i = 0;i < length; ++ i) cout << array[i] << " "; cout << endl; } int CalculateNext(const string& str,int** array) { int len = str.size(); int* next = (int *)new int[len]; next[0] = 0;//对于只包含第一个字符的字符序列来说,前缀和后缀都是空集。因此部分匹配长度为0; int prefix_tail_index = 0;//前缀串中下一次待比较的索引数,因为一开始的时候,都未比较过,索引前缀从零开始 for(int suffix_tail_index = 1;suffix_tail_index < len; ++ suffix_tail_index) { while(prefix_tail_index != 0 && str[prefix_tail_index] != str[suffix_tail_index] ) { //如果前缀的待比较位与后缀最后一位不相等,那么找出长度为suffix_tail_index的字串的最长部分匹配子串 // 从该串的后面一位开始与str[suffix_tail_index]比较 prefix_tail_index = next[prefix_tail_index - 1]; } if(str[prefix_tail_index] == str[suffix_tail_index]) { //在求出了next[i - 1]的基础上发现最长部分匹配序列可以向后扩展一位。 prefix_tail_index ++; //记录最长部分匹配长度为prefix_tail_index,但是前缀串的最后一个字符的索引为prefix_tail_index - 1; next[suffix_tail_index] = prefix_tail_index; continue; } else { next[suffix_tail_index] = 0; } } *array = next; return len; } int KMP(const string& o_str,const string& target) { int *pmt; int tlen = CalculateNext(target,&pmt); int match_bits = 0;//比较过程两个序列前面已经相同的位数 int start_pos = 0;//某次比较过程中,原串的起始比较位置 int olen = o_str.size(); while(match_bits < tlen && start_pos + match_bits < olen) { if(target[match_bits] == o_str[start_pos + match_bits]) { match_bits ++; cout << "match_bits:" << match_bits << endl; } else { if(match_bits == 0) { //前面没有发现匹配的字符 start_pos += 1; } else { //移动位数 = 已匹配的字符数 - 对应的部分匹配值 int offset = match_bits - pmt[match_bits - 1]; start_pos += offset; match_bits = pmt[match_bits - 1]; } cout << "start_pos:" << start_pos << ",match_bits:" << match_bits << endl; } } if(match_bits == tlen) return start_pos; else return -1; } int main() { // string str1("ABCDABD"); string str1("ABCFABCDABCFABD"); // string str2("ABD"); string str2("ABCFABD"); cout << KMP(str1,str2) << endl; return 0; }
PS
如果你觉得不错,点个赞吧相关文章推荐
- 搜狗百度360市值齐跌:搜索引擎们陷入集体焦虑?
- 本人即将筹备败家日志,敬请期待!
- 书评:《算法之美( Algorithms to Live By )》
- IE:使用搜索助手
- 动易2006序列号破解算法公布
- C#数据结构之顺序表(SeqList)实例详解
- C#递归算法之分而治之策略
- Ruby实现的矩阵连乘算法
- C#插入法排序算法实例分析
- C#算法之大牛生小牛的问题高效解决方法
- Lua教程(七):数据结构详解
- C#算法函数:获取一个字符串中的最大长度的数字
- 解析从源码分析常见的基于Array的数据结构动态扩容机制的详解
- 超大数据量存储常用数据库分表分库算法总结
- C#数据结构与算法揭秘二
- C#冒泡法排序算法实例分析
- C#数据结构之队列(Quene)实例详解
- C#数据结构揭秘一
- C#数据结构之单链表(LinkList)实例详解
- 算法练习之从String.indexOf的模拟实现开始