KMP算法(Go语言描述)

基础知识:

1. 前缀(Prefix)

前缀是指一个字符串从第一个字符开始的任何子串,不包括最后一个字符。换句话说,前缀是字符串的开头部分。

  • 示例:
    • 对于字符串 "ABC",其前缀有:
      • "A"
      • "AB"

2. 后缀(Suffix)

后缀是指一个字符串从最后一个字符开始的任何子串,不包括第一个字符。后缀是字符串的结尾部分。

  • 示例:
    • 对于字符串 "ABC",其后缀有:
      • "C"
      • "BC"

3. 公共前后缀(Common Prefix and Suffix)

公共前后缀是指某个字符串的前缀和后缀相同的部分,长度必须大于 0。换句话说,字符串的前缀和后缀是完全相同的某一段子串。

  • 示例:
    • 对于字符串 "ABAB",它的公共前后缀是:
      • "AB",它既是字符串的前缀(前两个字符)又是后缀(后两个字符)。

4. 例子分析

假设有字符串 "ABCDABD"

  • 前缀"A", "AB", "ABC", "ABCD", "ABCDA", "ABCDAB"
  • 后缀"D", "BD", "ABD", "DABD", "CDABD", "BCDABD"
  • 公共前后缀"A"(开头的 "A" 和结尾的 "A" 相同)。

KMP算法的原理

KMP 的核心思想是,在匹配过程中,如果出现字符不匹配,借助模式串自身的部分匹配信息,直接跳过一些不必要的字符比较。KMP 通过构建一个 next 数组(也称为部分匹配表)来实现这个跳过操作。

算法的两大步骤

  1. 构建 next 数组(部分匹配表)
  2. 利用 next 数组进行快速匹配
1. 构建 next 数组

next 数组记录了模式串中每个字符之前的部分子串的前缀与后缀的匹配情况。当出现字符不匹配时,可以借助 next 数组快速找到下一个可能的匹配位置,避免从头开始匹配。

  • next[i] 的含义
    • next[i] 表示在模式串的第 i 个位置失配时,模式串应跳到的下一个位置,即前面部分子串的最长公共前缀的长度减 1。
    • 如果 next[i] = k,说明在模式串的前 i 个字符中,存在长度为 k+1 的相同前缀和后缀。
构建 next 数组的步骤
  • 从模式串的第二个字符开始,依次计算每个字符的 next 值。
  • 使用两个指针 ij
    • i 用于遍历模式串,j 表示当前匹配前缀的长度。
    • 如果 needle[i] == needle[j],说明模式串前缀和后缀相等,next[i] = j + 1
    • 如果不相等,则通过回溯 next[j] 来找到一个更短的匹配前缀。
2. 利用 next 数组进行匹配
  • 主串(文本)与模式串进行字符逐个比较。
  • 当出现字符不匹配时,利用 next 数组找到模式串应该回退到的位置,继续比较后续字符,而不是从头开始比较模式串。
  • 当模式串全部匹配时,找到一次匹配。

示例:

流程

数组索引: 0 1 2 3 4 5 6 7 8 9
匹配目标: A B C A B C E A B C
匹配子串: A B C A B C D
next数组:-1 0 0 0 1 2 3

我们用一个非常经典的位置来看公共前后缀的问题,当我们的指针匹配到对应的索引为6的位置时(子串末尾位置)时,我们会发现在已有的基础上已经找到了一个最长前缀:ABC(索引:0~2) ,这里的前缀和后缀ABC(索引3~5)是相等的(或者说子串中有包含首字符的子串和子串中包含尾字符的子串相等存在相等的情况),而在与目标串进行比较时,因为索引为6的子串之前的字符和目标串的字符是匹配的,这也就意味着,如果我们在索引为6的位置的字符和目标串的字符不匹配时,我们可以直接跳转到索引为3的子串位置和目标串字符进行比较(因为后缀部分和前缀部分相等嘛,所以可以直接把前缀移到后缀的位置,并且不比较前缀,接着比较前缀后面的部分):

数组索引:0 1 2 3 4 5 6 7 8 9
匹配目标:A B C A B C E A B C
匹配子串:      A B C A B C D


通过上面的跳转,我们就会发现,子串索引为3的之前的字符和目标串的字符是匹配的,这样我们就可以继续直接将子串索引为4的位置向后进行匹配,这就是KMP算法的核心思想,通过next数组来记录当前字符不匹配时应该跳转到的位置,这样就可以减少不必要的比较,提高匹配的效率。

再举个例子:

数组索引: 0 1 2 3 4 5 6
匹配目标: A B C A B D E
匹配子串: A B C A B D A
next数组:-1 0 0 0 1 2 0

还举一个:

数组索引: 0 1 2 3 4 
匹配目标: A A A A D
匹配子串: A A A A B
next数组:-1 0 1 2 3 

通过例子尝试自己总结,这样会更加容易理解

注意

为什么必须是公共前缀和公共后缀呢?
本人也曾走过一些弯路,认为为什么一定要前缀和后缀呢?不可以是直接两个子串相等吗?这里我们可以通过一个例子来说明这个问题:

数组索引: 0 1 2 3 4 5 6 7 8 9
匹配目标: A B C A B D E A B C
匹配子串: A B C A B D A
next数组:-1 0 0 0 1 0 0


这里我们接着在原来的位置上匹配(索引为6)时,出现与目标串字符不相等的情况,这时我们如果按照子串相等的模式来看,存在前缀AB(索引0~1)和子串AB(索引3~4,这里不是后缀,因为没有包含索引为5的字符),如果我们将其看作匹配,那么也就意味着我们可以跳转到索引为2的位置,但是跳转后就会发现,索引为3的位置的字符和目标串的字符是不匹配的,而遍历匹配目标字符串的指针也不能够向后移动,所以也就更不需要谈索引为4处是否匹配的问题了,这样就会导致错误的结果,所以必须是公共前缀和公共后缀的情况下才能够进行跳转,这样才能够保证我们的匹配是正确的。

数组索引:0 1 2 3 4 5 6 7 8 9
匹配目标:A B C A B D E A B C
匹配子串:      A B C A B D A

求解next数组

上面的字符串匹配过程,我们就能够想到,该如何求解next数组呢?因为上面通过我们大脑能够快速计算到前缀和后缀相等的最长前后缀,但是计算机是不可能直接找到的,所以需要设计一套算法来求解这里的next数组,这里我们可以谈谈求解next数组的原理:
我们首先需要弄懂对于next数组的定义,next数组的定义是:next[i]表示在索引为i的字符不匹配时,应该跳转到的位置,所以,如果我们要求解前缀和后缀相等的最长前后缀,我们可以通过前一个字符的最长前缀后缀的位置来匹配当前位置的:
依旧是上面的例子:

数组索引: 0 1 2 3 4 5 6 7 8 9
匹配目标: A B C A B C E A B C
匹配子串: A B C A B C D
next数组:-1 0 0 0 1 2 3

首先我们在构建时,需要使用提前构建后一个数组元素的方式,因为在索引为0的位置没有对应的前缀,所以我们首先初始化next[0] = -1,在构建next[1]时,我们通过查看next[0]对应的公共前缀后一位字符是否和当前字符(公共后缀的后一位字符)相等,因为没有对应的前缀比较,所以我们就可以将next[1] = 0,这样以此类推,当我们构建到索引为2的位置时,判断当前索引的公共前缀的位置的后一位字符是否和当前字符相等,如果不相等,那么我们需要接着向前查找,获取当前公共前缀的位置后再次获取其公共前缀(有点嵌套套娃的意思在里面,即如果后一位不相等就不断循环获取其当前这位的公共前缀,因为其构建的特性原因,这里的前缀会逐渐变小,直到变为索引-1,或者是0),直到找到其后一位字符和当前字符相等的情况,这样就可以获得next数组的值。

可以总结为通过已有的next数组去查找公共前后缀的长度,如果没有找到,那么就继续查找前一个字符的最长前后缀的长度,直到找到相等的情况或者找到了第一个字符。有种递推的思想在里面。

代码

构建next数组

func getNext(needle string, next []int) {
	i, j := 0, -1 // 初始化指针 i 和 j,i 为遍历的当前字符位置,j 为前一个字符的最长前缀后缀的长度
	next[0] = -1  // 初始化 next 数组第一个位置为 -1,表示第一个字符没有前缀
	for i < len(needle)-1 { // 遍历模式串,构建 next 数组
		// 如果当前字符 needle[i] 和前缀字符 needle[j] 不相等,并且 j >= 0
		for j >= 0 && needle[i] != needle[j] {
			// 回溯到前一个位置,继续检查是否有公共前缀和后缀
			j = next[j] 
		}
		// 如果 needle[i] 和 needle[j] 相等,或者 j < 0(无公共前后缀)
		if j < 0 || needle[i] == needle[j] {
			// 公共前缀后缀长度增加 1
			j++
			// 当前字符位置后移
			i++
			// 将前缀后缀的长度保存到 next 数组中
			next[i] = j
		}
	}
}

找出字符串中第一个匹配项:

func StrMatch(haystack string, needle string) int {
	length := len(needle)       // 获取模式串的长度
	stackLength := len(haystack) // 获取主串的长度
	if length == 0 {            // 如果模式串为空,直接返回 0,表示匹配从头开始
		return 0
	}
	next := make([]int, length)  // 创建 next 数组用于存储模式串的部分匹配信息
	getNext(needle, next)        // 调用 getNext 函数构建 next 数组
	i, j := 0, 0                 // i 指向主串,j 指向模式串
	for i < stackLength && j < length && stackLength-i >= length-j { 
		// 只要 i 还在主串范围内,并且 j 还未遍历完模式串,就继续匹配
		if j < 0 || haystack[i] == needle[j] { 
			// 如果 j < 0(即 j 已经回溯到 -1,表示需要重新开始匹配),或者当前字符匹配
			i++ // 主串指针前进
			j++ // 模式串指针前进
		} else {
			// 如果字符不匹配,则根据 next 数组决定 j 的回溯位置
			j = next[j]
		}
	}
	// 如果 j 达到模式串的末尾,表示完全匹配
	if j == length {
		return i - length // 返回匹配的起始位置
	}
	return -1 // 如果没有找到匹配,返回 -1
}
  • getNext 函数:构建模式串的 next 数组,该数组记录模式串中前缀和后缀相同的部分信息。next[i] 表示在模式串的第 i 个位置失配时,模式串应跳回到的下一个位置。

  • StrMatch 函数:使用 KMP 算法在主串中查找模式串。主串和模式串进行字符逐个匹配,如果发现不匹配,就通过 next 数组跳过已经匹配的部分,避免重新从头开始匹配。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值