KMP算法

文章出处:http://www.cnblogs.com/epsilon/archive/2013/01/16/KMP.html

今天,寒假第5天。

从昨天开始,就有些体会到内心的疲乏和懈怠了。因为重复而单调的生活很难让内心感到“爽快”,甚至在当今社会显得有些不合时宜。但这种生活也自有它的调味品,那些被人称为“有些挑战性”的算法,就能带来些欢欣、掀起些波澜。今天写的KMP算法就算是其中的一个。

上学期《数据结构》老师说到KMP时就一带而过,因为书上打了星号(还是双星呢**),当时也就没仔细看,但是KMP却在我心中树立起了“伟岸”的形象。今天终于一睹其尊容,不亦快哉?

要讲清楚KMP算法确非易事,这倒不是应为它有多复杂,其实从直观上看,KMP是相当简单的。但是要用准确的语言表述,就感觉有些抽象。

我们看一个书上的例子:

   (图1)

上面的串叫做“主串”,下面的串是用来和“主串”匹配的,叫做“模式串”。在比较过程中,在i=3,j=3时不匹配了。怎么办?一般的想法是:让i=2,j=1,再进行比较。

(图2)

但KMP算法告诉我们,不用这样。在KMP算法中i是不会走回头路的。我们看看它是怎么做的。

(图3)

KMP算法直接让模式串向右移动2位。我们此时可能并不理解为什么可以这样。我们不妨再看看KMP算法下一步怎么做。

(图4)

在第二趟的比较过程中,很“不幸”的,模式串的前四个都匹配了,但是第五个和主串出现了不同。那下一步怎么做呢?我们直观观察发现,我们把模式串向右移3位就可以了。

(图5)

我们的直观告诉我们,连图5中的j=1的a也不用比较了,直接从j=2开始就可以了(不知道你的直观是不是这样~如果不是,尝试着多看几遍图4)。为什么中间的步骤都可以省略呢?其实在运行到图4状态的时候,我们已经对主串有所了解了,那就是主串[3…6]和模式串[1…4]一定是一样的。其实,KMP算法的优势就在于利用了我们在匹配过程中,对主串形成的了解,这种了解就是模式串和主串有相同的地方。比如在运行到图4状态时,想要了解主串i=7之前的具体情况,模式串就可以回答我们。

在运行到图4之后,我们接下来该做什么呢?因为主串和模式串已经出现了不匹配,那我们就应该把模式串进行移动。常规的思路就是:把模式串向右移动1位,看看和主串是否匹配;若不匹配,就移动2位。。。直到匹配或是比较到最后。由于我们之前讲过,想要了解主串i=7之前的具体情况,模式串就可以回答我们。我们之所以能直接在图5中直接比较j=2,是因为相同是相同的(见图6划线部分)。

(图6)

是一定相同的,因为主串和模式串的这一位已经匹配,要想相同,只要相同即可。而这是模式串自存在起就已经决定的。我们是不是可以这样归纳(k的求法):对于某一个j,只需在模式串[1..j-1]中找到两个相同的子串: 模[1..k-1]和 [j-k+1..j-1](要保证k取到最大,这样才最有意义;在示例中k=2),然后我们继续比较主串的i和模式串的k就可以了。(仔细体会!)

对于模式串,它的每一位所对应的k都是确定的,示例中j=5时,k=2。我们可以用一个数组next[]来记录,其中next[j]=k。

KMP算法就是说这样,它在遇到不匹配后,不移动主串的i,而是将主串的第i位和模式串第k位继续比较。k则是在模式串确定后,计算好并保存在next[]数组中的

 

//到此处,你应该明白了KMP的基本原理,以及k怎么求。

 

对于刚才所说的内容,下面有一个数学证明:(基本抄课本上的)

定理:设主串为’s1s2…sn’,模式串为’p1p2。。。pn’。

如果主串中第i个字符和模式串中第j个字符失配,此时主串第i个字符应该与模式串第k(k<j)个字符继续比较,则模式串前k-1个字符,且不存在k>k满足下列关系式:

‘p1p2。。。pk-1’ = ‘si-k+1si-k+2…si-1

已经得到的匹配部分是:

‘pj-k+1pj-k+2。。。pj-1’ = ‘si-k+1si-k+2…si-1

联立上面两式,得到

‘p1p2。。。pk-1’ = ‘pj-k+1pj-k+2。。。pj-1

(就是说,在模式串中找到的k就是主串第i个字符应该与模式串继续比较的k。)

//证毕

 

下面说说怎么求next数组。下面是定义。

 

还是先看看课本上的例子。

(图7)

j==1时next[1]=0;j!=1时,手工算就是看看[1..j-1]最长的相同子串长度([1..k-1]和[j-k+2..j-1])。比如j=4时,前面最长的为[1]和[3],都是’a’,所以k=2。又如j=6时,最长的子串为[1..2]和[4..5],都是’ab’,所以k=3。

 

那怎么编程呢?如果对每个j都要这样一个个地算,是很费时间的。

我们发现,如果对于next[j]=k,那么next[j+1]则可以根据已经算出的值加以推算。

如果next[j]=k,则有:

‘p1p2。。。pk-1’ = ‘pj-k+1pj-k+2。。。pj-1

(1)   若pk=pj,则有

‘p1p2。。。pk’ = ‘pj-k+1pj-k+2。。。pj

也就是说,next[j+1]=k+1.

(2)   若pk!=pj,则有

‘p1p2。。。pk’ != ‘pj-k+1pj-k+2。。。pj

我们把这个模式串既看成主串,也看成模式串。相当于在比较到主串的j和模式串的k时,发现二者不匹配。根据前面说的,KMP算法会将主串的j和模式串的next[k]继续比较。

(图8)

不妨令k=next[k].

假如p[j]==p[k],那就是说:p[j]之前有:

‘p1p2。。。pk’’ != ‘pj-k’+1pj-k’+2。。。pj

(仔细体会)。

所以有:

next[j+1]=next[k]+1

假如p[j]!=p[k],那我们依次类推,令k=next[k], 看有没有[j]==p[k]成立.若不成立,重复这个操作…如果找不到这样的k(1< k<j),那么next[j+1]=1.

 

于是有下面的代码:

int GetNext(char *s,int next[])
{
    int i = 1,j = 0;
    next[1]=0;
    while(i<strlen(s)) {
        if(j==0 || s[i]==s[j]) {
            ++i;++j;
            next[i] = j;
        }
        else j = next[j];
    }
}

课本上最后对这个代码做了改进。

(图9)

如果next[j]=k,而模式串中p[k]==p[j],那么如果主串中si!=pj,就无需再比较si和pk,而直接可以比较si和pnext[k].

改进后的代码如下:

void GetNext(char t[],int nextval[])
{
    int i = 1,j = 0;
    nextval[1] = 0;
    while(i<t[0]) {
        if(j == 0 || t[i]==t[j]) {
            ++i,++j;
            if(t[i]!=t[j]) nextval[i] = j;
            else nextval[i] = nextval[j];
        } 
        else j = nextval[j];
    }
}

再回过头来,看KMP的主代码,相信已经可以看懂了。
int Index_KMP(char s[],char t[],int pos,int nextval[])
//字符数组第0位不存放数据 
{
    int i = pos, j = 1;
    while(i<strlen(s)&&j<strlen(t)) {
        if(!j || s[i]==t[j]) {
            ++i;++j;
        }
        else
            j = nextval[j];
    }
    if(j>=strlen(t)) return i-strlen(t)+1;
    else return 0;
}

已经写得很多了:)

若有疏漏、错误,欢迎批评指正,多谢!





<think>好的,用户想了解KMP算法的原理及实现。首先,我需要回顾一下KMP的基本概念。KMP算法主要用于字符串匹配,解决传统暴力匹配中的重复遍历问题。传统方法在主串和子串部分匹配时,主串指针会回退,导致效率低下。而KMP通过预处理子串生成next数组,利用已知信息避免不必要的回溯,提高效率。 接下来,我需要解释KMP的核心原理。关键点在于next数组,它记录了子串每个位置的最长公共前后缀长度。当发生不匹配时,根据next数组的值调整子串的位置,主串指针无需回退。例如,在子串"ABABC"中,每个字符对应的next值需要明确说明如何计算。 然后是实现部分。需要详细说明如何构建next数组,通常包括初始化、处理前后缀相同和不同的情况。然后描述匹配过程,主串和子串指针的移动逻辑,特别是当不匹配时如何利用next数组跳转。可能还需要一个具体的例子来演示流程,比如主串"ABABABABC"和子串"ABABC"的匹配过程。 还要注意时间复杂度的分析,KMP的预处理是O(m),匹配是O(n),总的是O(n+m),比暴力法的O(n*m)更高效。同时,可能需要对比传统算法,突出KMP的优势,尤其是在有大量部分匹配的情况下。 用户提供的引用资料中提到next数组的构造类似于KMP算法本身,这一点需要强调,可能在代码实现中体现。另外,引用5提到前缀数组的计算方法,这应该包含在next数组的构建步骤中。 需要确保回答结构清晰,分点讲解原理、next数组构建、匹配步骤、代码实现和示例。最后生成相关问题,帮助用户进一步学习,如优化、应用场景、对比其他算法等。 在代码实现部分,要使用Python示例,并确保语法正确。同时,数学公式如计算最长公共前后缀时可能需要用LaTeX表示,如最长公共前后缀长度公式,使用$...$格式。 最后检查是否满足用户的所有要求,包括中文回答、LaTeX格式正确、引用标注自然等。确保回答正确可靠,参考了提供的引用资料,并正确标注来源。</think>### KMP算法原理及实现 #### 一、核心原理 KMP算法通过预处理子串生成**next数组**(即前缀函数),利用已匹配的信息避免主串指针回退。核心思想是:当子串与主串不匹配时,根据next数组的值调整子串的起始位置,而非暴力匹配中的逐字符回退。 1. **next数组定义** next数组存储子串每个位置$i$的最长公共前后缀长度(不包含自身)。例如子串$P=ABABC$的next数组为: $$next = [-1, 0, 0, 1, 2]$$ 其中,$next[4]=2$表示前缀$AB$与后缀$AB$匹配[^5]。 2. **匹配过程优化** - 主串指针$i$始终单向移动,子串指针$j$根据next数组跳转。 - 当$P[j] \neq T[i]$时,子串向右滑动$j - next[j]$位,即$j = next[j]$[^2][^4]。 #### 二、next数组构建步骤 以子串$P$为例(索引从0开始): 1. 初始化$next[0] = -1$,$j = -1$(哨兵节点)。 2. 遍历子串$P$,若$P[i] = P[j]$,则$next[i+1] = j + 1$。 3. 若$P[i] \neq P[j]$,则递归回退$j = next[j]$直至匹配或$j = -1$[^4]。 #### 三、算法实现(Python) ```python def build_next(pattern): next_arr = [-1] * len(pattern) j = -1 for i in range(1, len(pattern)): while j >= 0 and pattern[i] != pattern[j + 1]: j = next_arr[j] if pattern[i] == pattern[j + 1]: j += 1 next_arr[i] = j return next_arr def kmp_search(text, pattern): next_arr = build_next(pattern) j = -1 for i in range(len(text)): while j >= 0 and text[i] != pattern[j + 1]: j = next_arr[j] if text[i] == pattern[j + 1]: j += 1 if j == len(pattern) - 1: return i - j # 返回匹配起始位置 return -1 ``` #### 四、示例分析 假设主串$T=ABABABABC$,子串$P=ABABC$: 1. next数组为$[-1, 0, 0, 1, 2]$。 2. 当匹配到$T[4]$(字符A)与$P[4]$(字符C)不匹配时,子串跳转至$j=next[4]=2$,继续匹配[^2]。 #### 五、时间复杂度 - 预处理next数组:$O(m)$ - 匹配过程:$O(n)$ - 总时间复杂度:$O(n + m)$,优于暴力法的$O(n \times m)$[^3]。 ---
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值