关于两个字符串的kmp比对算法

关于两个字符串的kmp比对算法

假设有字符串X和Y,满足len(X)>len(Y),要比对这两个字符串。

我们知道,最朴实的方法,就是现将二者对齐,然后依次比对对应位置的字符。如果能匹配到Y最后位置,则匹配成功;如果匹配失败,则将Y右移一位,再从头进行匹配。

设字符串X为dababeabafdababcg;字符串Y为ababc。

这种比对方法如下所示:

起始时,二者对其,第一个字符不匹配

:|
:dababeabafdababcg
:ababc

右移一位,比对位置移动到Y起始位置

: |
:dababeabafdababcg
: ababc

连续成功4次,再次遇到不匹配

:     |
:dababeabafdababcg
: ababc

右移一位,比对位置移动到Y起始位置

:  |
:dababeabafdababcg
:  ababc
:

不断重复该过程,直到…………

:               |
:dababeabafdababcg
:           ababc

Y完整匹配到X,结束。

毫无疑问,这种方法太笨了。时间复杂度高达O(mn)。最大的问题是,进行了大量的重复比对工作。

kmp算法正是为了解决这一点而提出的。kmp算法的中心思想是,已经匹配的部分不需要再次去匹配,相反,应根据已匹配的部分进行多字节的挪动,加快匹配速度。

怎么做呢?kmp给出的答案是:

已经匹配的部分,可以说是已知的,而且是只取决于字符串Y的,对字符串Y的挪动可以直接挪动到下一个满足匹配的位置。

什么叫下一个满足匹配的位置?

假设字符串X和字符串Y已经匹配的部分为abcab,显然:

: abcab...  : abcab...   :
:  abcab... :   abcab... :

都是不匹配的,只有如下的

:abcab...
:   abcab...

才匹配。显然,新的匹配序列,是旧的匹配序列的一个真后缀,而且还是字符串Y的一个前缀;而旧的匹配序列也是字符串Y的前缀。

也就是说,根据已经匹配的部分,我们已经可以排除大量的位置了。kmp算法正是基于这一原理实现的。

规则1
如果当前比对位置两个字符相同,则比对位置右移1位
规则2
如果当前比对位置两个字符不同,则:

如果没有匹配到序列,则比对位置右移1位,字符串Y右移1位
如果已有匹配到序列,则移动字符串Y到下一个匹配位置

如下例,起始

:|
:dababeabafdababcg
:ababc

字符不匹配,比对位置和字符串Y都右移一位

: |
:dababeabafdababcg
: ababc

字符匹配,比对位置右移1位

:  |
:dababeabafdababcg
: ababc

连续成功匹配4次,再次遇到不匹配

:     |
:dababeabafdababcg
: ababc

字符不匹配,挪动字符串Y到下一个匹配位置

:     |
:dababeabafdababcg
:   ababc

仍旧不匹配,继续挪动字符串Y

:     |
:dababeabafdababcg
:     ababc

仍旧不匹配,已经没有匹配序列了,比对位置和字符串Y都右移一位

:      |
:dababeabafdababcg
:      ababc

字符匹配,比对位置右移1位

:       |
:dababeabafdababcg
:      ababc

连续成功匹配3次,再次遇到不匹配

:         |
:dababeabafdababcg
:      ababc

字符不匹配,挪动字符串Y到下一个匹配位置

:         |
:dababeabafdababcg
:        ababc

仍旧不匹配,继续挪动字符串Y

:         |
:dababeabafdababcg
:         ababc

字符不匹配,无匹配序列,比对位置和字符串Y都右移一位

:          |
:dababeabafdababcg
:          ababc

字符不匹配,无匹配序列,比对位置和字符串Y都右移一位

:           |
:dababeabafdababcg
:           ababc

字符匹配,比对位置右移1位

:            |
:dababeabafdababcg
:           ababc

连续成功匹配5次,到达字符串Y终点,匹配成功

:               |
:dababeabafdababcg
:           ababc

由此可见,kmp算法的比对位置没有倒车的情况,而且同一位置比对的次数也明显少于朴实算法,比对速率是相当快的。

kmp算法的关键,是根据已有匹配序列计算下一个匹配序列,而这一部分,是只需要字符串Y就可以实现的,因为下一匹配序列的计算和已有匹配序列之外的部分,无关。

事实上,根据已有匹配序列,完全可以得到多个满足要求的下一级匹配序列(即既是已有匹配序列的真后缀,又是字符串Y的前缀),但是毫无疑问,我们应该选择最长的那个。

kmp算法的关键,也就是对根据已有匹配序列计算下一个匹配序列这一个步骤的计算了,因为我们知道下一匹配序列必然是字符串Y的前缀,实际上只需要记录下一匹配序列的长度即可。这也就是所谓的next数组的真面目。(其他人介绍的next数组的值可能和我说的不同,但实际上都是对下一匹配长度进行数学变换的结果)

那么关于next数组的计算,自然也有多种方法,比如很朴实的方法,我就不多说了。事实上,有个很好的方法可以轻松的计算出next数组。这个方法和kmp本身比对的过程有些类似,同时也和后缀树构树的ukkonen算法有异曲同工之妙。

go代码如下:

// 成员n[i]表示既是s[:i]真后缀又是s前缀的最长序列的长度。
// 真后缀指非自身的非空后缀。如不存在,则置该成员值为0。
func next(s string) []int {
   n := make([]int, l)
   // 从n[2]开始算起
   for i, j := 2, 0; i < l; {
       // 已知前面的字符全部匹配
       if s[i-1] == s[j] {
           j++
           n[i] = j
           i++
           continue
       }
       if j == 0 {
           // 申请的n会全部初始化为零
           i++
           continue
       }
       // 类似后缀指针的功用
       j = n[j]
   }
   return n
}

我们每一次循环的时候,只进行了一个字符的比对!这是因为之前的比对已经保证前面的部分是匹配的。那么如果这一次比对成功,只需要延长下一次匹配的长度就行了;如果比对失败,我们也不需要从头开始去查找下一个匹配的起始位置,因为之前的匹配结果已经告诉了我们下一个匹配的位置。

结果是,kmp算法构造next数组和比对的过程,都很迅速!

kmp算法完整代码如下:

// kmp字符串搜索算法
func KMP(s, r string) int {
   l := len(r)
   // 成员n[i]表示既是s[:i]真后缀又是s前缀的最长序列的长度。
   // 真后缀指非自身的非空后缀。如不存在,则置该成员值为0。
   n := func(s string) []int {
       n := make([]int, l)
       // 从n[2]开始算起
       for i, j := 2, 0; i < l; {
           // 已知前面的字符全部匹配
           if s[i-1] == s[j] {
               j++
               n[i] = j
               i++
               continue
           }
           if j == 0 {
               // 申请的n会全部初始化为零
               i++
               continue
           }
           // 类似后缀指针的功用
           j = n[j]
       }
       return n
   }(r)
   // 进行搜索
   i, j := 0, 0
   for i+l < j+len(s) && j < l {
       if s[i] == r[j] {
           i, j = i+1, j+1
           continue
       }
       if j == 0 {
           i++
       } else {
           j = n[j]
       }
   }
   if j == l {
       return i - l
   }
   return -1
}

kmp算法是一个相当精巧,但是代码却非常简单的算法。相比之下,虽然速度可能逊色于BM算法,但是确实优美得多。

时间: 2024-10-24 12:01:48

关于两个字符串的kmp比对算法的相关文章

获取两个字符串全部公共的子串算法

应用场景: 获取两个字符串全部公共的子串. 思路: 1. 先获取两个子串的交集 2. 遍历交集子串,从最短子串到最长子串 public static List<String> getAllCommonSubStrings(String str1, String str2) { //TODO null check. String longString = str1; String shortString = str2; if(str1.length() < str2.length()){

获取两个字符串所有公共的子串算法

应用场景: 获取两个字符串所有公共的子串. 思路: 1. 先获取两个子串的交集 2. 遍历交集子串,从最短子串到最长子串 public static List<String> getAllCommonSubStrings(String str1, String str2) { //TODO null check. String longString = str1; String shortString = str2; if(str1.length() < str2.length()){

求两个字符串最大子串的lcs算法

/************************************************************************* > File Name: lcs.c > Author: dingzhengsheng > Mail: [email protected] > Created Time: 2015年05月20日 星期三 16时07分50秒 > Version: v0.01 > Description: > History: ****

计算两个字符串的相似度---动态规划实现

问题描述:把两个字符串变成相同的基本操作定义如下:1.     修改一个字符(如把 a 变成 b)2.     增加一个字符 (如 abed 变成 abedd)3.     删除一个字符(如 jackbllog 变成 jackblog)针对于 jackbllog到jackblog 只需要删除一个或增加一个 l 就可以把两个字符串变为相同.把这种操作需要的次数定义为两个字符串的距离 L, 则相似度定义为1/(L+1) 即距离加一的倒数.那么jackbllog和jackblog的相似度为 1/1+1

字符串模式匹配KMP算法中的next数组算法及C++实现

一.问题描述: 对于两个字符串S.T,找到T在S中第一次出现的起始位置,若T未在S中出现,则返回-1. 二.输入描述: 两个字符串S.T. 三.输出描述: 字符串T在S中第一次出现的起始位置,若未出现,则返回-1. 四.输入例子: ababaababcbababc 五.输出例子: 5 六.KMP算法解析: KMP算法分为两步,第一步是计算next数组,第二步是根据next数组通过较节省的方式回溯来比较两个字符串. 网络上不同文章关于next数组的角标含义略有差别,这里取参考文献中王红梅<数据结构

字符串匹配算法KMP详细解释——深入理解

1. 前言 字符串匹配是一个经典算法问题,展开来讲各类问题多达几十种,有名称的算法也不下三十种,所以需要深入学习的东西有很多.这次我们来探讨一个最简单的问题,假设现在随机输入一个长度为m的主串T,另外输入一个长度为n(n≤m)的字符串P,我们来判断字符串P是否是主串T的一个子串(即能否从T中随机取出与P同长的一段字符串,与P完全匹配). 2. 蛮力匹配法 问题很简单,当然也有最直接.最直观也是最好想到的方法,蛮力串匹配.即两个字符串像物流传送带一般,主串固定,子串一步步像前移动,一位位匹配比较,

[小明学算法]6.字符串匹配算法---KMP

1.简介  字符串匹配就是看看那字符串b是不是字符串a的子串.常用的Knuth-Morris-Pratt 算法,又称KMP算法. 2.主要思想 当patter在某一位置与string匹配失败时,我们除了知道从string的这个位置进行匹配失败这个结果外,是否可以从前面的匹配中获得更多的信息呢.即当前匹配点匹配失败之后,向右滑动的距离是可以提前计算出来的. 3.举例 abcabcabcdef   --------- string abcabcdef         --------- patter

【字符串】KMP

Algorithm Task 给定一个文本串 \(S\) 和一个模式串 \(T\),求 \(T\) 在 \(S\) 中出现的所有位置. Limitations 要求时空复杂度均为线性. Solution 回头重新学一遍看毛片 KMP 算法. 设 \(X\) 是一个字符串,则以下表述中,\(X_u\) 代表 \(X\) 的第 \(u\) 个字符,\(X_{u \sim v}\) 代表 \(X\) 的从 \(u\) 起到 \(v\) 结束的字串. 首先定义一个字符串的公共前后缀为这个字符串的一个 \

JavaScript基础 使用+号连接两个字符串

镇场诗: 清心感悟智慧语,不着世间名与利.学水处下纳百川,舍尽贡高我慢意. 学有小成返哺根,愿铸一良心博客.诚心于此写经验,愿见文者得启发.------------------------------------------ code: 1 <!DOCTYPE html> 2 <html> 3 <head> 4 <meta http-equiv="Content-Type" content="text/html; charset=ut