数据结构与算法 KMP算法详解 以及与普通匹配优势比较

大名鼎鼎的KMP算法

来吧今天我们一起搞懂它!💪
首先不太清楚KMP算法的可以戳这里!百度百科欢迎您~

KMP算法简介:
KMP算法是一种改进的字符串匹配算法。KMP算法的核心是利用匹配失败后的信息,尽量减少模式串与主串的匹配次数以达到快速匹配的目的。具体实现就是通过一个next()函数实现,函数本身包含了模式串的局部匹配信息。KMP算法的时间复杂度O(m+n)。(来源百度)

可以看到KMP算法它的优势就是不回溯!
那么它怎么做到不回溯呢?我们一起来看看吧!如果你对普通算法清楚的话,请直接滑到KMP算法开始处~

普通的字符串匹配

如上图,上面的长串是被查找串,我们称源串;下面的短串"abcd" 是我们要查找的字符串,我们称子串。i 和 j 分别是源串和子串的查找下标。我们可以看到当下标 i 和 j 查到的内容不相等时,下标 i 立马回溯到第二个空间,而下标 j 也重新开始匹配。当下标 i 和 j 查到的内容再次不相等时,下标 i 立马回溯到第三个空间,下标 j 重新开始匹配。如果相等则继续向后比较,直到下标 j 所查找的空间值为0时,也就是 ‘\0’ --0结束标志,那么就算是找到了!当然有一些条件我没有在这里说,这里仅作介绍。具体代码里有。

上述方法看似没有问题,实则不然,当遇到一些神奇的字符串时,它就吃了大亏啦!下面两个字符串我们肉眼可见其实下标 i 可以不用回溯的,完全可以接着比,只要我们找到里面的玄机就OK!

KMP算法开始

我们以↖左上图为例讲解

按照普通方法比较,下标 i 和 j 比较到下图位置,他们比较的字符开始不相同了。

问:子串从失配点开始,左边存在最长多少个字符,与该子串最开始的字符是相同的?
答:5个。

此时,i 和 j 前面的字符都相等,我们观察子串,下标 j (失配点)前面,我们可以观察到从左至右aadaa出现了两次,且是相等的;因为绿框中的aadaa与红框中的aadaa已经比较过了,是相等的,且黄框与绿框中的字符串也相等,所以黄框与红框中的字符串相等。

因为子串从失配点开始,左边存在最长5个字符,与该子串最开始的字符是相同的,所以我们将下标 j 移至下标为 5 的地方与 i 继续比较,这样做 i 不用回溯,也不会漏掉我们要找的字符串,并且前面比过的字符也不用再比一遍了!

我们继续用这种方法匹配;

我们看到这里找到了!比普通方法不知快了多少步!

综上,我们发现使用这种方法 i 可以不用回溯,j 每次回溯的下标恰好等于失配点左边最多的相同字符数!

那么这个方法具体该怎么去用呢?如果要用这种方法,势必得全面考虑,也就是说,我们必须考虑每一个字符失配之后的操作!

aadaadaaaadaa
这是我们的子串,我们必须算出当子串中每一个字符为失配点时,下标 j 将会移动到哪个下标去|才能保证不丧失可能存在的匹配!其实这一步方法就是前面提到过的:子串从失配点开始,左边存在的字符|与该子串最开始的字符相同的最长字符个数。
我们需要构造一个next[]数组,它的长度就是子串数组的长度,用next[]数组来存储子串字符的回溯下标。

之所以要取最长相同字符个数是因为如果取短了,可能会错过我们要匹配的字符串!
举例:如下图,其实我们如果不取最长相同字符的话,也可以取"aa",也就是 j 回溯到下标为2的地方和 i 继续比较。

看下图,我们发现中间的一些字符被跳过了,万一以它开头的字符串就是我们要找的呢,那我们就完美的错过了心心念念的字符串!可见,一定要找最长的相同字符个数!

next[]数组的手工过程(代码角度)

下面我们就开始愉快的手工过程~
注意:图中的下标 i, j 不再是源串与子串比较时的下标了,而是计算next[]数组时,子串的两个下标!图中的sub[]数组就是存储子串的数组!因为下标为0,1时,失配点左边最多的相同字符数都为0 。所以下标 i 直接从2开始。

i 从 2 开始,j 从 0 开始,若 以(i - 1)为下标的元素与以 j 为下标的元素相同,则 j ++ ,并且把 j 的值赋值给 next[ i ],然后 i++ ;若不相等,则执行( j = next[ j ] ; )操作,然后继续比,相同了就继续 j ++ ,并且把 j 的值赋值给 next[ i ],然后 i++;不相同则执行( j = next[ j ] ; ) 操作;若 j 已经为0了,就不必再执行( j = next[ j ] ; ) 操作了,因为再怎么执行也是0 啊,而且死循环的!这时候直接把 j 的值赋值给 next[ i ] ,然后 i++ 就行了。请结合文字和下面图片一起看!

图片中 【 j = next[ j ] 】这是一步很骚的操作!大家应该也感受到了!这个伟大算法的精妙之处!

当所有的下标计算完成后,大名鼎鼎的next[]数组就完成了,记录它的目的是:若当前下标的字符失配,我们就可以知道当前下标应该回溯到哪个下标位置继续与源串下标为i 的字符进行比较。(j 回溯,不是 i)

这个数组就是KMP算法的核心!有了这个数组,一但字符失配,就去数组里查查下标 j 该回溯到哪里继续与下标 i 比,而下标 i 就可以做到一直屹然不动!当然有一些不用继续比较的条件,我这里没有说,在代码里,大家一看就懂啦!

一年一度的代码环节

kmp.h

#ifndef _MEC_KMP_H_
#define _MEC_KMP_H_

int KMPSearch(const char *str, const char *sub);

#endif

kmp.c

#include <stdio.h>
#include <malloc.h>
#include <string.h>

#include "mec.h"
#include "KMP.h"

static void getNext(const char *str, int *next);
static int searchMainBody(const char *str, const 
		char *sub, int *next, int strLen, int subLen);

static void getNext(const char *str, int *next) {
	int i = 2;
	int j = 0;

	while (str[i]) {
		if (str[i - 1] == str[j] || 0 == j) {
			next[i++] = (j == 0 ? 0 : ++j);
			continue;
		}
		j = next[j];
	}

	
}

static int searchMainBody(const char *str, const char *sub,
		int *next, int strLen, int subLen) {
	int i = 0;
	int j = 0;

	while ((strLen - i) >= (subLen - j)) {
		if (0 == sub[j]) {
			return i - subLen;
		}
		if (str[i] == sub[j]) {
			++i, ++j;
			continue;
		}
		if (0 == j) {
			++i;
			continue;
		}
		j = next[j];
	}

	return NOT_FOUND;
}

int KMPSearch(const char *str, const char *sub) {
	int strLen = strlen(str);
	int subLen = strlen(sub);
	int *next = NULL;
	int result;

	if (NULL == str || NULL == sub 
			|| subLen <= 0 || subLen > strLen) {
		return NOT_FOUND;
	}

	next = (int *) calloc(sizeof(int), subLen);
	if (subLen > 2) {
		getNext(sub, next);
	}

	for(int i = 0;i < subLen;i++){
        printf("%d ",next[i]);
    }

    printf("hh %d ",next );

    printf("\n");

	result = searchMainBody(str, sub, next, strLen, subLen);

	free(next);

	return result;
}

test.c

#include <stdio.h>

#include "KMP.h"

int main() {
	char s[80];
	char sub[80];
	int index;

	printf("请输入原串:");
	gets(s);
	printf("请输入子串:");
	gets(sub);

	index = KMPSearch(s, sub);
	printf("%d\n", index);

	return 0;
}

“mec.h” 要看的话点这里! 直接滑到底看~

结果展示:
在这里插入图片描述

©️2020 CSDN 皮肤主题: 数字20 设计师:CSDN官方博客 返回首页