您的位置:首页 > 编程语言 > C#

C#算法系列(11)——KMP模式匹配算法

2017-12-15 16:58 901 查看
今天要实现的这个算法是在复习串操作时出现的,之前看过一遍,但时隔久远,毫无印象,捡起来还有点儿困难,发现当时理解不是那么透彻,自己主要理解难点是这个算法如何求解next数组。明白之后,发现它也并不难理解,就是有些资料的术语起了误导的作用,下面会按照小白的思路进行一系列说明,力求道明它的本质。

一、KMP算法背景

主要是要解决求解子串在主串中第一次出现位置的问题,KMP的提出能使得这个求解过程效率得到提升。下面看一下使用串的基本操作来实现这个功能。代码如下:

//若主串S中第pos个字符之后存在与T相等的子串
//则返回第一个与子串T相同的在主串S中的位置,否则返回0
public int IndexOfStr(string S, string T,int pos)
{
int n = S.Length, m = T.Length, i = pos;
string sub;
if (pos >= 0)
{
while (i <= n - m + 1)
{
//取出主串第i个位置长度与T相等子串给sub
sub = S.Substring(i,m);
if (sub != T)
++i;
else
return i;
}
}
return -1;
}


上述代码使用了,求字符串长度、字符串取子串以及字符串比较等基本操作。下面将单纯用数组来实现同样的功能,这种被称为朴素的模式匹配,主要思路如下:将子串的字符挨个与主串的字符比较,若相同,则进行下一个字符比较;若不相同,则主串的迭代变量回退到上次能够匹配的下一位置,而子串的迭代变量回到首位。代码如下:

/// <summary>
/// 返回子串在主串中的索引
/// </summary>
/// <param name="s">主串</param>
/// <param name="t">子串</param>
/// <returns></returns>
/// 最坏的时间复杂度为O((n-m+1)*m)
public int IndexString(string s, string t)
{
int i=0,j=0;//i,j,分别指向S,T串中,当前下标位置
//j<t.Length的作用在于只检测一次子串,即返回第一个与主串发生匹配
while (i < s.Length && j < t.Length)
{
//挨个字符匹配
if (s[i] == t[j])
{
++i;
++j;
}
//不匹配,则i,j回退
else
{
i = i - j + 1;//i退回到上次匹配首位的下一位
j = 0;//j退回到子串T的首位
}
}
if (j >= t.Length)
return i - t.Length;//返回子串在主串中的第一个起始位置
else
return -1;
}


但是,i = i-j+1这样回退,会导致出现重复匹配的过程,导致效率降低。比如:主串S为”abcdefgab…”, 子串T为“abcdex”,当主串i=2、3、4、5、6(下标从1开始)时,主串的首字符与子串的首字符均不等。仔细观察发现,对于要匹配的子串T来说,”abcdex”首字母“a”与后面的串“bcdex”中任意一个字符都不相等。若子串的前五位与主串的前五位相等,如步骤1,意味着子串T的首字符“a”不可能与S串的第2位到第5位的字符相等,因此上图2,3,4,5步判断就是多余的,因此主串的迭代变量是可以保持不变。



上面的重复性是主串的迭代变量重复,下面在来看另外一个例子,主串S=”abcababca”,子串T = “abcabx”。对于开始的判断,前5个字符完全相等,第6个字符不等。此时,根据刚才的经验,T的首字符“a”与T的第二位字符“b”、第三位字符“c”均不等,所以不需要做判断,因此当主串i=2,3时,判断就是多余的。关键的地方来了,T的首位“a”与T的第四位“a”相等,第二位“b”与第五位“b”相等。而在步骤1时,T串的第四位“a”与第五位“b”已经与主串S中相应位置比较过了是相等的,因此可以断定,T首字符“a”、第二位字符“b”与S的第四位字符和第五位字符也不需要比较,肯定也相等了,所以步骤4,5这两步也可以省略。



KMP模式匹配就是为了减少这种没必要的重复性比对操作。因此在保持主串的迭代变量i值不回溯,也就是不可以变小,则需要变化的就是子串的迭代变量j值。j值的变化也是有规律的。j值的多少取决于当前字符的串的真前后缀的相似度。于是,j值的变化定义了一个数组next来进行描述了,那么next的长度的长度就是T串的长度。next数组函数定义如下:



二、next数组值推导

以子串“ababaaaba”为例,有如下表:



当j=0时,next[0] = 0(此时下标从0开始);当j=1,2时,next[1]=next[2]=1(属于其它情况);

当j=3时,j有0到2的串是,“aba”,前缀字符“a”,与后缀字符“a”相等,next[3]=1+1=2;

当j=4时,j有0到3的串是,“abab”,前缀字符“ab”,与后缀字符“ab”相等,next[4]=2+1=3;

当j=5时,j有0到4的串是,“ababa”,前缀字符“aba”,与后缀字符“aba”相等,next[5]=3+1=4;

当j=6时,j有0到5的串是,“ababaa”,前缀字符“ab”,与后缀字符“aa”不相等,next[6]=1+1=2;

当j=7时,j有0到6的串是,“ababaaa”,只有前缀字符“a”,与后缀字符“a”相等,next[7]=1+1=2;

当j=8时,j有0到7的串是,“ababaaab”,只有前缀字符“ab”,与后缀字符“ab”相等,next[8]=2+1=3;

因此,我们可以根据经验得到,如果前后缀有n个相等的k值,就是n+1。这里的前后缀为真前后缀,即不包括自身的子串

三、KMP算法实现过程

KMP算法的提出就是解决这种重复匹配的过程。也是在上述朴素模式匹配算法的基础上修改得到的,下来面看下next的实现。

(1)实现得到next数组,代码如下:

//计算next数组
//next数组含义,子串的最长前缀和最长后缀相同的长度+1(+1的原因,在于相同的字符串的下一个位置)
//next数组的每个值,即是对应位置下次往前移动的距离
private void GetNext(string T,int[] next)
{
int i = 0, j = 0; //i为串T的迭代时的下标,j为子串的相同部分的数量+1
next[0] = 0;
while ((i+1) < T.Length)
{
//T[i]表示后缀的单个字符,T[j]表示前缀的单个字符
if (j == 0 || T[i] == T[j-1])
{
++i;
++j;
next[i] = j;
}
//当前字符比较不同,j根据next表值进行回溯
//回溯的目的是为了找到上一次相同的字符位置,来确定当前i位置对应的n
else
//此时的j值,在上一轮判断中自动后移了,而next数组对应的位置从0开始,因此下标需要减1;若next数组从1开始,则此处不需要减1
j = next[j-1];
}
}


再来看下代码中是如何实现回溯的,过程如下图:



看到这里的时候,不由得感慨写出这段代码的人编程内功深厚,佩服佩服!!!以上纯属个人感概,不喜勿喷,谢谢^_^.下面我们在再看看下子串在主串位置的主逻辑,代码如下,与朴素模式匹配基本一致。

public int Index_KMP(string S,string T)
{
int i = 0, j = 0; //i为主串的迭代变量,j为子串迭代变量
int[] next = new int[T.Length];
//计算next数组
GetNext(T,next);
while (i < S.Length && j < T.Length)
{
//两字母相等则继续
if (j == 0 || S[i] == T[j])
{
++i;
++j;
}
//指针后退重新开始匹配
else
{
//j根据next数组回退到合适的位置,i值不变
//此时的j值,在上一轮判断中自动后移了,而next数组对应的位置从0开始,因此下标需要减1;若next数组从1开始,则此处不需要减1
j = next[j-1];
}
}
if (j >= T.Length)
return i - T.Length;
else
return -1;
}


此时,上述代码就是去掉了i值回溯的部分,其余与朴素算法一样。对于get_next函数来说,若T的长度为m,因此只设计到简单的单循环,时间复杂度为O(m),Index_KMP的i值不回溯之后,while的时间复杂度为O(n)。因此,整个算法时间复杂度为O(n+m)。相比较朴素模式匹配算法O((n-m+1)*m)来说,是要好些。这里需要强调的是,KMP算法仅当模式与主串之间存在许多“部分匹配”的情况下才会体现出它的优势,否则二者差异不明显

四、KMP算法改进

改进其实也是针对于next数组改进。目前还存在如下问题:



这当中的第2,3,4,5步骤,其实都是多余的。由于T串的第2,3,4,5位置的字符都与首位的“a”相等,那么可以用首位next[0]的值去取代与它相等的字符后续next[j]的值。于是GetNext函数代码修改后如下:

//对上述计算next移动数组的优化
private void GetNextVal(string T, int[] next)
{
int i = 0, j = 0;
next[0] = 0;
while ((i + 1) < T.Length)
{
if (j == 0 || T[i] == T[j - 1])
{
++i;
++j;
//若当前字符与前缀字符不同
if (T[i] != T[j - 1])
//当前j为next在i位置上的值
next[i] = j;
else
next[i] = next[j - 1];
}
else
j = next[j - 1];
}
}


五、实验结果

主函数测试代码如下:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace KMP_模式匹配
{
class Program
{
static void Main(string[] args)
{
string s = "googlegood";
string t = "google";
Console.WriteLine("字符串t:"+t+"在字符串s:"+s+"中的位置在第几个?");
//传统方法
int pos1 = KMP_Test.Instance.IndexOfStr(s,t,0);
Console.WriteLine("传统方法求解:" + (pos1 + 1));
//朴素的模式匹配(即未优化匹配项)
int pos2 = SimpleKMP.Instance.IndexString(s,t);
Console.WriteLine("朴素模式匹配求解:" + (pos2+1));
//KMP算法
int pos3 = KMP_Test.Instance.Index_KMP(s,t);
Console.WriteLine("KMP算法求解:" + (pos3 + 1));
Console.ReadKey();
}
}
}


实验截图:



以上,就是本次KMP算法的讲解,如有疑问,欢迎留言!谢谢浏览!!!
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签:  算法 kmp c#