新人向系列
本人之前几乎未接触过算法,这个系列送给跟我一样的小白。如果有想学的算法可以留下评论,我会试着去学然后总结出博客,这个系列尽量周更,共勉
引言
很久没有更新了(因为这段时间确实很混)
这次算法小白给大家介绍一个KMP字符串匹配算法。
一看到字符串匹配,我就想起了以前做过的一种暴力破解的方法(也是唯一我能自己想到的做法)。
对了,为了方便起见,这篇博客中,我们把需要找的字符串我们称之为pattern,从什么地方找的目标字符串称为target。
它的思想很简单,例如:target为ABABABCCB,pattern为ABABC,那么我们要做的事就是逐个比较
target | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 |
---|---|---|---|---|---|---|---|---|---|
- | A | B | A | B | A | B | C | C | B |
pattern | 0 | 1 | 2 | 3 | 4 |
---|---|---|---|---|---|
- | A | B | A | B | C |
首先target的第0格的A跟pattern的第0格的A匹配,然后B跟B,B跟C不匹配,那么从target第1格开始重新比较…
写三个循环控制变量,两个用来记录target和pattern中匹配字符串的开头位置,最后一个用来记录target中正在匹配的位置即可。
请大家仔细观察:
在第一趟中,target和pattern的第4格字符不同,于是我们又重新开始第二趟,哇,有没有办法能够不那么费劲呢?
当然有了,n年前,三个大佬发明了一个算法,就叫做KMP算法,来解决这个问题。
KMP算法思想
最长公共前后缀
首先我们需要了解一个概念:最长公共前后缀
公共前后缀就是:例如说ABAB,它前数两格AB和后数两格AB是一样的,他们就是公共前后缀。
再举个例子,ABCABA,它的最长公共前后缀就是1,ABBC,它的最长公共前后缀就是0,这下应该知道它是什么了吧。
请大家看一下pattern的最长公共前后缀是多少。
答案是2。
另外,公共前后缀不能为字符串本身(如果是的话岂不是所有最长公共前后缀大小都为本身了?)
那么它又有什么用呢?
还是拿上边例子举例,它的作用在于:
当第一趟的第四格不匹配时,我能够不用重新比较target的34格和pattern的12格,(因为target的12格pattern的12,且因为pattern的最长公共前后缀是2,pattern的12格pattern的34格,所以不必重新比较,可以直接从target的4格跟pattern的3格来看是否匹配。
总而言之,它的作用在于我们能够不必重新匹配前面属于前后缀的已经匹配过的子串(如果没有前后缀,那么还是要重新匹配的)
算法步骤
一、打出前缀表(方便查询使用)
关于这个表我还是再拿上面的pattern举个例子 (pattern:又拿我?)。
pattern | 0 | 1 | 2 | 3 | 4 |
---|---|---|---|---|---|
- | A | B | A | B | C |
prefix_table | 0 | 0 | 1 | 2 | 0 |
prefix_table我们人写起来简单,但是代码怎么写呢?
仔细观察0~2的ABA,它的最长公共前后缀是1,那么:若想要0~3的字符串最长公共前后缀为2,是不是只要第三格是第一格就可以了。
如此它们存在一定的关系。
但是如果不匹配,情况就不一样了。
不能简单的令prefix_table中对应格数为0。
举例子:
如ABA和ABAC,这里第三格的C跟第一格B不匹配,确实它的最长公共前后缀为0。
但是如果是ABA和ABAA呢,第三格的A和第一格的B不匹配,但是答案是1。
因为你没法保证后半部分的子串会不会和前缀开始的子串有公共部分
也就是说又要重新匹配了吗?
是的,但是我们可以再利用KMP的思维进行匹配。(禁止套娃)
前边说过,不匹配的时候只要把pattern当前位的前面的子串的最长公共前后缀位置开始比较就可以,那么这里也是一样。
最后实现时为了方便,把prefix_table全部向右移动一格(最后一位丢掉),第一位补-1(也可以是别的),这里我们会在传参的时候巧妙处理(见下)
二、正式进行KMP匹配
这部分就像上面引言讲的那样了,其实打表相对复杂,这部分可以去看动画,然后根据代码理解。
KMP模式搜索算法动画演示
代码
注释已经比较清楚了,要注意的是边界情况,避免一些段错误的发生。
#include <stdio.h>
#include <string.h>
//初始化前缀表
//pattern[]: 模式字符串(要比较的)
//prefix_table[]: 前缀表
//n: 字符串的长度
void prefix_table_init(char pattern[], int prefix_table[], int n)
{
prefix_table[0] = 0; //最大共同前后缀不包括本身,只有一个字符时前缀长度肯定为0
int i = 1; //填第几位的prefix_table[]
int j = 0; //从数组的第几位开始匹配前面的字符串,初始当然是0
while (i < n)
{
if (pattern[i] == pattern[j]) //如果相同,i和j同时后移,继续匹配后面的字符串
{
j++;
prefix_table[i] = j;
i++;
}
else
{
if (j == 0) //若第一位就不匹配
{
prefix_table[i] = 0;
i++;
}
else
{
j = prefix_table[j - 1]; //重新从j之前的字符串中,最大前缀的后一位开始比较
//继续匹配该处字符,i无需++
}
}
}
}
//target[]: 目标字符串(你要从那里找的)
void kmp_search(char pattern[], char target[], int prefix_table[])
{
int len_pattern = strlen(pattern);
int len_target = strlen(target);
int i = 0; //target的定位
int j = 0; //pattern的定位
int count = 1;
//跟prefix_table_init()里面的类似
while (i < len_target)
{
if ((j == len_pattern - 1) && (target[i] == pattern[j])) //可以不要前缀表最后的原因就在这,最后一位匹配则匹配
{
printf("在下标%d开始第%d次找到%s\n", i - j, count++, pattern);
//未达终点,继续寻找
//仍是kmp匹配的思维
j = prefix_table[j];
}
if (target[i] == pattern[j])
{
i++;
j++;
}
else
{
if (j == 0)
{
i++;
j++;
}
//否则从....开始匹配
else
{
j = prefix_table[j];
}
}
}
if (count == 1)
{
printf("%s中未出现%s\n", target, pattern);
}
}
int main(int argc, char *argv[])
{
char pattern[] = "ABABC";
char target[] = "ABABBABABC";
printf("pattern: %s\ntarget: %s\n", pattern, target);
int len = strlen(pattern);
int prefix_table[len] = {-1}; //prefix_table[0]初始化为-1
prefix_table_init(pattern, prefix_table + 1, len - 1); //最后一位其实用不着
kmp_search(pattern, target, prefix_table);
return 0;
}
Reference
KMP字符串匹配算法1
KMP字符串匹配算法2
【soso字幕】汪都能听懂的KMP字符串匹配算法【双语字幕】
(看了第三个才懂了,等等这标题怎么怪怪的)