KMP算法及其优化(超详解)

KMP算法的推导

主串ABCDABBACABCDABD
子串ABCDABD
这时,我们发现子串的第7位(这里的位数指的都是从一开始的位数)与主串不同。
那么,我们下一次应该让主串哪一位与子串的哪一位进行比较呢?
由于子串的前6位与主串的前6位,已经比较过了我们已经知道了前6位所有的数字,我们当然不用再次比较。
给出假设,假如我们只知道子串的字母与顺序,对于主串我们只能得到哪一位等于或者不等于(只有等于了才能确定它的值)。
先不考虑算法的实现,用人工的方法考虑,最好的方式显然是用主串的第7位(虽然比较过,但我们不知道它是多少)与子串的第3位

进行比较。
主串ABCDABBACABCDABD
子串          ABCDABD
我们发现再次匹配失败,下一次要比较哪一位呢?
思考过后我们发现,第7位只能和第一位进行比较。
主串ABCDABBACABCDABD
子串               ABCDABD
我们发现,仍然不匹配,这时我们知道以第7位开头的主串的子串,不可能和我们我们寻找的子串相等了,我们需要比较主串下一位
和子串的第一位

因为我们一直都不知道主串出现不匹配的位置的字母是什么,所以我们每次遇到不匹配的位置,对子串的操作是完全一样的。
我们可以计算出每种不匹配情况下,下一次该比较哪一位。
子串     ABCDABD
下一次 0 111 1 2 3
我们思考,为什么能得到这样一个序列?
主串ABCDEF
子串ABCDA
主串ABCDEF
子串  ABCDA
主串ABCDEF
子串     ABCDA
主串ABCDEF
子串       ABCDA
先看前4个,ABCD均不相同,如果这时候第五个不匹配,我们知道前四个都不一样,所以不论如何移动前四个都不能对的上
所以,只能下一次比较第一位。

主串ABCDABBACABCDABD
子串ABCDABD
主串ABCDABBACABCDABD
子串          ABCDABD
我们看之前的这个序列,之所以下次能够不从第一个开始,是因为我们发现AB和AB恰好可以对上。
我们得到结论:下一次访问的位置,由已经匹配了的字符的最长的后缀和前缀相等的长度决定。
我们的移动位置值是每次已经匹配的字符的最长的后缀和前缀相等的长度+1。
是因为要移动到已经匹配了的字符串的下一位

那我们怎么让计算机像我们一样,找到下一次该从哪里进行比较呢?
因为我们只知道子串的数据,只需将将我们得到的下一次要比较的位置记录下来即可。
这就是Next数组。下面给出代码实现:

最初的KMP算法

int Next[100];//数组大小根据需要而定,这里的Next数组从一开始,所以为子串的长度+1
//从第一位开始,首位设为0,且只有第一个为零(只有第一个不匹配或者匹配的情况才直接移动主串的位置)
//数组Next[i]中记录的是,如果这一位不一样,下一次从哪一位进行匹配
void getnext(string a) {
 memset(Next, 0, sizeof(Next));
 int i = 1, j = 0;//j=0为了i比j大,进行错位比较
 //其中j用来记录已经匹配的长度+1(因为要移动到已经匹配了的值的下一位),Next数组中存的值就是当时j的值
 //i进行位移操作
 Next[1] = 0;//因为1之前没有没有字符,所以赋值为0(表示不用再进行回溯操作)
 while (i < a.length()) {//i的范围是[1,a.length-1],所以字符串的最后一位没有起作用,似乎是少了一位,但是稍加分析可以得出,第二位相当于肯定是1
  if (j == 0 || a[i-1] == a[j-1]) {//j==0进入循环,这就是不出现0的原因,如果符合或者第一位不符合之后,都进行后移操作。
   i++;
   j++;
   Next[i] = j;
  }
  else j = Next[j];//Next中有每次不符合,下一次从哪一位查起
  //算法的核心部分
  //每一次匹配的过程都是子串的一个子子串,与子串进行比较的过程
  //子串 A B C D A B D
  //相当于把这个子串拆开,一点点求出最终的Next数组
  //A->AB->ABC->ABCD->ABCDA->ABCDAB->ABCDABD
  //0->01->011->0111->01111->011112->0111123
  //Next[i] = (a[i-1] == a[Next[i-1]]) ? Next[i-1] + 1  : 一次或多次回溯后的结果
  //子子串S[i-j]->S[i]
  //无->B ->C  ->   D ->   A-> AB->   D(不起作用)
  //不匹配就记成回溯后的结果,匹配就记成匹配的长度+1
 }
}

KMP的过程和求Next数组的过程很像,但需要注意i,j都是从1开始的

int KMP1(string a, string b) {
 getnext(b);
 int i = 1, j = 1;//从每一个字符串的第一位进行遍历
 while (i <= a.length() && j <= b.length()) {//当某一的字符串全部遍历完成以后终止
  if (j == 0 || a[i - 1] == b[j - 1])i++, j++;
  else j = Next[j];
 }
 if (j > b.length())return i - b.length();
 else return 0;
}

优化后的KMP算法

子串     ABCDABD
Next     0 111 1 2 3
我们发现S[5]=‘A’(方便理解从第一位开始计数)
S[Next[5]]=‘A’
如果S[5]!=X 那么S[Next[5]]!=X也一定成立
我们发现进行了一次多余的比较,这时对Next数组进行修正。
使得Next[5]=Next[Next[5]]

得到新的Next
Next 0 1 1 1 0 1 3

int Next_val[100];
void getnext_val(string a) {
 int i = 1, j = 0;Next_val[1] = 0;
 while (i < a.length()) {
  if (j == 0 || a[i - 1] == a[j - 1]) {//-1是为了和string 从零开始相对应
   i++, j++;
   if (a[i - 1] != a[j - 1]) {//相同序列的第一位不同位不进行回溯,(已经不同了,回溯之后的位置的元素与它不同)
   Next_val[i] = j;
   }
   else Next_val[i] = Next_val[j];//跳过一次无意义的比较

    }
  else j = Next_val[j];
 }
}

此时有两种情况
第一种 j==0&&a[0]==已经遍历到的a的位置的元素a[i-1]
说明子串在这个位置时,的Next值为1且这一个元素和子串第一个元素相等,那么它下一次回溯到的元素是第一个,
由于它和第一个元素一样,所以也让Next_val[i]=Next_val[j]=0
第二种

//例子
//A B C A B C D
//0 1 1 1 2 3 4  
//0 1 1 0 1 1 4
//a[i-1]==a[j-1]&&a[i]==a[j]说明前一位对应的比较相等,且这一位对应的比较也相等(这里的i,j指的都是最一开始ij)
//也说明它回溯的位置的元素和它自己一样,所以让Next_val[i] = Next_val[j]
//比如S[4]与S[1]相等并且S[5]和S[2]相等这时Next[5]的值为Next[2]
//a[i-1]==a[j-1]&&a[i]!=a[j]说明前一位对应的比较相等,且这一位对应的比较也不相等(这里的i,j指的都是最一开始ij)
//也说明它回溯的位置的元素和它自己不一样,所以让Next_val[i] = j
//比如S[7]!=S[4]它回溯的值不一样
//综上所述,前一位和这一位检测的作用:前一位确定这一位之前已经匹配的长度,这一位则确定回溯位置的元素和这一位的元素是否相同,判断
Next数组是否需要修正

综上所述,前一位和这一位检测的作用:前一位确定这一位之前已经匹配的长度,这一位则确定回溯位置的元素和这一位的元素是否相同,判断Next数组是否需要修正

KMP过程一模一样,只有Next数组不一样

int KMP2(string a, string b) {
 getnext_val(b);
 int i = 1, j = 1;
 while (i <= a.length() && j <= b.length()) {
  if (j == 0 || a[i - 1] == b[j - 1])i++, j++,cnt++;
  else j = Next_val[j];
 }
 if (j > b.length())return i - b.length();
 else return 0;
}
  • 8
    点赞
  • 15
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值