KMP 牛人讲解

KMP算法

我们从一个普通的串的模式匹配算法开始讲起,这样你才能更深入的了解KMP算法及其优点。
咱们先来看看普通的串的模式匹配算法是怎么进行比较的

主串 (S) a b a b c a b c a c b a b
子串 (T)a b c a c       (子串又被称为模式串)

红色表示当前这趟比较指针所在位置,兰色表示当前这趟比较中匹配的部分

第一趟(详细过程)

a b a b c a b c a c b a b
a b c a c


a b a b c a b c a c b a b
a b c a c


a b a b c a b c a c b a b
a b c a c
遇到不匹配的地方时指针回朔,子串向前移动一位(下同),变成如下形式

a b a b c a b c a c b a b
a b c a c

第二趟(省略了中间阶段指针移动比较过程,下同)

a b a b c a b c a c b a b
      a b c a c

第三趟

a b a b c a b c a c b a b
      a b c a
c

第四趟

a b a b c a b c a c b a b
           a b c a c

第五趟

a b a b c a b c a c b a b
               a b c a c

第六趟

a b a b c a b c a c b a b
               
a b c a c _
完成匹配,跳出

这就是普通算法的详细匹配过程,看明白了算法就简单了
详细算法我现在就不给了,等以后有时间再编辑。不过假如串的长度为m,子串的长度为n的话,那么这个算法在最坏的情况下的时间复杂度为O(m*n) ,有没有办法降低它的时间复杂度呢?(废话,当然有拉,不然回这个帖子干什么)

-----------------------------
拜D.E.Knuth 和 J.H.Morris 和 V.R.Pratt     所赐,我们有了一种时间复杂度为O(m+n)的算法,为了纪念这3位强人为计算机科学所做的贡献,分别取这3位先生的名字的首写字母K,M,P来命名这个算法,即著名的KMP算法。

我们先不管这个KMP算法是什么,我们先来看看我们能够想到怎样的方法来改进上面的普通算法。

通过观察,我们发现一个问题,如果一个子串,假设为a b c d e f , 兰色的部分与主串匹配, 红色的f 与主串不匹配,那么这个子串最多能往右边移动几位呢?因为子串中的第一个字符 a!=b !=c !=d !=e !=f ,那么主串中与b c d e 匹配的部分肯定和a不匹配而与
f不匹配的那部分无法判断与a是否匹配。因此子串最多向右移动5位,即a移动到f所在的位置上,再进行判断。

在解决这个问题的同时,我们又发现了一个问题,当这个子串为a b c d a f 时又会如何呢?因为无法判断主串中与a匹配对应的位置开始往后的部分是否与整个子串相匹配,所以子串最多向右移动4位,即a移动到a的位置上,然后再进行判断。

按照这个方法,我们再来看看前面我们举的例子。

第一趟

a b a b c a b c a c b a b
a b c a c

第二趟

a b a b c a b c a c b a b
      a b c a c

第三趟

a b a b c a b c a c b a b
               a b c a c _
完成匹配,跳出

是不是简单多了呢?这个方法不困难吧,也很容易理解吧。
事实上这就是很多人大叫困难的KMP算法的最基本也是最核心的方法

你现在是不是在想 如果我早生50年,KMP算法就要改名了呢?

当然,强人们的思维比我们严密多了,他们考虑的更完全,比如象 a b c d e a b c     ,这种字符串相同的情况,当然这种情况并不比单个字符相同的情况复杂多少。在思想上KMP算法与上面我们讲的方法完全一致,都是要让子串向右移动尽可能远的一段距离来达到降低时间复杂度的目的,但在具体操作上KMP算法与上面的方法又有所不同。他们为子串引入了一个参数next[ j ] ,我们先来讲下next[j] 怎么求:

假设子串为 'p1 p2 p3 p4.......pm '
对于第j个字符,有next[ j ] =  
(1)      0       (j=1)
(2)      Max{ k | 1<k<j     &&     ' p1...p k-1 '     = ' p j-k+1...p j-1 ' }
(3)      1       其他情况

没有大括号就是不方便。哎,上面的算式是不是把你弄晕了呀?没关系,下面我介绍一种偷懒的方法

我就以

子串        a b a b a a b a b
next[j]      0 1 1 2 3 4 2 3 4

为例 来讲下
next[j] 里面 开头的红色01 是固定格式.

我就以兰色的4 来说明下为什么是4.
与4有关的, 是4所对应的兰色a之前的所有的字符,即紫色的 a b a b a
这个字符串中所有符合匹配条件的字符串如下
a b a b a                a b a b a
a
                                         a
a b                                   b a (不符合,舍去)                                                                                       

a b a                             a b a       最长的匹配字符串(a b a b a本身除外)在这里 ,长度为3, 再加上1,就是4
注意next[ j ]中4 的位置!
next[ j ]中其他位置的数值也可以用同样的方法得出

为什么next[j]开头为固定的01?这个问题不太好回答。
最开头的next[j]是由子串第一个字符前面的字符来决定的,但那不存在,因此第一个next[j]的值为0
第二个next[j]是由第一个字符决定的,由于他是自己与自己比较,一定相等,所以值为1。
这样说可能有点牵强。那么我们来看看下面,可能会明白些,也更加了解next[j]到底是起什么作用的。

子串                      a b a b a a b a b
next[j]                   0 1 1 2 3 4 2 3 4
匹配子串长度       1 2 3 4 5 6 7 8 9      (对应位置     也可看成是截止到当前字符的子串串长)
最大右移              1 1 2 2 2 2 5 5 5      (当前字符不匹配时,子串最大右移距离)

我们发现,最大右移 + 对应的next[j] = 匹配子串长度,我估计这就是为什么要引入next[ j ] 这个参量,为了方便计算最大右移。不过对于next[j]的官方具体定义我没找到,望高人相告,在下先谢过了。

事实上,KMP算法还是有改进余地的,我们一直都在避免讨论这样一种情况:

a b c d e a
0 1 1 1 1 1
a不匹配时,能向右最大移动几位呢?答案是6位,a 所能移动到的位置为a的下一位,即a 的右边一位
而根据next[j]计算出子串最大右移为6-1=5。这里他将a移动到a的位置上,然后做了一次无意义的比较(a = a     ,a 与主串不匹配,那么显然a 与主串也不匹配)。这个问题我就不说了,虽然我有个改进方法,但这个问题还是留给大家去思考吧。

<think>嗯,用户想了解KMP算法,并且希望用Python来讲解。首先,我需要回忆一下KMP算法的核心思想。KMP算法主要用于字符串匹配,相比暴力匹配,它通过预处理模式串来避免重复比较,提高效率。关键点在于部分匹配表(也就是前缀函数或者next数组),这一步可能需要详细解释。 接下来,用户可能需要知道为什么需要KMP算法。暴力匹配的时间复杂度是O(mn),而KMP是O(m+n),这在处理大文本时优势明显。所以,我应该先对比一下暴力匹配和KMP的区别,突出其优势。 然后,部分匹配表的构建是关键。这部分可能比较抽象,需要举例说明。例如,对于模式串“ABABC”,如何生成next数组。可能需要分步骤解释每个位置的最长公共前后缀长度,以及如何利用这些信息在匹配失败时跳过不必要的比较。 用户可能对如何将理论转化为Python代码感兴趣。所以,我需要分步骤讲解如何构建next数组,然后如何利用这个数组进行匹配。代码部分需要简洁,同时要有注释,方便理解。另外,要注意处理边界情况,比如模式串为空或者文本比模式串短的情况。 另外,用户可能对KMP的实际应用场景不太清楚,可以举一些例子,比如文本编辑器中的查找功能或者DNA序列匹配等,说明其应用价值。 在解释过程中,要避免使用过于专业的术语,尽量用通俗的语言。同时,可能需要用例子来演示算法的执行过程,比如用具体的文本串和模式串,逐步展示匹配过程,以及next数组如何发挥作用。 还需要检查代码的正确性,确保没有语法错误,并且逻辑正确。比如,构建next数组时,双指针的移动是否正确,匹配过程中索引的处理是否恰当。可能需要用几个测试用例验证代码的正确性,比如模式串出现在文本开头、中间、末尾,或者多次出现的情况。 最后,总结KMP算法的优缺点,以及适用场景,帮助用户全面理解。可能还要指出,虽然KMP算法理论复杂度低,但在实际应用中,如果模式串不长或者文本串不大,暴力匹配可能更简单直接,而KMP在预处理上的开销是否值得需要根据具体情况判断。</think>### KMP算法核心思想 KMP算法(Knuth-Morris-Pratt)是一种**字符串匹配算法**,通过预处理模式串生成`next`数组(部分匹配表),在匹配失败时利用`next`值跳过不必要的字符比较,将时间复杂度优化至$O(m+n)$($m$为模式串长度,$n$为文本串长度)。 --- ### 核心概念:next数组 `next[i]`表示模式串`P[0..i]`中**最长公共前后缀**的长度。 **示例:** 模式串`P = "ABABC"`的`next`数组计算如下: $$ \begin{aligned} P[0] &= "A" &\rightarrow next[0] &= 0 \\ P[0..1] &= "AB" &\rightarrow next[1] &= 0 \\ P[0..2] &= "ABA" &\rightarrow next[2] &= 1 \quad (\text{前缀"A"和后缀"A"}) \\ P[0..3] &= "ABAB" &\rightarrow next[3] &= 2 \quad (\text{前缀"AB"和后缀"AB"}) \\ P[0..4] &= "ABABC" &\rightarrow next[4] &= 0 \end{aligned} $$ --- ### Python实现步骤 #### 1. 构建next数组 ```python def build_next(pattern): next = [0] * len(pattern) j = 0 # 前缀末尾指针 for i in range(1, len(pattern)): # i是后缀末尾指针 while j > 0 and pattern[i] != pattern[j]: j = next[j-1] # 回退到前一个匹配位置 if pattern[i] == pattern[j]: j += 1 next[i] = j return next ``` #### 2. 匹配过程 ```python def kmp_search(text, pattern): if not pattern: return 0 # 空模式直接匹配 next = build_next(pattern) j = 0 # 模式串指针 for i in range(len(text)): while j > 0 and text[i] != pattern[j]: j = next[j-1] # 根据next回退 if text[i] == pattern[j]: j += 1 if j == len(pattern): return i - j + 1 # 返回匹配起始位置 return -1 # 未找到 ``` --- ### 实例演示 **输入:** ```python text = "ABABABCABAABABABCA" pattern = "ABABC" ``` **执行过程:** 1. 生成`next = [0, 0, 1, 2, 0]` 2. 当匹配到`text[4]`时发现`'A' ≠ 'C'` 3. 根据`next[3]=2`回退到`pattern[2]`继续匹配 4. 最终在`text[2:7]`找到完整匹配 --- ### 关键优势 - **跳过无效比较**:通过`next`数组避免回溯文本指针 - **时间复杂度**:预处理$O(m)$ + 匹配$O(n)$,总体$O(m+n)$ - **空间复杂度**:$O(m)$(存储`next`数组) --- ### 应用场景 - 大文本中的关键字搜索 - 生物信息学中的DNA序列匹配 - 代码编辑器/IDE的查找替换功能 实际使用时可直接调用Python标准库`re`模块,但理解KMP原理对优化特殊场景的字符串处理至关重要。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值