字符串匹配的KMP算法和C语言代码,不需要思考就能理解

        KMP算法用于判断一个字符串是否包含另一个字符串,如果包含就返回脚标。其实KMP算法本身特别简单,我看了几篇本章都号称简单易懂,结果看得我云里雾里,直到我看到了阮一峰:字符串匹配的KMP算法,才真正看懂。下文的第一部分基本上都是阮一峰文章的转述,第二部分代码也是在网上其他地方找的。第三部分KMP算法的理解,才是笔者的东西,相信看了第三部分你会豁然开朗。

一、KMS算法的处理过程

下面用 KMP算法在字符串"BBC ABCDAB ABCDABCDABDE"中寻找字符串"ABCDABD":

字符串匹配的KMP算法和C语言代码,不需要思考就能理解_第1张图片

首先,字符串"BBC ABCDAB ABCDABCDABDE"的第一个字符与搜索词"ABCDABD"的第一个字符,进行比较。因为B与A不匹配,所以搜索词后移一位。

字符串匹配的KMP算法和C语言代码,不需要思考就能理解_第2张图片

因为B与A不匹配,搜索词再往后移。

字符串匹配的KMP算法和C语言代码,不需要思考就能理解_第3张图片

就这样,直到字符串有一个字符,与搜索词的第一个字符相同为止。

字符串匹配的KMP算法和C语言代码,不需要思考就能理解_第4张图片

接着比较字符串和搜索词的下一个字符,还是相同。

字符串匹配的KMP算法和C语言代码,不需要思考就能理解_第5张图片

直到字符串有一个字符,与搜索词对应的字符不相同为止。

字符串匹配的KMP算法和C语言代码,不需要思考就能理解_第6张图片

这时,最自然的反应是,将搜索词整个后移一位,再从头逐个比较。这样做虽然可行,但是效率很差,因为你要把"搜索位置"移到已经比较过的位置,重比一遍。

字符串匹配的KMP算法和C语言代码,不需要思考就能理解_第7张图片

一个基本事实是,当空格与D不匹配时,你其实知道前面六个字符是"ABCDAB"。KMP 算法的想法是,设法利用这个已知信息,不要把"搜索位置"移回已经比较过的位置,继续把它向后移,这样就提高了效率。

字符串匹配的KMP算法和C语言代码,不需要思考就能理解_第8张图片

怎么做到这一点呢?可以针对搜索词,算出一张《部分匹配表》(Partial Match Table)。这张表是如何产生的,后面再介绍,这里只要会用就可以了。

字符串匹配的KMP算法和C语言代码,不需要思考就能理解_第9张图片

    已知空格与D不匹配时,前面六个字符"ABCDAB"是匹配的。查表可知,最后一个匹配字符B对应的"部分匹配值"为2,因此按照下面的公式算出向后移动的位数:

    移动位数 = 已匹配的字符数 - 对应的部分匹配值

    因为 6 - 2 等于4,所以将搜索词向后移动 4 位。

字符串匹配的KMP算法和C语言代码,不需要思考就能理解_第10张图片

移动完后,再比较,发现空格与C还是不匹配,于是搜索词还要继续往后移。这时,已匹配的字符数为2("AB"),对应的"部分匹配值"为0。所以,移动位数 = 2 - 0,结果为 2,于是将搜索词向后移 2 位。

字符串匹配的KMP算法和C语言代码,不需要思考就能理解_第11张图片

这时空格与A还是不匹配,继续后移一位。(因为搜索词已经移动到头了,所以默认的移动距离为1位)

字符串匹配的KMP算法和C语言代码,不需要思考就能理解_第12张图片

这个时候发现能匹配了,于是又逐位比较,直到发现C与D不匹配。于是,移动位数 = 6 - 2,继续将搜索词向后移动 4 位。

字符串匹配的KMP算法和C语言代码,不需要思考就能理解_第13张图片

这下就完全匹配了。

下面介绍《部分匹配表》是如何产生的。

  首先,要了解两个概念:"前缀"和"后缀"。 "前缀"指除了最后一个字符以外,一个字符串的全部头部组合;"后缀"指除了第一个字符以外,一个字符串的全部尾部组合。

        比如“bread”这个字符串的前缀是除了最后一个字符“d”,还剩下"brea",它的全部组合字符串有:"b"、"br"、"bre"、"brea"。

        “bread”这个字符串的后缀是除了第一个字符“b”,还剩下"read",它的全部组合字符串有:"r"、"re"、"rea"、"read"。

字符串匹配的KMP算法和C语言代码,不需要思考就能理解_第14张图片

        "部分匹配值"就是"前缀"和"后缀"的最长的共有元素的长度。以"ABCDABD"为例,

-"A"的前缀和后缀都为空集,共有元素的长度为0;

-"AB"的前缀为[A],后缀为[B],共有元素的长度为0;

-"ABC"的前缀为[A, AB],后缀为[BC, C],共有元素的长度0;

-"ABCD"的前缀为[A, AB, ABC],后缀为[BCD, CD, D],共有元素的长度为0;

-"ABCDA"的前缀为[A, AB, ABC, ABCD],后缀为[BCDA, CDA, DA, A],共有元素为"A",长度为1;

-"ABCDAB"的前缀为[A, AB, ABC, ABCD, ABCDA],后缀为[BCDAB, CDAB, DAB, AB, B],共有元素为"AB",长度为2;

-"ABCDABD"的前缀为[A, AB, ABC, ABCD, ABCDA, ABCDAB],后缀为[BCDABD, CDABD, DABD, ABD, BD, D],共有元素的长度为0。

         "部分匹配值"其实还可以定义为:把字符串沿着中间的字符折叠,字符串首尾重叠的字符个数。

        比如“ABC”沿着字符“B”折叠,首尾的“A”和“C”就不能重叠,所以"部分匹配值"为0.

        "ABCDA"沿着字符“C”折叠,首尾的“A”能重叠,所以"部分匹配值"为1.

        "ABCDAB"沿着字符“C”和“D”的中间折叠,首尾的“AB”能重叠,所以"部分匹配值"为2.

二、C语言程序

        利用上面KMS的处理过程和部分匹配值的生成方法,就可以写程序了:

void cal_next(char *str, int *next, int len)
{
    next[0] = -1;//next[0]初始化为-1,-1表示不存在相同的最大前缀和最大后缀
    int k = -1;//k初始化为-1
    for (int q = 1; q <= len-1; q++)
    {
        while (k > -1 && str[k + 1] != str[q])//如果下一个不同,那么k就变成next[k],注意next[k]是小于k的,无论k取任何值。
        {
            k = next[k];//往前回溯
        }
        if (str[k + 1] == str[q])//如果相同,k++
        {
            k = k + 1;
        }
        next[q] = k;//这个是把算的k的值(就是相同的最大前缀和最大后缀长)赋给next[q]
    }
}

int KMP(char *str, int slen, char *ptr, int plen)
{
    int *next = new int[plen];
    cal_next(ptr, next, plen);//计算next数组
    int k = -1;
    for (int i = 0; i < slen; i++)
    {
        while (k >-1&& ptr[k + 1] != str[i])//ptr和str不匹配,且k>-1(表示ptr和str有部分匹配)
            k = next[k];//往前回溯
        if (ptr[k + 1] == str[i])
            k = k + 1;
        if (k == plen-1)//说明k移动到ptr的最末端
        {
            //cout << "在位置" << i-plen+1<< endl;
            //k = -1;//重新初始化,寻找下一个
            //i = i - plen + 1;//i定位到该位置,外层for循环i++可以继续找下一个(这里默认存在两个匹配字符串可以部分重叠),感谢评论中同学指出错误。
            return i-plen+1;//返回相应的位置
        }
    }
    return -1;  
}

测试程序:

    char *str = "bacbababadababacambabacaddababacasdsd";
    char *ptr = "ababaca";
    int a = KMP(str, 36, ptr, 7);
    return 0;

三、KMS的理解

当搜索词已经匹配了6个字符,最后一个字符不匹配时

字符串匹配的KMP算法和C语言代码,不需要思考就能理解_第15张图片

传统的做法是移动一位,然后重新比较

字符串匹配的KMP算法和C语言代码,不需要思考就能理解_第16张图片

可是KMS却是直接移动了6 - 2 =4位:

字符串匹配的KMP算法和C语言代码,不需要思考就能理解_第17张图片

这是为什么呢?

怎么保证的移动1位、移动2位、移动3位时遇到的字符串都不匹配,所以KMS能直接移动4位,再去比较呢?

其实非常简单,这就是字符串沿中间折叠后,首尾字符重叠个数(也就是部分匹配值)的作用所在。

下面我举个例子就懂了:

比如要在字符串“123456789”中找“4568”

字符串匹配的KMP算法和C语言代码,不需要思考就能理解_第18张图片

当匹配到上图的位置时,发现字符“7”和字符“8”不匹配,于是传统的方法是向后移动一位:

字符串匹配的KMP算法和C语言代码,不需要思考就能理解_第19张图片

而KMS的移动距离用公式计算为:3-0=3位

字符串匹配的KMP算法和C语言代码,不需要思考就能理解_第20张图片

KMS算法把“4568”中已经匹配过的“456”,直接跳过去了。为什么这里能直接把匹配过的字符串“456“跳过去呢?

下面我们来讨论,我们把已经匹配过的字符分5种情况:(以3个字符来举例)

1)、已经匹配过的字符串中,每个字符都不相同,如”456“

    如果是这种情况,毫无疑问是可以直接把已匹配的字符跳过去的。

2)、已经匹配过的字符串中,每个字符都相同,如”444“

    这种情况,最多只能移动1位,比如下面的例子:

字符串匹配的KMP算法和C语言代码,不需要思考就能理解_第21张图片

3)、已经匹配过的字符串中,首尾字符相同,如”454“

    这种情况,移动位数要大于1,小于等于2。比如:

字符串匹配的KMP算法和C语言代码,不需要思考就能理解_第22张图片

移动一位后:

字符串匹配的KMP算法和C语言代码,不需要思考就能理解_第23张图片

首字符会和中间的字符对齐,他们不相同。再移动一位,就可能完全匹配了,因此移动位数最多为2。

4)、已经匹配过的字符串中,首两个字符相同,如”445“

字符串匹配的KMP算法和C语言代码,不需要思考就能理解_第24张图片

移动一位:

字符串匹配的KMP算法和C语言代码,不需要思考就能理解_第25张图片

最后两个字符会和首两个字符对齐,他们一定不相同。

移动两位:

字符串匹配的KMP算法和C语言代码,不需要思考就能理解_第26张图片

最后一个字符会和首字符对齐,他们一定不相同。因此移动位数最少为3。

5)、已经匹配过的字符串中,尾两个字符相同,如”455“

同理,移动位数最少为3。

        

        经过上面的例子分析,可以看出KMS算法就是对已经匹配过的字符串再次利用来提高效率的。

        KMS算法可以一句话总结为:已经匹配过的字符串如果首尾有相同的字母,就要少移动相应的位数,否则,已经匹配了多少个字符,就可以移动多少位。

你可能感兴趣的:(MCU,算法)