问题
有一个字符串,要在中寻找子串,问在中出现了几次?
暴力匹配
首先能想到的就是进行暴力匹配。
把从号位开始,到号位,截下长度为的子串依次进行比较。当失配时,把右移一位,继续从头开始匹配。
算法思想
暴力算法中,每一次失配,都需要把从第位再重新开始比较,且相较于仅仅右移了位,这样重复的操作,使得时间复杂度很高。
让我们再回到暴力匹配时的第一次失配,发现了此时的到项与的到项相同。我们为了把的到项得到匹配,则最少需要与的到项对齐。
对齐后,字符串与字符串仅需从第位开始比较就行了(前面几位相同)
就这样一直进行匹配,直到配上了为止。
算法实现
匹配
代码中,我们没有办法像上面演示的那样进行滑动,那可以通过两个下标,对目前匹配到的地方进行表示(对应上图的蓝色箭头,对应上图的黄色箭头)
则匹配过程可以分为两种情况:
- 配上:把,把两个箭头都右移一位
- 失配:保持不动,,此处的就是在第位失配后所需要到的位置(具体求法在下一节中写出)
如果要多次进行都次匹配,char字符数组的结尾处有'\0'的结束符,相当于一次失配,能让程序继续进行。
这样的算法可以把时间复杂度从降为
code(多次匹配):
int t1 = 0,t2 = 0;
while(t1 < len1){
if(t2 == -1 || s1[t1] == s2[t2]){
t1 ++;
t2 ++;
}else t2 = nxt[t2];
if(t2 == len2) printf("%d\n",t1 - len2 + t2);
}
预处理
数组在更深层次的意义上就是前项的最长公共前后缀(Longest Border)。而要用的时间复杂度进行递推,也需要两个下标,。
此时枚举的字符串范围是,而这个字符串的最长公共前后缀长度是。
递推时,最长公共前后缀长度的倍可能会超出原字符串长度,此处假设最长公共前后缀长度的倍原字符串长度
递推时也可以分为两种情况:
(1)时,意味着原来最长公共前后缀的后一项与原字符串的后一项相等(橙色段),把,。
(2)时,意味着原来最长公共前后缀的后一项与原字符串的后一项不相等,递归回溯的最长公共前后缀,把,。(证明在后面)
证明
此时,我们面对的时第种情况。需要证明字符串的最长公共前后缀一定是字符串的公共前后缀(可能没有)
首先,因为第种情况不成立且选择长度更长的字符串的前缀肯定不是该字符串的后缀(如有,则字符串就不是原来的最长公共前后缀)(如图)。所以没有一个长度原来最长公共前后缀的字符串。
然后,字符串的公共前后缀一定是字符串的前缀。
图中,蓝色区域全部相等,所以字符串的公共前后缀才有成为字符串的最长公共前后缀的可能。
同理,非字符串的公共前后缀连蓝色区域相等都无法满足(证明同字符串的最长公共前后缀一定是字符串的公共前后缀)
字符串的最长公共前后缀的公共前后缀也满足上图的性质。
最终,通过数组递归对长度从大到小进行可能的公共前后缀进行遍历。
code:
int t1 = 0,t2 = -1;
nxt[0] = -1;
while(t1 < len2){
if(t2 == -1 || s2[t1] == s2[t2]){
t1 ++;
t2 ++;
nxt[t1] = t2;
}else{
t2 = 0;
nxt[t1] = 0;
t1 ++;
}
}
拓展
公共前后缀(Border) 的一些性质:
- 一个字符串公共前后缀的公共前后缀也是该串的公共前后缀。
- 可以通过找一个字符串最长公共前后缀,递归求出该串的全部公共前后缀。
- 一个字符串的公共前后缀都是这个字符串最长公共前后缀的公共前后缀。
这些是一些有用的性质,做题时可能会用到。