KMP算法(绝对简单易懂)

KMP算法的作用

比较一个字符串是否为另一个字符串的子串,并可以返回匹配首位置

 

算法实现过程

1.

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


2.

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

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


4.

 

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

 


5.

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


6.

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


7.

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


8.

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


9.

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


10.

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


11.

因为空格与A不匹配,继续后移一位。


12.

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


13.

逐位比较,直到搜索词的最后一位,发现完全匹配,于是搜索完成。如果还要继续搜索(即找出全部匹配),移动位数 = 7 - 0,再将搜索词向后移动7位,这里就不再重复了。


14.

下面介绍《部分匹配表》是如何产生的。
首先,要了解两个概念:"前缀"和"后缀"。 "前缀"指除了最后一个字符以外,一个字符串的全部头部组合;"后缀"指除了第一个字符以外,一个字符串的全部尾部组合。


15.

"部分匹配值"就是"前缀"和"后缀"的最长的共有元素的长度。以"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。


16.

"部分匹配"的实质是,有时候,字符串头部和尾部会有重复。比如,"ABCDAB"之中有两个"AB",那么它的"部分匹配值"就是2("AB"的长度)。搜索词移动的时候,第一个"AB"向后移动4位(字符串长度-部分匹配值),就可以来到第二个"AB"的位置。

来自:字符串匹配的kmp算法

 

 

求解next数组(核心部分)

1、next数组里面存的是什么值呢?其实就是该位置前的字符串前缀与后缀的共用长度(文中的部分匹配值)-1(说白了就是前后相同的长度-1)

 

 

实现代码:

//其实这个你自己静下心模拟一下过程,可以很快理解的
void kmp_next(char *s)
{
    int len = strlen(s);
    next[0] = -1;
    int k = -1;
    for(int i = 1; i < len; i ++) {
        while(k > -1 && s[k+1] != s[i]) {
            k = next[k];
        }
        if(s[k+1] == s[i]) {
            k ++;
        }
        next[i] = k;
    }
}

 

 

字符串匹配

相信看懂了前面的描述,你一定理解了匹配过程,接下来就给出匹配过程的代码:(这个其实你完全按照自己理解写,别人的代码仅供参考)

//理解运用next数组的写法
void kmp1(char *s1, char *s2)
{
    int len1 = strlen(s1);
    int len2 = strlen(s2);
    int idx = 0; //s2中匹配的位置
    kmp_next(s2);
    for(int i = 0; i < len1; i ++) {
        if(s1[i] == s2[idx]) {
            idx ++;
        }else {
            if(idx > 0) { //此时不匹配之前有匹配的
                idx = next[idx-1] + 1;
                i--; //避免多移一位
            }else idx = 0;
        }
        if(idx == len2) { //完全匹配
            idx = next[idx-1] + 1;
            a[cnt ++] = i-len2+1;
        }
    }
}

//更高深的写法
void kmp2(char *s1, char *s2)
{
    int len1 = strlen(s1);
    int len2 = strlen(s2);
    kmp_next(s2);
    int k = -1;
    for(int i = 0; i < len1; i ++) {
        while(k > -1 && s2[k+1] != s1[i]) { //此时不匹配但是前面有匹配的
            k = next[k];
        }
        if(s2[k+1] == s1[i]) {
            k ++;
        }
        if(k == len2-1) a[cnt ++] = i;
    }
}

 

<think>嗯,用户让我解释KMP算法的原理,而且要简单易懂。首先,我得回想一下KMP的基本概念。KMP算法是用于字符串匹配的,相比于暴力匹配,它更高效。那它的核心思想是什么呢?应该是利用已经部分匹配的信息来跳过不必要的比较,从而减少时间复杂度。 接下来,我需要确定用户可能的背景。他可能在学习算法或者遇到字符串匹配的问题,想了解KMP如何工作。用户可能对暴力匹配的低效有体会,比如在长文本中查找子串时时间过长。因此,我需要对比暴力方法,突出KMP的优势。 KMP的关键在于部分匹配表,也就是前缀函数或者next数组。这个表记录了模式串中每个位置的最长公共前后缀长度。这样,当匹配失败时,可以根据这个表移动模式串,而不是每次只移动一位。例如,在模式串"ABABC"中,如果某个位置不匹配,利用next数组可以知道应该移动几位,避免重复比较。 我需要用简单例子来说明这个过程。比如,主串是"ABABABCABAABABABCABAA",模式串是"ABABC"。当匹配到某个字符失败时,如何根据next数组调整位置。可能需要分步骤解释匹配过程,以及如何计算next数组。 不过用户可能对如何构建next数组不太清楚,所以需要详细解释这部分。构建next数组的过程其实也是模式串自我匹配的过程。比如,对于每个位置i,找到最大的k,使得前k个字符和后k个字符相同。这样,当匹配失败时,模式串可以直接跳到k的位置继续比较,而不用回退主串的指针。 可能用户还会关心时间复杂度,KMP是O(n+m),而暴力是O(n*m)。这点也需要提到,但重点还是原理的解释。需要用通俗的语言,避免太多数学公式,而是通过例子和步骤说明。 最后,给出一个C++的实现示例,展示如何构建next数组和使用KMP进行匹配。代码需要注释清晰,结构正确。确保用户能看懂每个步骤的作用,比如如何初始化next数组,如何更新指针i和j。 现在,检查是否有遗漏点。比如,为什么要用最长公共前后缀?这样移动模式串能保证不错过可能的匹配,同时跳过不必要的比较。这部分可能需要进一步解释,但用户要求简单易懂,所以可能不需要太深入数学证明。 总结,回答应该包括:KMP解决的问题,暴力法的不足,部分匹配表的作用,构建next数组的方法,以及匹配过程的步骤。最后用代码示例巩固理解。</think><< KMP算法是一种高效的字符串匹配算法,其核心思想是通过预处理模式串构建「部分匹配表」(也称next数组),避免匹配失败时的重复比较。以下是关键原理解释: 1. **暴力匹配的缺陷**: 传统暴力匹配每次失配时,主串指针回退并从头比较,时间复杂度O(m*n) 2. **部分匹配表(next数组)**: 记录模式串每个位置的最长公共前后缀长度,用于确定失配时模式串的滑动距离 3. **匹配过程优化**: 当字符失配时,根据next数组值跳过已匹配的前缀部分,主串指针无需回退 示例代码: ```cpp #include <vector> using namespace std; vector<int> buildNext(const string& pattern) { vector<int> next(pattern.size(), 0); for (int i = 1, j = 0; i < pattern.size();) { if (pattern[i] == pattern[j]) { next[i++] = ++j; } else { j ? j = next[j-1] : i++; } } return next; } int kmpSearch(const string& text, const string& pattern) { vector<int> next = buildNext(pattern); int i = 0, j = 0; while (i < text.size()) { if (text[i] == pattern[j]) { i++; j++; if (j == pattern.size()) return i - j; // 找到匹配 } else { j ? j = next[j-1] : i++; } } return -1; // 未找到 } ``` 运行示例: ```cpp string text = "ABABABCABAABABABCABAA"; string pattern = "ABABC"; cout << kmpSearch(text, pattern); // 输出匹配位置2 ```
评论 6
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值