赞
踩
参考文章:python之KMP算法(原理详解)_kmp算法代码python-CSDN博客
B站视频:【最浅显易懂的 KMP 算法讲解】https://www.bilibili.com/video/BV1AY4y157yL?vd_source=c8da04bb60a93f6134c95538c2559fb1
首先第一次接触到关于字符串匹配的问题,我第一时间想的就是python中的字符串查找index函数,比如说给你一个被查找字符串 s = 'ababcabcacbab' ,查找字符串 t = 'abcac',就可以实现如下操作:
- s = 'ababcabcac'
- t = 'abcac'
- print(f't出现的位置: {s.index(t)})
- ***
- t出现的位置: 5
- ***
如果题目上要求一些其他条件就可以在这个基础上进行改进,就可以在处理长字符串检索时加一些条件进行逐一匹配。
同样的,还有一种朴素的匹配算法:
- def search(s, t, m, n):
- i, j = 0, 0
- while i < m and j < n:
- if s[i] == t[j]:
- i += 1
- j += 1
- else:
- i = i - j + 1
- j = 0
- if j == n:
- return i - j
- return 0
-
-
- s = 'ababcabcac'
- t = 'abcac'
- print(f't出现的位置: {search(s, t, len(s), len(t))}')
-
- ***
- t出现的位置: 5
- ***
这两种方法原理其实都一样,都是通过逐一字符串匹配,进行目标检索,其时间复杂度非常大。因此,学习了一种减少时间复杂度的算法--KMP算法。
KMP算法(Knuth-Morris-Pratt算法)是一种用于在字符串中查找子串的高效算法。它的主要思想是利用已经部分匹配的信息来避免无效的回溯,从而实现更快的字符串匹配。
KMP算法的核心是构建一个部分匹配表(也称为Next数组),用于记录在匹配过程中已经匹配的部分,从而在匹配失败时可以快速地确定应该将模式串向右移动的位置。
具体来说,KMP算法的实现步骤如下:
- 构建部分匹配表(Next数组):遍历模式串,对于每个位置记录其前缀子串中的最长相等前缀后缀的长度。
- 匹配过程:在文本串中从左到右逐个字符地与模式串进行匹配。如果当前字符匹配成功,则继续比较下一个字符;如果匹配失败,则根据部分匹配表确定模式串向右移动的位置。
- 匹配成功:当模式串完全匹配文本串中的子串时,匹配成功,返回匹配的起始位置。
- 匹配失败:如果模式串未能完全匹配文本串,则根据部分匹配表确定模式串的移动距离,继续匹配。
KMP算法的时间复杂度为O(m+n),其中m为模式串的长度,n为文本串的长度。相较于朴素的字符串匹配算法,KMP算法避免了大量的重复比较操作,因此具有更高的效率。
正所谓,基础不牢地动山摇,next就是整个算法的基础。其中next数组的求法也涉及到了前后缀的求法。
next的本质就是一个前缀表,前缀表是一张表(或者直接理解为一个数组),表中记录的是字符串中的每个子串的最大相等前后缀的长度。举个例子:
假如有一个目标字符串 T :‘abcab’,其前缀表用next来表示:
1. 前一个是 a 无前后缀,长度为 0
2.前两个是 ab ,前缀为a ,后缀为b。前后缀不相等,长度也为 0。
3.前三个是 abc, 前缀为 a,ab;后缀为c,bc。前后缀不相等,长度为 0。
4.前四个是 abca, 前缀为a,ab,abc;后缀为a,ca,bca。前后缀最长相等长度为 1。
5.前五个是 abcab,前缀为a,ab,abc,abca;后缀为b,ab,cab,bcab。前后缀最长相等长度为 2。
所以可以得到next数组:
T | a | b | c | a | b |
---|---|---|---|---|---|
next | 0 | 0 | 0 | 1 | 2 |
既然我们得到了next数组,那么KMP算法已经掌握一大半了。next数组就决定着字符串匹配过程中指针回溯的位置。
那么接下来用代码实现如何求得next数组:
- def build_next(patt):
- next = [0] # next数组 初始值为0
- prefix_len = 0 # 当前共同前后缀的长度
- i = 1
- while i < len(patt):
- if patt[prefix_len] == patt[i]:
- prefix_len += 1
- next.append(prefix_len)
- i += 1
- else:
- if prefix_len == 0:
- next.append(0)
- i += 1
- else:
- prefix_len = next[prefix_len - 1]
-
- return next
-
-
- t = 'abcab'
- print(f'next: {build_next(t)}')
-
- ***
- next: [0,0,0,1,2]
- ***
- 首先建一个next数组,初始值为0,就是前一个a的前后缀长度。
- predix_len = 0,即为当前前一个a 的前后缀长度
- i = 1 ,初始指针为字符串第二个字符
- 接下来进行while 循环,直到指针指向最后一个为止
- 如果当前指针指向的字符patt[i] 和 patt[predix_len]相等,那么就说明 前 i+1 个字符的前后缀最长相等长度为 predix_len+1(比方说前俩字符为‘aa’,此时 i 指向第二个a,predix_len指向的是第一个,他俩相等,就说明aa的前后缀相等,分别为a,a。)所以next.append(predix_len+1),(next就变成了[0,1]),i + 1继续进行判断下一个字符的前后缀最长相等长度(这一步分比较抽象,大家学习的时候可以在本上画图演示一遍)
- 如果不相等的话,进入else语句里面,再进行①判断prefix_len是否为0,(比方说字符串为‘ab’,此时 i 指向b,predix_len指向a,他俩不相等,同时predix_len为 0 ,那么就是说这个字符串前后缀长度为0),如果为0,i + 1进行下一个判断;②如果不为0,predix_len变为前一位字符串的最大相等长度。
- 最后经过循环,返回next数组。
整个过程可能会有些抽象,接下来我用图表的格式来进行一个简单演示。
- def build_next(patt):
- next = [0] # next数组 初始值为0
- prefix_len = 0 # 当前共同前后缀的长度
- i = 1
- while i < len(patt):
- if patt[prefix_len] == patt[i]:
- prefix_len += 1
- next.append(prefix_len)
- i += 1
- else:
- if prefix_len == 0:
- next.append(0)
- i += 1
- else:
- prefix_len = next[prefix_len - 1]
-
- return next
-
-
- def kmp(target, patt):
- next = build_next(patt)
- i = 0
- j = 0
- while i < len(target):
- if target[i] == patt[j]:
- i += 1
- j += 1
-
- elif j > 0:
- j = next[j - 1]
- else:
- i += 1
- if j == len(patt):
- return i - j
-
- t = 'abcabc'
- s = 'asabdababcabc'
- print(f't 在 s 的 位置 {kmp(s,t)}')
-
- ***
- t 在 s 的 位置 7
- ***
整个KMP算法中,通过 匹配过程中省去重头开始匹配的时间,只需每次匹配失败时求出next中next[j-1]的值,使之与i对应再次进行匹配,直至匹配成功。
接下来,用图来演示整个流程:
经过查了可多资料,以及看了好多视频,开始看了好多理论知识,完全搞不懂,特别是求next的那一部分,但是看过那个图解kmp以后,感觉kmp算法也不是那么难。因此我也是,做了很多图解,能够更加清晰地了解kmp。
我觉得最难的还得是next的求法,如果这些图解也搞不懂,可以举个例子,在演草本上把每个流程写下来,写完一遍就会把这个算法吃透了。
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。