KMP算法字符串匹配

本文探讨了如何通过暴力匹配解决字符串查找问题,提出利用最长公共前后缀降低时间复杂度的方法。算法通过两个下标跟踪匹配过程,并介绍了公共前后缀的性质和预处理技巧,将时间复杂度从O(n^2)降低至O(n)。
摘要由CSDN通过智能技术生成

问题

有一个字符串s_1,要在s_1中寻找子串s_2,问s_2s_1中出现了几次?

暴力匹配

首先能想到的就是进行暴力匹配。

s_11号位开始,到|s_1|-|s_2|号位,截下长度为|s_2|的子串依次进行比较。当失配时,把s_2右移一位,继续从头开始匹配。

算法思想

暴力算法中,每一次失配,都需要把s_2从第1位再重新开始比较,且s_2相较于s_1仅仅右移了1位,这样重复的操作,使得时间复杂度很高。

让我们再回到暴力匹配时的第一次失配,发现了此时s_146项与s_202项相同。我们为了把s_202项得到匹配,则最少需要与s_146项对齐。

对齐后,字符串s_1与字符串s_2仅需从第3位开始比较就行了(前面几位相同)

就这样一直进行匹配,直到配上了为止。

算法实现

匹配

代码中,我们没有办法像上面演示的那样进行滑动,那可以通过两个下标t_1,t_2对目前匹配到的地方进行表示(t_1对应上图的蓝色箭头,t_2对应上图的黄色箭头)

则匹配过程可以分为两种情况:

  1. 配上:t_1+1t_2+1把两个箭头都右移一位
  2. 失配:t_1保持不动,t_2=next_{t_2},此处的next_i就是s_2在第i位失配后所需要到的位置(具体求法在下一节中写出)

如果要多次进行都次匹配,char字符数组的结尾处有'\0'的结束符,相当于一次失配,能让程序继续进行。

这样的算法可以把时间复杂度从\varTheta(|s_1| \cdot |s_2|)降为\varTheta(|s_1| + |s_2|)

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);
}

预处理

next数组在更深层次的意义上就是前i项的最长公共前后缀(Longest Border)。而要用\varTheta(|s_2|)的时间复杂度进行递推,也需要两个下标t_1,t_2

此时枚举的字符串范围是[0,t_1),而这个字符串的最长公共前后缀长度是[0,t_2)

递推时,最长公共前后缀长度的2倍可能会超出原字符串长度,此处假设最长公共前后缀长度的2\le原字符串长度

递推时也可以分为两种情况:

(1)s1_{t_1}=s2_{t_2}时,意味着原来最长公共前后缀的后一项与原字符串的后一项相等(橙色段),把t_1+1t_2+1

(2)s1_{t_1} \not = s2_{t_2}时,意味着原来最长公共前后缀的后一项与原字符串的后一项不相等,递归回溯s_2的最长公共前后缀,把t_1+1t_2=next_{t_2}(证明在后面)

证明

此时,我们面对的时第2种情况。需要证明字符串[0,t_1+1)的最长公共前后缀一定是字符串[0,t_2)的公共前后缀(可能没有)

首先,因为第1种情况不成立且选择长度更长的字符串[0,t_1+1)的前缀肯定不是该字符串的后缀(如有,则字符串[0,t_2)就不是原来的最长公共前后缀)(如图)。所以没有一个长度>原来最长公共前后缀+1的字符串。

然后,字符串[0,t_2)的公共前后缀一定是字符串[0,t_1+1)的前缀。

图中,蓝色区域全部相等,所以字符串[0,t_2)的公共前后缀才有成为字符串[0,t_1+1)的最长公共前后缀的可能。

同理,非字符串[0,t_2)的公共前后缀连蓝色区域相等都无法满足(证明同字符串[0,t_1+1)的最长公共前后缀一定是字符串[0,t_2)的公共前后缀)

​​​​​​​字符串[0,t_2)的最长公共前后缀的公共前后缀也满足上图的性质。

最终,通过next数组递归对长度从大到小进行可能的公共前后缀进行遍历。

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) 的一些性质:

  1. 一个字符串公共前后缀的公共前后缀也是该串的公共前后缀。
  2. 可以通过找一个字符串最长公共前后缀,递归求出该串的全部公共前后缀。
  3. 一个字符串的公共前后缀都是这个字符串最长公共前后缀的公共前后缀。

这些是一些有用的性质,做题时可能会用到。 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值