本系列文章是笔者复习自用,参考教材为严蔚敏编著的《数据结构(C语言版)》,如能对大家有所帮助实属荣幸
本篇的内容主要是介绍串的模式匹配和KMP算法,并介绍KMP算法中next数组的手算方法。
6.1 串的相关概念
串是由零个或多个字符组成的有限序列,一般记为
其中,S是串名,单引号括起来的是串的值,字符的个数n称为串的长度,n=0的串被称为空串。
串中任意连续的字符组成的子序列称为该串的子串,某个字符在串中的序号称为该字符在串中的位置,子串在主串中的位置以子串的第一个字符在主串中的位置表示。
例如,串A=‘China BeiJing’ ,B='China',C='BeiJing',那么B和C都是A的子串,且B在A中的位置是1,C在A中的位置是7。
介绍完串的概念之后,我们就可以进入模式匹配算法的学习了,由于研究生考试对串的存储结构没有要求,所以我们不介绍串的定义和相关函数。
6.2 模式匹配算法
1.暴力算法
子串的定位操作通常称为串的模式匹配,求的是子串在主串的位置。在学习KMP之前,我们通常使用一种简单暴力算法,即从主串的开头开始遍历,寻找与模式串中第一个字符相匹配的字符,找到之后,再看主串中的下一个字符与模式串中下一个字符是否匹配,以此类推,若发现不匹配项,则返回主串上次开始匹配的字符的下一位重新寻找匹配,如下图所示:
注意,在这里我将 至少有一个字符相同 的情况算作一次匹配。
仔细观察之后会发现,有很多比较其实不必要做,比如在第一次匹配失败之后,完全可以从主串第三位开始比较;在第二次匹配失败之后,可以从主串第六位开始比较。按照这个思路,我们对上面的过程进行优化,得到下图::
依照上图,我们可以发现节省了很多步骤,这就是KMP算法的思路。
2.KMP算法
在暴力匹配中,每趟匹配失败都是模式后移一位再从头开始比较。而某趟已匹配相等的字符序列是模式的某个前缀,这就相当于模式串在不断自我比较,导致低效率。因此,我们可以从分析模式本身的结构着手,如果已匹配相等的前缀序列中有某个后缀正好是模式的前缀,那么就可以将模式向后滑动到与这些相等字符对齐的位置,而主串指针无需回溯,并从该位置开始继续比较。
KMP的相关概念:
前缀:除了最后一个字符以外,字符串所有的头部子串。
后缀:除了第一个字符以外,字符串所有的尾部子串。
部分匹配值:字符串的前缀和后缀的最长相等前后缀长度。
以‘ababa’为例:
‘a’无前缀后缀,所以最大共同前后缀为0。
'ab'的前缀为{'a'},后缀为{'b'},所以最大共同前后缀为0。
'aba'的前缀为{‘a’,'ab'},后缀为{'ba','a'},有共同前后缀a,所以最大共同前后缀为1。
‘abab’的前缀为{'a','ab','aba'},后缀为{'bab','ab','b'},有共同前后缀ab,所以最大共同前后缀为2。
'ababa'的前缀为{'a','ab','aba','abab'},后缀为{'baba','aba','ba','a'},有共同前后缀aba,所以最大共同前后缀为3。
下面我们来谈谈最大共同前后缀的作用
这里我们以ABABAAABABAA为例:
如果第一位就不匹配,那就只能用子串第一位和主串的下一位进行比较,相当于用子串第0位与当当前位进行比较,取0。
如果第二位发生不匹配,取第二位之前的部分,‘A’,最大前后缀长度为0,则需要用子串第一位与主串当前位进行比较,取1。
如果第三位发生不匹配,取第三位之前的部分,‘AB’,最大前后缀长度为0,则需要用子串第一位与主串当前位进行比较,取1。
如果第四位发生不匹配,取第四位之前的部分,‘ABA’,最大前后缀长度为1,则需要用子串第二位与主串当前位进行比较,取2。
如果第五位发生不匹配,取第五位之前的部分,‘ABAB’,最大前后缀长度为2,则需要用子串第三位与主串当前位进行比较,取3。
如果第六位发生不匹配,取第六位之前的部分,‘ABABA’,最大前后缀长度为3,则需要用子串第四位与主串当前位进行比较,取4。
如果第七位发生不匹配,取第七位之前的部分,‘ABABAA’,最大前后缀长度为1,则需要用子串第二位与主串当前位进行比较,取2。
如果第八位发生不匹配,取第八位之前的部分,‘ABABAAA’,最大前后缀长度为1,则需要用子串第二位与主串当前位进行比较,取2。
如果第九位发生不匹配,取第九位之前的部分,‘ABABAAAB’,最大前后缀长度为2,则需要用子串第三位与主串当前位进行比较,取3。
如果第十位发生不匹配,取第十位之前的部分,‘ABABAAABA’,最大前后缀长度为3,则需要用子串第四位与主串当前位进行比较,取4。
如果第十一位发生不匹配,取第十一位之前的部分,‘ABABAAABAB’,最大前后缀长度为4,则需要用子串第二位与主串当前位进行比较,取5。
如果第十二位发生不匹配,取第十二位之前的部分,‘ABABAAABABA’,最大前后缀长度为5,则需要用子串第二位与主串当前位进行比较,取6。
最终得出该模式串的next数组为:[0,1,1,2,3,4,2,2,3,4,5,6]。
基于这种思想,有一种简便的计算方法,将数组每一位对应模式串每一位,首先第一位默认取0,对于第一位之后的位数,截取该位之前的字符串,找到其最大前后缀长度,加一后就是数组中的值,举个例子。
设有字符串P='aabaac',求其next数组。
首先第一位取0,略过。
第二位,前面的字符串a,无最大前后缀,0+1=1。
第三位,前面的字符串aa,最大前后缀a,1+1=2。
第四位,前面的字符串aab,无最大前后缀,0+1=1。
第五位,前面的字符串aaba,最大前后缀a,1+1=2。
第六位,前面的字符串aabaa,最大前后缀aa,2+1=3。
所以,该next数组为[0,1,2,1,2,3]。