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

       今天要实现的这个算法是在复习串操作时出现的,之前看过一遍,但时隔久远,毫无印象,捡起来还有点儿困难,发现当时理解不是那么透彻,自己主要理解难点是这个算法如何求解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算法的讲解,如有疑问,欢迎留言!谢谢浏览!!!

  • 4
    点赞
  • 9
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值