KMP 算法注解

KMP

暴力算法是在失配时,回溯子串指针,从头开始匹配。而 KMP 失配时尝试更有进取心地往前走,掠过那些不可能匹配的情况。往前移动的长度由一个「部分匹配表」来确定。

KMP

(图片来自 CLRS《Introduction to Algorithms, 3rd Ed》1

部分匹配表其实就是失配那个位置前面(匹配上的)里面最长的公共前后缀长度,移动就是要把前缀直接对上后缀那个位置,就略过了中间不可能匹配(已经扫过了,后面不匹配,再重复扫也还是不匹配)的那一段,直接开始尝试下一个可能的匹配。

部分匹配表

参考 Wikipedia 的算法 2(看英文页,中文页没写全,有些段落缺失)。

手算

func GetNext(substr: string) []int {  // substr: [0…M-1]
    M := len(substr)
    next := make([]int, M)

    next[0] = -1
    for j := 1; j <= M; j++ {
        next[j] = len(
            最长公共前后缀(substr[0:j])  // 就是 j 那个字符之前的
        )
    }

    return next
}

其中「最长公共前后缀」就是例如:

最长公共前后缀

(图片来自 阮一峰《字符串匹配的KMP算法》3

编程快速计算:

func GetNext(substr string) []int {
	next := make([]int, len(substr))
	next[0] = -1

	i, j := 0, -1
	for i < len(substr)-1 {
		if j == -1 || substr[i] == substr[j] {
			i++
			j++
			next[i] = j
		} else {
			j = next[j]
		}
	}
	return next
}

匹配

KMP:

func KMP(s string, substr string) int {
	next := GetNext(substr)

	i, j := 0, 0
	for i < len(s) && j < len(substr) {
		if j == -1 || s[i] == substr[j] {
			i++
			j++
		} else { // miss matching
			j = next[j]
		}
	}

	if j >= len(substr) { // matched
		return i - len(substr)
	}
	return -1 // not matched
}

这个程序是真的可以运行的:

package main

// ...

func main() {
	s := "BBC ABCDAB ABCDABCDABDE"
	sub := "ABCDABD"

	res := KMP(s, sub)
	println(res)
}

next 数组

补充一点:部分匹配表,还有几种不同的版本,比如《算法导论》1 上的,再比如我们的考研书(天勤王道)4 , ^, ,5上的。

(考研书上把部分匹配表亲切地称为 “next 数组”。。。)

这几种的区别看下面这个例子:

index0123456
charababaca
CLRS0012301
Wikipedia-1001230= CLRS 右移一个,左边补-1
天勤王道0112341= Wikipedia + 1

若在原串(大的)索引 i 处、模式(子串)索引 j 处失配:

  • CLRS 移动到 T[j-1] 上(特殊情况:j=0i+=1
  • Wikipedia 移到 T[j] 上(特殊情况:j=-1j+=1)。

更好理解的代码

说实话,我看不懂上面给的代码。主要是这段 if (j==-1 || s[i] == substr[j]) {...} WTF??

研究了好久才看懂明明就是两种不同的情况,只是凑巧处理方式一样就合并在一起,,搁这儿写汇编呢。。吐了。

参考 知乎上的回答 6 把这两种情况分开写就容易理解多了:

func GetNext(substr string) []int {
	next := make([]int, len(substr))
	next[0] = -1

	i, j := 0, -1
	for i < len(substr)-1 {
        if j == -1 { // 特殊: 到头了: 最小了,无可再缩
			i++
			j = 0
			next[i] = j
		} else if substr[i] == substr[j] { // 配上了:扩大一位
			i++
			j++
			next[i] = j
		} else { // 失配:缩小,重新匹配
			j = next[j]
		}
	}
	return next
}

func KMP(s string, substr string) int {
	next := GetNext(substr)

	i, j := 0, 0 // i 是 s 的,j 是 substr 的
	for i < len(s) && j < len(substr) {
        if j == -1 { // 特殊: 回到头了: i 移一个,j 从头比较
			i++
			j = 0
		} else if s[i] == substr[j] { // 配上了: 下一个字符
			i++
			j++
		} else { // 失配: j 移到下一个可能匹配的位置
			j = next[j]
		}
	}

	if j >= len(substr) { // matched
		return i - len(substr)
	}
	return -1 // not matched
}

改进 KMP

改进个🔨,要睡了。

考研书上的,用个 nextval 替代 next 数组。

nextval 构造规则如下:

nextval[0] = -1;
for (int j = 1; j < len(substr); j++) {
    nextval[j] = (substr[j] != substr[next[j]] ? 
        next[j] :
        nextval[next[j]]
    );
}

就是构建部分匹配表,回溯(缩小)的时候,如果回到的上一步 next[j] 的字符和当前字符相同,就不必一步步回溯了(之前已经走过了),直接借用之前的结果,快速回溯到头。

e.g.

substrABABAAB
j0123456
next-1001231
nextval-10-10-130

(记得考研书上索引从 1 开始,next、nextval 的值都要加一)

推荐阅读

还有下面参考文献的阮一峰,通俗易懂,看这篇手算就够了。Wikipedia 的也还行(一定要看英文页)。

有时间再啃 CLRS。

死背代码的话,天勤王道。

还有 https://leetcode-cn.com/problems/implement-strstr/solution/kmp-suan-fa-xiang-jie-by-labuladong/ 这篇写的也屌(这个大佬的公众号我关注了数年了),用动规(其实是 DFA)解释了 KMP,推荐阅读。

参考文献


  1. 【1】Cormen T H , Leiserson C E , Rivest R L , et al. Introduction to Algorithms, 3rd Ed. ↩︎ ↩︎

  2. 【2】Wikipedia. Knuth–Morris–Pratt algorithm. https://en.wikipedia.org/wiki/Knuth–Morris–Pratt_algorithm ↩︎

  3. 【3】阮一峰. 字符串匹配的KMP算法. http://www.ruanyifeng.com/blog/2013/05/Knuth–Morris–Pratt_algorithm.html ↩︎

  4. 【4】率辉主编. 数据结构高分笔记(2022版 天勤第10版). 机械工业出版社, 2020. ↩︎

  5. 【5】王道论坛组编. 2022年数据结构考验复习指导. 电子工业出版社, 2021 ↩︎

  6. 【6】阮行止. 如何更好地理解和掌握 KMP 算法?. https://www.zhihu.com/question/21923021/answer/1032665486 ↩︎

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值