代码随想录day9 KMP算法/golang实现strStr

代码随想录day9  KMP算法/golang实现strStr

目录

KMP基础

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

思路:


KMP基础

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

什么是KMP?

        说到KMP,先说一下KMP这个名字是怎么来的,为什么叫做KMP呢。因为是由这三位学者发明的:Knuth,Morris和Pratt,所以取了三位学者名字的首字母。所以叫做KMP

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

        所以如何记录已经匹配的文本内容,是KMP的重点,也是next数组肩负的重任。其实KMP的代码不好理解,一些同学甚至直接把KMP代码的模板背下来。没有彻底搞懂,懵懵懂懂就把代码背下来太容易忘了。不仅面试的时候可能写不出来,如果面试官问:next数组里的数字表示的是什么,为什么这么表示?估计大多数候选人都是懵逼的。

什么是前缀表?

        写过KMP的同学,一定都写过next数组,那么这个next数组究竟是个啥呢?next数组就是一个前缀表(prefix table)。

前缀表有什么作用呢?

        前缀表是用来回退的,它记录了模式串与主串(文本串)不匹配的时候,模式串应该从哪里开始重新匹配。

为了清楚的了解前缀表的来历,我们来举一个例子:

        要在文本串:aabaabaafa 中查找是否出现过一个模式串:aabaaf。请记住文本串和模式串的作用,对于理解下文很重要,要不然容易看懵。所以说三遍:

要在文本串:aabaabaafa 中查找是否出现过一个模式串:aabaaf。

要在文本串:aabaabaafa 中查找是否出现过一个模式串:aabaaf。

要在文本串:aabaabaafa 中查找是否出现过一个模式串:aabaaf。

        我特意把 子串aa 标记上了,这是有原因的,大家先注意一下,后面还会说道。可以看出,文本串中第六个字符b 和 模式串的第六个字符f,不匹配了。如果暴力匹配,会发现不匹配,此时就要从头匹配了。

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

此时就要问了前缀表是如何记录的呢?

        首先要知道前缀表的任务是当前位置匹配失败,找到之经匹配上的位置,再重新匹配,此也意味着在某个字符失配时,前缀表会告诉你下一步匹配中,模式串应该跳到哪个位置。那么什么是前缀表:记录下标i之前(包括i)的字符串中,有多大长度的相同前缀后缀。

最长公共前后缀?

        文章中字符串的前缀是指不包含最后一个字符的所有以第一个字符开头的连续子串。后缀是指不包含第一个字符的所有以最后一个字符结尾的连续子串。正确理解什么是前缀什么是后缀很重要!

        那么网上清一色都说 “kmp 最长公共前后缀” 又是什么回事呢?我查了一遍 算法导论 和 算法4里KMP的章节,都没有提到 “最长公共前后缀”这个词,也不知道从哪里来了,我理解是用“最长相等前后缀” 更准确一些。

        因为前缀表要求的就是相同前后缀的长度。而最长公共前后缀里面的“公共”,更像是说前缀和后缀公共的长度。这其实并不是前缀表所需要的。所以字符串a的最长相等前后缀为0。字符串aa的最长相等前后缀为1。字符串aaa的最长相等前后缀为2。等等.....

为什么一定要用前缀表?

        这就是前缀表,那为啥就能告诉我们 上次匹配的位置,并跳过去呢?回顾一下,刚刚匹配的过程在下标5的地方遇到不匹配,模式串是指向f,如图:

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

         以下这句话,对于理解为什么使用前缀表可以告诉我们匹配失败之后跳到哪里重新匹配 非常重要!

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

        所以前缀表具有告诉我们当前位置匹配失败,跳到之前已经匹配过的地方的能力。

接下来就要说一说怎么计算前缀表。如图:

         长度为前1个字符的子串a,最长相同前后缀的长度为0。(注意字符串的前缀是指不包含最后一个字符的所有以第一个字符开头的连续子串;后缀是指不包含第一个字符的所有以最后一个字符结尾的连续子串。)

长度为前2个字符的子串aa,最长相同前后缀的长度为1。 

 长度为前3个字符的子串aab,最长相同前后缀的长度为0。

        以此类推:长度为前4个字符的子串aaba,最长相同前后缀的长度为1。长度为前5个字符的子串aabaa,最长相同前后缀的长度为2。长度为前6个字符的子串aabaaf,最长相同前后缀的长度为0。

那么把求得的最长相同前后缀的长度就是对应前缀表的元素,如图:

        可以看出模式串与前缀表对应位置的数字表示的就是:下标i之前(包括i)的字符串中,有多大长度的相同前缀后缀。再来看一下如何利用 前缀表找到 当字符不匹配的时候应该指针应该移动的位置。如动画所示:

 

         找到的不匹配的位置, 那么此时我们要看它的前一个字符的前缀表的数值是多少。为什么要前一个字符的前缀表的数值呢,因为要找前面字符串的最长相同的前缀和后缀。所以要看前一位的 前缀表的数值。前一个字符的前缀表的数值是2, 所有把下标移动到下标2的位置继续比配。 可以再反复看一下上面的动画。最后就在文本串中找到了和模式串匹配的子串了。

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

        给你两个字符串 haystack 和 needle ,请你在 haystack 字符串中找出 needle 字符串的第一个匹配项的下标(下标从 0 开始)。如果 needle 不是 haystack 的一部分,则返回  -1 。

输入:haystack = "sadbutsad", needle = "sad"
输出:0
解释:"sad" 在下标 0 和 6 处匹配。
第一个匹配项的下标是 0 ,所以返回 0 。

思路:

在一个串中查找是否出现过另一个串,这是KMP的看家本领。

// getNext 构造前缀列表next
// 参数:
//		next前缀表数组
//		s 模式串

// 前缀是指不包含最后一个字符结尾的连续子串
// 后缀是指不包含第一个字符的所有以最后一个字符结尾的字串

// 1.初始化
// 2.处理前后缀不相同的情况
// 3.处理前后缀相同的情况
// 4.更新next数组的值

// j是指向前缀末尾位置,i是指向后缀末尾位置
// j还代表着包括i和i之前的字串的最长相等前后缀的长度
// j的初始化是0。前缀一开始是从最开始的位置开始。
// next数组初始化。next数组是指要回退到的位置的。即next[0]其实就还是回退到0
// i初始化多少呢?i在for循环比较里面初始化。因为j初始化为0,因为j是前缀的末尾, 那么要比较前后缀所对应的字符是否相等,那么i应该从1开始,这样i和j才能开始比较

//getNext函数主要是填充模式串的next数组值
func getNext(next []int, s string) {
	j := 0                        // j的初始化是0。前缀一开始是从最开始的位置开始。
	next[0] = j                   // next数组初始化。next数组是指要回退到的位置的下标。即next[0]其实就还是回退到0
	for i := 1; i < len(s); i++ { // i初始化多少呢?i在for循环比较里面初始化。因为j初始化为0,因为j是前缀的末尾, 那么要比较前后缀所对应的字符是否相等,那么i应该从1开始,这样i和j才能开始比较
		for j > 0 && s[i] != s[j] { // 进行处理前后缀不相同的情况,j为什么要大于0因为j要是等于0的话j = next[j-1]就数组越界了。因为j最多也就会退到0。
			j = next[j-1] //j要回退到前一个位置所在的next数组的值,回退是一个连续过程,所以要用while循环。
		}
		if s[i] == s[j] { //如果前后缀相同
			j++ // 例如aa前后缀相同的,第二个a的next值应该为1了,往后依次推
		}
		next[i] = j //更新next数组的值,更新i所对应的next值
	}
}

//举例       a  a  b  a  a  f
//数组下标    0  1  2  3  4  5
//next       0
//1.进入for循环,j=0,此时s[i]为s[1]为a == s[j]为s[0]为a,两个相等,所以j++然后更新给next。既填充next[1]=1
//举例       a  a  b  a  a  f
//数组下标    0  1  2  3  4  5
//next       0  1
//2.进入for循环的for循环。j=1, i = 2, s[i]==b,s[j]==a,前后缀不匹配,j = next[j-1]然后更新next[i]=j,即填充next[2]=0
//举例       a  a  b  a  a  f
//数组下标    0  1  2  3  4  5
//next       0  1  0
//3.进入for循环的for循环。j=0,i=3, s[j]==a, s[i]==a. 前后缀匹配。j++, next[i]=j.即填充next[3]=1
//举例       a  a  b  a  a  f
//数组下标    0  1  2  3  4  5
//next       0  1  0  1
//4.进入for循环的for循环。j=1,i=4,s[j]==a, s[i]==a,前后缀匹配。j++, next[i]=j.即填充next[4]=2
//举例       a  a  b  a  a  f
//数组下标    0  1  2  3  4  5
//next       0  1  0  1  2
//5.进入for循环的for循环。j=2,i=5, s[j]=b, s[i]=f,前后缀不匹配,j=next[j-1]=1
//别忘记这里第二层循环是while哦,这时候j=1,i=5是依然满足二层循环条件的
//6.继续第二层循环,j=1,i=5,s[j]==a, s[i]==f,前后缀不匹配,j=next[j-1]=0
//7.这时候二层循环结束了,该更新next数组了,next[i]=next[5]=j=0,即填充next[5]=0
//举例       a  a  b  a  a  f
//数组下标    0  1  2  3  4  5
//next       0  1  0  1  2  0

//使用next数组来做匹配
func strStr(haystack string, needle string) int {
	n := len(needle)
	if n == 0 {
		return 0
	}
	j := 0
	next := make([]int, n)
	getNext(next, needle)
	// 得到了next数组之后,就要用这个来做匹配了
	//定义两个下标,j指向模式串起始位置,i指向文本串起始位置。
	for i := 0; i < len(haystack); i++ { //遍历文本串
		for j > 0 && haystack[i] != needle[j] { //如果不一样了,j要回退到前一个位置所在的next数组的值,
			j = next[j-1]
		}
		if haystack[i] == needle[j] { //如果一直相同就一直往右边移动,i和j都是
			j++
		}
		if j == n { //判断在文本串s里出现了模式串t。如果j指向了模式串t的末尾,那么就说明模式串t完全匹配文本串s里的某个子串了
			return i - n + 1 //本题要在文本串字符串中找出模式串出现的第一个位置 (从0开始),所以返回当前在文本串匹配模式串的位置i 减去 模式串的长度,就是文本串字符串中出现模式串的第一个位置。
		} //至少返回1的,因为如果i和j和n都为0,那在前面就返回0了。
	}
}

思路2 :暴力匹配

              从主串中每次拿一串长度为模式串的长度去匹配模式串,一共要匹配len(主串)-len(模式串)次,每次的匹配是模式串所有字母挨个匹配。

        这样做的缺点是容易超时,当主串特别长的时候,但是目前在leetcode可以过。

func strStr(haystack string, needle string) int {
    l1 := len(haystack)
    l2 := len(needle)
    for i:=0;i<=l1-l2;i++{
        if haystack[i:i+l2] == needle {
            return i
        }
    }
    return -1
}

思路3:库函数

func strStr(haystack string, needle string) int {
	return strings.Index(haystack, needle)    // 找出needle在haystack中出现的第一个位置
}

总结:

kmp好难哦。其实也不难,主要就是next数组的构建。构建完根据这个来会退j就可以啦。内容许多是代码随想录的。坚持第九天。

以上

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值