当前位置:   article > 正文

28. 找出字符串中第一个匹配项的下标(KMP)

找出字符串中第一个匹配项的下标

解题思路

KMP的经典思想就是:当出现字符串不匹配时,可以记录一部分之前已经匹配的文本的内容,利用这些信息避免从头再去做匹配。(代码随想录)

1、什么是KMP

KMP是由三位学者发明的,Knuth,Morrist,Pratt,取了三位学者名字的首字母。

2、KMP的作用

KMP主要应用在字符串匹配上。主要思想是当出现字符串不匹配时,可以知道一部分之前已经匹配的文本内容,可以利用这些信息避免从头再去做匹配了。重点是如何激励已经匹配的文本内容。

3、什么是前缀表

前缀表是用来回退的,它记录了模式串与文本串不匹配的时候,模式串应该从哪里重新匹配。
前缀表是:记录下标i之前(包括i)的字符串中,有多大长度相同前缀后缀
例:要在文本串aabaabaafa中查找是否出现过一个模式串aabaaf
如下图可以看出,文本串中的第六个字符b和模式串中第六个字符f不匹配了。

若使用前缀表,就看可以不用从头匹配。而是从上次已经匹配的内容开始匹配,找到模式串中第三个字符b继续开始匹配。

 

 

那么前缀表是如何记录的呢?
前缀表的任务是当前位置匹配失败,找到之前已经匹配的位置,在重新匹配,也就意味着在某个字符匹配失败,前缀表会告诉你下一步匹配,模式串应该跳到哪里。

4、最长公共前后缀。

前缀是指不包含最后一个字符的所有以第一个字符开头的连续子串。
后缀是指不包含第一个字符的所有以最后一个字符开头的连续子串。

5、为什么一定要用前缀表

回顾一下,刚刚在匹配过程中,在下标5的地方不匹配,如图

 然后就找到了下标2,指向b,继续匹配,如图

 

解释:下标5之前这部分的字符串(aabaa)的最长相等的前缀和后缀是子字符串aa,因为找到了最长相等的前缀和后缀,匹配失败的位置就是后缀字串的后面,那么我们就只需找到与其相同的前缀后面重新匹配就可以了。

6、如何计算前缀表

长度为前1个字符的子串a,最长相同前后缀的长度为0。
长度为前2个字符的子串aa,最长相同前后缀的长度为1。
长度为前3个字符的子串aab,最长相同前后缀的长度为0。
长度为前4个字符的子串aaba,最长相同前后缀的长度为1。
长度为前5个字符的子串aabaa,最长相同前后缀的长度为2。
长度为前6个字符的子串aabaaf,最长相同前后缀的长度为0。

 可以看出,模式串与前缀表对于位置的数字表示的就是:下标i之前(包括i)的字符串中,有多大长度的相同前缀后缀。

 找到的不匹配的位置,此时看它的前一个字符的前缀表数值是多少(因为要找前面字符串的最长相同的前缀和后缀),然后移动到下标为那个数值的位置

7、前缀表与next数组

next数组就是前缀表,很多实现是把前缀表统一减一,但也可以就是前缀表。

8、时间复杂度分析

文本串长度为n,模式串长度为m。因为在匹配过程中,根据前缀表不断调整匹配的位置,可以看出匹配的过程是O(n),之前还要单独生成next数组,时间复杂度是O(m),所以整个KMP算法的时间复杂度是O(m+n).暴力法的时间复杂度是O(m*n)。

9、构造next数组

  1. //函数参数为指向next数组的指针,和一个字符串
  2. void getNext(int* next, const string& s){
  3. int j = -1;//j指向前缀末尾的位置
  4. next[0] = j;
  5. for(int i = 1; i < s.size(); i++) { // 注意i从1开始
  6. while (j >= 0 && s[i] != s[j + 1]) { // 前后缀不相同了
  7. j = next[j]; // 向前回退,next[j]就是记录着j(包括j)之前的子串的相同前后缀的长度。那么 s[i] 与 s[j+1] 不相同,就要找 j+1前一个元素在next数组里的值(就是next[j])。
  8. }
  9. if (s[i] == s[j + 1]) { // 如果 s[i] 与 s[j + 1] 相同,那么就同时向后移动i 和j 说明找到了相同的前后缀,同时还要将j(前缀的长度)赋给next[i], 因为next[i]要记录相同前后缀的长度。
  10. j++;
  11. }
  12. next[i] = j; // 将j(前缀的长度)赋给next[i]
  13. }
  14. }

10、使用next数组来做匹配

  1. int j = -1; // 定义两个下标j 指向模式串起始位置,因为next数组里记录的起始位置为-1。
  2. for (int i = 0; i < s.size(); i++) { //i指向文本串起始位置,注意i就从0开始遍历文本串
  3. while(j >= 0 && s[i] != t[j + 1]) { // 不匹配
  4. j = next[j]; // j 寻找之前匹配的位置,就要从next数组里寻找下一个匹配的位置
  5. }
  6. if (s[i] == t[j + 1]) { // 匹配,j和i同时向后移动
  7. j++; // i的增加在for循环里
  8. }
  9. if (j == (t.size() - 1) ) { // 文本串s里出现了模式串t
  10. return (i - t.size() + 1);
  11. }
  12. }

代码

  1. class Solution {
  2. public:
  3. void getNext(int* next, const string& s)
  4. {
  5. int j = -1;
  6. next[0] = j;
  7. for(int i=1;i<s.size();i++)
  8. {
  9. while(j>=0 && s[i]!=s[j+1])
  10. {
  11. j=next[j];
  12. }
  13. if(s[i] == s[j+1])
  14. {
  15. j++;
  16. }
  17. next[i]=j;
  18. }
  19. }
  20. int strStr(string haystack, string needle) {
  21. if(needle.size() == 0) return 0;
  22. int next[needle.size()];
  23. getNext(next,needle);
  24. int j=-1;
  25. for(int i=0;i<haystack.size();i++)
  26. {
  27. while(j>=0 && haystack[i] != needle[j+1])
  28. {
  29. j=next[j];
  30. }
  31. if(haystack[i] == needle[j+1])
  32. {
  33. j++;
  34. }
  35. if(j == (needle.size()-1))
  36. {
  37. return (i-needle.size()+1);
  38. }
  39. }
  40. return -1;
  41. }
  42. };

声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/凡人多烦事01/article/detail/669545
推荐阅读
相关标签
  

闽ICP备14008679号