小白用最原始的语言来完全理解KMP算法

前言

整了两天才算是领悟了KMP,够费劲的,回头再看好像还真的是一个挺简单的算法(以学习者角度看,非发明者)。网上有人说,KMP是一个很简单的算法,之所以难,是因为没有人能讲清楚。现在看来,感觉有一点道理,不过呢,难免每个人介绍侧重点不同,还是得靠自己多去思考才行。

弄懂KMP算法后,感觉其实主要是两个大问题,一个是算法本身,一个是代码理解。

以下纯属一些个人的见解,用于帮助翻了多篇文章但还是不甚理解的小伙伴,作一个思路上的参考,其实就是有一些疑惑没有解决,这些疑惑或许博主觉得太简单或者理所应当就没有提,但恰恰是给小白拦路的地方,所以文章主体是以 提问-解答 的方式来介绍KMP,或许这些问题超级简单,但是我认为它们是有用的

本人也是看了很多其他大佬的博客才搞懂这个问题的,因为翻的博客很多,因此没有记住他们的博客地址,但在这里仍十分感谢他们的分享精神,明白算法后,趁脑袋还不糊涂特此梳理记录。

以上。

KMP算法思路

KMP算法干嘛的就不多说了,思路部分这里不放代码(很多教程都通过分析代码介绍KMP,其实我觉得没必要,用代码反而把内容整多了),而且也不提出新概念,用原始的语言介绍,然后在算法实现部分,介绍概念。

常规暴力查找

常规暴力查找就是,挨个匹配,发现不同了就挪一个位置,从头继续挨个匹配,直到找到结果(要找的位置),下面三张图说明问题:

在这里插入图片描述
图一中,发现了不匹配项,于是,下面的模式串挪一个位置:

在这里插入图片描述
图二中,发现了不匹配项,于是,下面的模式串再挪一个位置,以此类推。

在这里插入图片描述

KMP算法理解

KMP算法改进的地方就在于,上述暴力匹配中,出现了很多不必要的匹配(言外之意就是,有些匹配可以预先知道不行,直接跳过),KMP算法相当于是一个预处理,提前分析这些信息,跳过这些不必要的匹配。

上面我们说,预先知道了,那么问题来了,KMP怎么做到预先分析的?

按照我个人的理解:其实有时候匹配没法预先知道后面某个匹配是否可行,之所以能做到预先,是当这个模式串本身具有特殊性的时候。 (KMP中的next数组就是记录这个特殊性,如果不懂的话,括号这句话暂时可以不用看,这里抛个砖)啥特殊性?,其实就是字符串本身的局部重复性,正是这个重复性,导致的匹配重复,其他博文中写的是对称性(非中心对称),KMP就是分析了它的重复性。

例如:模式串 ABABC,这里面有两个AB,重复了,它重复意味着什么呢?

假设主串是ABABD ABABC(这个示例串中没有空格,我写空格是为了方便观察),好,现在用模式串ABABC开始匹配,发现第5个字符不同,

暴力情况下,我们把模式串右挪一个位置从头匹配, 看图:

在这里插入图片描述
KMP咋做的呢?这么做的: 在第一次匹配时,前面的ABAB都相同(其实这里为了说明特别举的例子,假如前面的匹配压根就没有相同的,就算是KMP来了,其实也只能一位一位挪的判断),只有第5位不同,言外之意就是前4位是相同的,KMP分析了前4位已匹配相同的串中(注意加粗的关键词),AB是重复的(这里隐含的一个信息就是A和B是不同的,不然ABAB就成了AAAA,就变成了A重复而不是AB重复了),因此现在有如下信息:

1.已匹配的部分是完全相同的(这里例子是前4位)
2.已匹配的部分中,有重复的(这里是AB)

因为前面匹配到的是相同的部分,因此你打算右挪一位后的模式串的第一位和主串字符比,就相当于跟自己的后一位对比,现在说了,KMP知道AB中的A和B是不重复的(AB重复,而A和B不重复),因此不挪一位了,挪2位(重复串AB的长度),就直接AB对上了,如图:

在这里插入图片描述
因为重复AB长度是两位,因此挪两位,就对上了,然后继续后面的匹配:

在这里插入图片描述
这个算法说穿了就是KMP做了一次预处理,提前判断了串的重复性。

重复性判断是已匹配串前的串,例如ABABC,前4个相同,第5个C不同,那么KMP就会判断ABAB的重复性(用重复串最长长度来衡量),假如ABC,在第3位就不同了,就判断前两位AB的重复性,但是示例匹配的时候,你也不知道已匹配多少位,可能全匹配?可能3位匹配上了,也可能一个没匹配上,因此KMP记录了,模式串中每个字符前的串重复性,匹配了几位,你自己就用几位的值。

现在在这里引出KMP中的next数组概念,next数组就做了一件事,就是记录了模式串中每个字符及之前的串的重复性(也就是重复字符串的最长长度)。

算法实现

基本概念

思路完了,在这里兑现代码,其实思路不难,只是代码实现有些地方比较容易绕。不急,慢慢说,先介绍一些基本概念:前缀,后缀,部分匹配值

前缀: 除最后一个字符外,所有头部子字符串的集合(有序,连续的)
例如:ABAB,前缀是,A,AB,ABA,没有其他情况了。

后缀: 除第一个字符外,所有尾部子字符串的集合(有序,连续的)
例如:ABAB,后缀是,B,AB,BAB,没有其他情况了。

前后缀就是用于找重复串用的,部分匹配值就是上文中说到的每个字符及之前的串的最长重复串长度。

例如:ABAB,前后缀中有AB是一样的,所以匹配值是2(有多个重复串的话,取长度最长的),这是匹配值,部分匹配值是每个字符及之前的匹配都在里面,还是ABAB,它的子串包括,A、AB、ABA、ABAB,每个子串都有一个匹配值,放在一起就成了PM(Partial Match)数组,next数组是为了编程方便做了一点变换,实质还是PM数组。

在这里插入图片描述

实现

获取next数组

在我们使用部分匹配值的时候,每当匹配失败,就得去找它前一个元素的匹配值,使用起来不方便,因此我们把上述PM数组向右平移一位,即得到了next数组,空余的补-1,溢出的部分因为原来的子串中,最后一个元素的部分匹配值是下一个元素使用,显然已没有下一个元素,故可以舍去,下面放代码:

// 获取 next 数组
void getNext(char *p, int *next)
{
	// 部分1
    int i = 0, k = -1;
    next[0] = -1;  

    while (i < (int)strlen(p) - 1)
    {
    	// 部分2
        if (k == -1 || p[i] == p[k])
        {
            i++;
            k++;
            next[i] = k;
        }
        // 部分3
        else k = next[k];
    }
}

现在分别对代码中的各部分做出解释:

部分1: k = -1,为啥k为-1呢,我觉得主要是从编程方便的角度来看,其一是因为作为一个特殊值标志着模式串回到初始状态,在部分2中的

if (k == -1)

这里,判断模式串的是否回到初始状态,其二是刚好-1加1后等于0,标志着模式串开始从0下标参与与主串的判断。
PM数组中第一个字符匹配值一定是0(单个字符部分匹配值一定是0),所以next数组前两位是-1、0,因此有:

next[0] = -1;

部分2: k是从0开始的,如果后面的值都相同,i所指向的字符匹配值刚好是k,如图:

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
以此类推,只要后面挨个都相同,就有next[i] = k。

部分3: 这部分也就是后面遇到了不同的,也就是说后面某一个地方,该语句失效:

p[i] == p[k]

失效后,表明再往后就不一样了,但是我直接清零从头开始并不是最优解,因为前面已经匹配的部分中的部分可能跟我一样,看图:

在这里插入图片描述
于是有:

k = next[k]

KMP登场

// 参数为(主串, 模式串, next数组)
int useKMP(char *pre, char *search, int *next)
{
    int i = 0, j = 0;
    int preLen = (int)strlen(pre);
    int searchLen = (int)strlen(search);  // 获取长度信息

    while (i < preLen && j < searchLen)
    {
        if(j == -1 || pre[i] == search[j])
        {
            i++;
            j++;
        }
        /*
        * 这里的 j = next[j], 个人认为是
        * j = j - (j - next[j])  也就是 (新位置)j = (旧位置)j - 移动位数
        * 向右移动位数: 已匹配的字符数 - 对应的部分匹配值
        * 刚好计算结果是 j = next[j] 就是模式串的新位置
        */
        else j = next[j];   // 模式串向右移动
    }
	
	// j 大于模式串长度,说明 j 顺利匹配到结束, 才算成功,否则返回 -1
    if (j >= searchLen) return (i - searchLen);
    return -1;
}

完结散花

结果:

int main()
{
    char str[] = {"ABCABCC"};
    char child[] = {"ABCC"};

    int len = (int)strlen(str);
    int next[len];

    getNext(str, next);
    printf("The index result is: %d\n", useKMP(str, child, next));

    //printArray(next, len);
    return 0;
}

在这里插入图片描述

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值