KMP算法--Sunday算法--笔记

1、KMP算法

参考视频:bilibili
参考博客:KMP
时隔一个月,KMP算法又忘了。

第一步:复习前缀、后缀的概念

例1:字符串"abcdefg"
前缀集合:{“a”,“ab”,“abc”,“abcd”,“abcde”,“abcdef”},g被排除了
后缀集合:{“g”,“fg”,“efg”,“defg”,“cdefg”,“bcdefg”},a被排除了

例2:字符串"ab"
前缀集合:{“a”}
后缀集合:{“b”}

例3:字符串"a"
前缀集合:{“a”}
后缀集合:{""}

第二步:复习next[i]的值的含义

参考博文:https://blog.csdn.net/x__1998/article/details/79951598
next[i]表示字符串str[0…i-1]中前缀、后缀集的交集中最长子串长度
例1:字符串“abab”,问next[3]=1的含义??
子串str[0…2]=“aba”
前缀集合:{“a”,“ab”}
后缀集合:{“a”,“ba”}
那么交集就是:“a”,长度为1,则next[3]=1

约定:next[0]=-1,这是标志,不代表长度。

第三步:手撕Getnext()代码

思路:输入的字符串既是目标字串,又是模式字串。

void Getnext(int next[],String t)
{
	/* 创建2个指针,left指前缀子串,right指向后缀子串 */
   int left = -1, right = 0;
   next[0]=-1;
   while(right < t.length - 1)	//right从0到n-2
   {
   		/* left == -1 情况有2种:
   		* 初始化后首次进入循环
   		* 多次匹配失败回溯后,left回溯到了最初开始的地方 
   		* 以上2种情况都意味着:没有1个字符匹配成功,那就双指针++*/
      if(left== -1 ||  t[left] == t[right])		
      {
      	/* 匹配成功也双指针++ */
         left++;right++;		
		 if (t[left] == t[right]) { 
		/* 对特殊情况的优化,例:字符串“aaaaaaax” ,连续重复字符*/		
		/* left与right此刻是相邻着的,且2指针指向的字符相同,那回溯的地方也相同 */
              next[right] = next[left];		

           } else {
           /* 一般情况:next[]存放子串长度 */
			   next[right] = left;	//left经过++后,既是下次循环的index,又是本次循环的最大前后缀交集字串的长度
		   }
      }
      else left = next[left];	//回溯,当模式匹配串失配时,next数组对应的元素指导应该用T串的哪个元素作为前缀进行下一轮的匹配
   }
}

第四步:手撕KMP算法框架

int KMP(String s,String t)
{
	//s表示目标串,t表示模式串
   int next[t.length],si=0;ti=0;
   Getnext(t,next);
   while(si<s.length && ti<t.length)
   {
      if(ti == -1 || s[si] == t[ti])	
      {
         si++;
         ti++;
      }
      else ti=next[ti];               //失配,回溯模式串指针,避免了模式串开头几个字符的重复匹配
	  //利用已经部分匹配这个有效信息,保持i指针不回溯,通过修改j指针,让模式串尽量地移动到有效的位置
   }
   if(ti >= t.length)
       return (si-t.length);         //匹配成功,返回子串的位置
   else
      return (-1);                  //没找到
}

2、Sunday算法

模式匹配算法都需要一个源串的滑动指针 i 和模式串的滑动指针 j 。普通的匹配算法在字符失配时需要同时回溯 i 和 j ,复杂度为O(m*n)。

  • KMP匹配算法就是在失配时不回溯源串指针 i,只回溯 j 到最长前缀处使得复杂度达到O(n)。
  • Sunday算法不仅不回溯 i,而且可以使 i 跨越模式串前进,进一步降低复杂度。

C++程序

i 作为模式串T的滑动指针,L作为匹配位置的起点,那么 L+i 就是源串S的滑动指针

string sunday(string S, string T) {
	static int cnt;
	int N = S.length(), M = T.length(), L = 0, i = 0;
	//L表示匹配开始的位置,i表示滑动指针
	map<char, int> sun;
	for (int c = 0; c < T.size(); ++c) {
		sun[T[c]] = c;
	}
	// 区间S[0, N)
	while ((L + i) < N) {
		// 匹配成功,指针右移
		while (i < M && S[L + i] == T[i]) {
			++i;
			// 匹配结束
			if (i == M) return T;
		}

		// 匹配失败
		if (S[L + i] != T[i]) {
			// 若字符S[L + M + 1]不曾在模式串中出现过,则移动下次匹配开始的位置
			while ((L < N - M) && (sun.find(S[L + M]) == sun.end())) {	
				L += M - 1;		//L表示匹配开始的位置
			}
			// 若已经到达边界,则退出循环,L表示匹配开始的位置
			if ((L + M) >= N) {
				break;
			}
			// 回溯i,重新定位匹配起始位置L
			// L += (L + M) - (L + sun[S[L + M]]);
			L += M - sun[S[L + M]];
			i = 0;
		}
	}
	return "";
}

记忆点:理解为什么要与S[L+M]比较?

记模式串为S,子串为T,N = S.length(),M = T.length(), 设S中此次第一个匹配的字符位置为L。那么,此次参与匹配的字符为S[L,L+M)区间。

当S[L+i]与T[i]失配时,显然,当前匹配区间S[L,L+M)的剩余长度肯定不足够了,那么字符S[L+M]肯定要参加下一轮的匹配,并且T至少要与S[L+M]匹配才有可能与整个S区间匹配。

记忆点:理解L的前进步长

Sunday算法需要对模式串T进行预处理,记录T中每个字符的最后一次出现的位置。可以使用数组或者哈希表,如

map<char, int> sun;

for (int c = 0; c < T.size(); ++c) sun[T[c]] = c;
当S[L+i]与T[i]失配时,就要考虑S[L+M]是否在T中出现过,位置是哪里,举例:
L=0,每次匹配的起始位置
i=3,模式串的滑动指针
M=4,模式串的长度

S:abcceabcaabcd
T:abcd
发现d与c不匹配。此时S[L+M]=='e',没有出现在T中。于是:
S:abcceabcaabcd
T:-----abcd
可见,L一次性增加了4+1个单位

发现d与a不匹配。此时S[L+M+1]=='a',T中最后出现在T[0],而M - 0 = 4 - 0。于是:
S:abcceabcaabcd
T:---------abcd
可见,L一次性增加了4个单位,成功匹配。
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值