C#算法系列(11)——KMP模式匹配算法
2017-12-15 16:58
901 查看
今天要实现的这个算法是在复习串操作时出现的,之前看过一遍,但时隔久远,毫无印象,捡起来还有点儿困难,发现当时理解不是那么透彻,自己主要理解难点是这个算法如何求解next数组。明白之后,发现它也并不难理解,就是有些资料的术语起了误导的作用,下面会按照小白的思路进行一系列说明,力求道明它的本质。
上述代码使用了,求字符串长度、字符串取子串以及字符串比较等基本操作。下面将单纯用数组来实现同样的功能,这种被称为朴素的模式匹配,主要思路如下:将子串的字符挨个与主串的字符比较,若相同,则进行下一个字符比较;若不相同,则主串的迭代变量回退到上次能够匹配的下一位置,而子串的迭代变量回到首位。代码如下:
但是,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数组函数定义如下:
当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。这里的前后缀为真前后缀,即不包括自身的子串。
(1)实现得到next数组,代码如下:
再来看下代码中是如何实现回溯的,过程如下图:
看到这里的时候,不由得感慨写出这段代码的人编程内功深厚,佩服佩服!!!以上纯属个人感概,不喜勿喷,谢谢^_^.下面我们在再看看下子串在主串位置的主逻辑,代码如下,与朴素模式匹配基本一致。
此时,上述代码就是去掉了i值回溯的部分,其余与朴素算法一样。对于get_next函数来说,若T的长度为m,因此只设计到简单的单循环,时间复杂度为O(m),Index_KMP的i值不回溯之后,while的时间复杂度为O(n)。因此,整个算法时间复杂度为O(n+m)。相比较朴素模式匹配算法O((n-m+1)*m)来说,是要好些。这里需要强调的是,KMP算法仅当模式与主串之间存在许多“部分匹配”的情况下才会体现出它的优势,否则二者差异不明显。
这当中的第2,3,4,5步骤,其实都是多余的。由于T串的第2,3,4,5位置的字符都与首位的“a”相等,那么可以用首位next[0]的值去取代与它相等的字符后续next[j]的值。于是GetNext函数代码修改后如下:
实验截图:
以上,就是本次KMP算法的讲解,如有疑问,欢迎留言!谢谢浏览!!!
一、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)的深入理解
- 模式匹配算法KMP详解
- KMP模式匹配算法--未优化
- [置顶] 解析KMP模式匹配算法
- [zz]三种模式匹配算法(KMP,MonteCarlo,LasVegas)的比较与分析
- 串模式匹配算法--KMP图解
- KMP模式匹配算法
- 简单模式匹配算法和KMP模式匹配算法
- KMP,模式匹配算法
- 串的KMP模式匹配算法(C语言优化)
- 字符串模式匹配算法——BM、Horspool、Sunday、KMP、KR、AC算法一网打尽
- 两种模式匹配算法:Brute-Force和KMP
- KMP模式匹配算法实现
- KMP模式匹配算法 C++实现
- [置顶] 解析KMP模式匹配算法
- 串模式匹配算法--KMP图解
- 9.KMP模式匹配算法实现o(n)复杂度的匹配
- 模式匹配算法--KMP c代码
- 数据结构(11)--串的模式匹配算法之BF、KMP算法
- 模式匹配的算法Kmp