算法学习笔记 KMP算法之 next 数组详解

最近回顾了下字符串匹配 KMP 算法,相对于朴素匹配算法,KMP算法核心改进就在于:待匹配串指针 i 不发生回溯,模式串指针 j 跳转到 next[j],即变为了 j = next[j]. 由此时间复杂度由朴素匹配的 O(m*n) 降到了 O(m+n), 其中模式串长度 m, 待匹配文本串长 n.

其中,比较难理解的地方就是 next 数组的求法。next 数组的含义:代表当前字符之前的字符串中,有多大长度的相同前缀后缀,也可看作有限状态自动机的状态,而且从自动机的角度反而更容易推导一些。

  • "前缀"指除了最后一个字符以外,一个字符串的全部头部组合;
  • "后缀"则指除了第一个字符以外,一个字符串的全部尾部组合。

next 数组推导

求 next 数组用到了递推,算法的构建思想就是:

已知初始 next[0] = -1, next[j] = k, 来求 next[j+1] 是多少,有点儿像数学归纳法的套路。

由 next[j] = k, 我们知道 p[k-1] = p[j-1], 前缀 [0] ~ [k-1] , 后缀则从 [j-k] ~ [j-1], 长度都为 k。接下来 next[j+1] 求法就分两种情况讨论:

如果 p[k]  = p[j], 很显然 next[j+1] = next[j] + 1 = k+1;

如果 p[k] != p[j], 那么说明匹配的前后缀更短,前缀里要找一个最后字符 p[k‘] = p[j], 前缀再往前推, 同时要保证 [0] ~ [k‘] = [j-k‘] ~ [j],k‘+ 1 就是要求的 next[j+1]. 这个符合条件的 k‘ 要怎么推,由 k 依次递减吗?显然不是,是由 k=next[k] 的往前跳;

由 next[j]=k ,我们已知了 [0] ~ [k-1] 等于 [j-k] ~ [j-1], 那么下一步我们在 [0] ~ [k-1] 中取最大匹配的前后缀,其正好可由 next[k] 求出,即:[0]~[next[k]-1] 等于 [k-next[k]]~[k-1], 有 next[k] 个字符匹配。

因为 [0] ~ [k-1] 等于 [j-k] ~[j-1], 所以在 [j-k] ~ [j-1] 也存在同样的最大前后缀匹配,我们取 [0] ~ [k-1] 中的前缀 [0]~[next[k]-1], [j-k] ~ [j-1] 中的后缀 [j-next[k]] ~ [j-1],它们必然相等,然后再进一步判断 p[j] 是否等于 p[next[k]] 即可,由此递推完成匹配 p[k‘]=p[j] 或者 递推到了 k=next[0]=-1
结束。

这种思路直接转换为代码就是:

void GetNext(char* p, int next[]) {
    next[0] = -1;
    int pLen = strlen(p), j, k;
    for (j = 0; j < pLen - 1; j++) {
        k = next[j];
        while (k != -1 && p[k] != p[j]){
            k = next[k];          // 在前缀里找 p[k'] = p[j],前缀=后缀,前缀的前缀 = 后缀的后缀,递推。
        }
        next[j + 1] = k + 1;      // 如果递推到了 next[0], 无匹配为0;递推找到 p[k']=p[j], next[j+1] = next[k']+1
    }
}

代码思路不变,进行一下优化:

void GetNext2(char* p, int next[]) {
    next[0] = -1;
    int j = 0, k = -1, pLen = strlen(p);
    while (j < pLen - 1) {
        if (k != -1 && p[k] != p[j])
            k = next[k];
        else
            next[++j] = ++k;     // <=> {++j; ++k; next[j] = next[k]}
    }
}

优化 next 数组

上面的 next 数组还是存在多余的跳转,进行一步判断优化即可:

void GetNextVal(char* p, int next[]) {
    next[0] = -1;
    int j = 0, k = -1, pLen = strlen(p);
    while (j < pLen - 1) {
        if (k != -1 && p[k] != p[j]) {
            k = next[k];
        } else {
            if (p[++k] != p[++j])
                next[j] = k;
            else
                next[j] = next[k]; // 比较前面改进相当于这里 next[j] = k 变为 next[j] = next[k], 多递推一次
        }                          // kmp 比较 s[i] != p[j], p[j] 跳到 p[next[j]], 而 p[j] = p[next[j]], 故多递推一次
    }
}

KMP 匹配主算法

int KmpSearch(char* s, char* p, int *next) {
    int sLen = strlen(s);
    int pLen = strlen(p);
    int i = 0, j = 0;
    while (i < sLen && j < pLen) {
        if (j == -1 || s[i] == p[j]) {     // 相对于朴素匹配,多判断下 j == -1, 因为 next[0]=-1
            i++;
            j++;
        } else {
            j = next[j];                 // 相对于朴素匹配,没有了指针 i 的回溯,j 跳转到 next[j]
        }
    }
    if (j == pLen)
        return i - j;
    else
        return -1;
}

小实验(C实现)

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

void GetNext(char* p, int next[]) {
	next[0] = -1;
	int j = 0, k = -1, pLen = strlen(p);
	while (j < pLen - 1) {
		if (k != -1 && p[k] != p[j])
			k = next[k];
		else
			next[++j] = ++k;
	}
}

void GetNext2(char* p, int next[]) {
	next[0] = -1;
	int pLen = strlen(p), j, k;
	for (j = 0; j < pLen - 1; j++) {
		k = next[j];
		while (k != -1 && p[k] != p[j]){
			k = next[k];
		}
		next[j + 1] = k + 1;
	}
}

void GetNextVal(char* p, int next[]) {
	next[0] = -1;
	int j = 0, k = -1, pLen = strlen(p);
	while (j < pLen - 1) {
		if (k != -1 && p[k] != p[j]) {
			k = next[k];
		} else {
			if (p[++k] != p[++j])
				next[j] = k;
			else
				next[j] = next[k];
		}
	}
}

int KmpSearch(char* s, char* p, int *next) {
	int sLen = strlen(s);
	int pLen = strlen(p);
	int i = 0, j = 0;
	while (i < sLen && j < pLen) {
		if (j == -1 || s[i] == p[j]) { // 相对于朴素匹配,多判断下 j == -1, 因为 next[0]=-1
			i++;
			j++;
		} else {
			j = next[j]; // 相对于朴素匹配,没有了指针 i 的回溯,j 跳转到 next[j]
		}
	}
	if (j == pLen)
		return i - j;
	else
		return -1;
}

int main() {
	char *s = "BBC ABCDAB ABCDABCDABDE";
	char *p = "ABCDABD";
	int n = strlen(p);
	int next[n], next2[n], nextVal[n];
	int index, indexVal, i;

	GetNext(p, next);
	index = KmpSearch(s, p, next);

	GetNext2(p, next2);

	GetNextVal(p, nextVal);
	indexVal = KmpSearch(s, p, nextVal);

	for (i = 0; i < n; i++)
		printf("%d\t", next[i]);
	printf("\n");

	for (i = 0; i < n; i++)
		printf("%d\t", next2[i]);
	printf("\n");

	for (i = 0; i < n; i++)
		printf("%d\t", nextVal[i]);
	printf("\n");

	printf("%d\t%d", index, indexVal);
	return 0;
}

/* 输出:
-1	0	0	0	0	1	2
-1	0	0	0	0	1	2
-1	0	0	0	-1	0	2
15	15
 */

参考

字符串匹配的KMP算法 - 阮一峰

从头到尾彻底理解KMP - JULY

【原文地址】:http://blog.csdn.net/thisinnocence

算法学习笔记 KMP算法之 next 数组详解

时间: 2024-10-12 19:56:05

算法学习笔记 KMP算法之 next 数组详解的相关文章

Cocos2d-x学习笔记(十四)CCAutoreleasePool详解

原创文章,转载请注明出处:http://blog.csdn.net/sfh366958228/article/details/38964637 前言 之前学了那么多的内容,几乎所有的控件都要涉及内存管理类CCAutoreleasePool,所以这一次的学习笔记,我们一起来看看CCAutoreleasePool,先从CCObject的autorelease方法入手. CCObject::autorelease CCObject* CCObject::autorelease(void) { // 将

Kinect学习笔记之三Kinect开发环境配置详解

0.前言: 首先说一下我的开发环境,Visual Studio是2013的,系统是win8的64位版本,SDK是Kinect for windows SDK 1.8版本.虽然前一篇博文费了半天劲,翻译了2.0SDK的新特性,但我还是决定要回退一个版本. 其实我之前一直在用2.0的SDK在调试Kinect,但无奈实验室提供的Kinect是for Windows 1.0版本的,而且Kinect从1.8之后就好像是一个分水岭,就比如win8和win7有很大的差别,2.0版的Kinect和SDK都是相较

算法学习笔记:最大连续子数组

寻找最大连续子数组 这两天看了看数据结构与算法,对其中一个问题颇感兴趣,所以在这里写一下.问题:寻找最大连续子数组. 问题:在一个有正有负的数组中,寻找一个连续的.和最大的子数组.这个数组类似于下面的数组,否则这个问题没有意义(如果全是正数的话,所有数组元素的和一定是最大的,同样全为负数也没有意义.). int a={1,-2,3,45,-78,34,-2,6}; 解法一:暴力求解. 那么如何来解决这个问题呢?这个思路要起来并不难,绝大多数人会想到这样的办法:遍历该数组的所有子数组,找到和最大的

【算法学习笔记】40.树状数组 动态规划 SJTU OJ 1289 扑克牌分组

Description cxt的扑克牌越来越先进了,这回牌面的点数还可以是负数, 这回cxt准备给扑克牌分组,他打算将所有的牌分成若干个堆,每堆的牌面总和和都要大于零.由于扑克牌是按顺序排列的,所以一堆牌在原牌堆里面必须是连续的.请帮助cxt计算一下,存在多少种不同的分牌的方案.由于答案可能很大,只要输出答案除以1,000,000,009的余数即可. Input Format 第一行:单个整数:N,1 ≤ N ≤ 10^6 第二行到N + 1行:在第i + 1行有一个整数:Ai, 表示第i张牌牌

[算法学习笔记]排序算法——堆排序

堆排序 堆排序(heapsort)也是一种相对高效的排序方法,堆排序的时间复杂度为O(n lgn),同时堆排序使用了一种名为堆的数据结构进行管理. 二叉堆 二叉堆是一种特殊的堆,二叉堆是完全二叉树或者是近似完全二叉树.二叉堆满足堆特性:父节点的键值总是保持固定的序关系于任何一个子节点的键值,且每个节点的左子树和右子树都是一个二叉堆. 如上图显示,(a)是一个二叉堆(最大堆), (b)是这个二叉堆在数组中的存储形式. 通过给个一个节点的下标i, 很容易计算出其父节点,左右子节点的的下标,为了方便,

深度学习笔记——PCA原理与数学推倒详解

PCA目的:这里举个例子,如果假设我有m个点,{x(1),...,x(m)},那么我要将它们存在我的内存中,或者要对着m个点进行一次机器学习,但是这m个点的维度太大了,如果要进行机器学习的话参数太多,或者说我要存在内存中会占用我的较大内存,那么我就需要对这些个点想一个办法来降低它们的维度,或者说,如果把这些点的每一个维度看成是一个特征的话,我就要减少一些特征来减少我的内存或者是减少我的训练参数.但是要减少特征或者说是减少维度,那么肯定要损失一些信息量.这就要求我在减少特征或者维度的过程当中呢,尽

JQuery学习笔记系列(一)----选择器详解

笔者好长时间没有更新过博客园的笔记了,一部分原因是去年刚刚开始工作一段时间忙碌的加班,体会了一种每天加班到凌晨的充实感,之后闲暇时间了也因为自己懒惰没有坚持记笔记的习惯,现在重新拾起来. 借用古人的一段话与诸君共勉: 人之为学,不日进则日退,独学无友,则孤陋而难成:久处一方,则习染而不自觉.不幸而在穷僻之域,无车马之资,犹当博学审问, 古人与稽,以求其是非之所在.庶几可得十之五六.若既不出户,又不读书,则是面墙之士,虽子羔.原宪之贤,终无济于天下. 翻译为:人们求学(或做学问),不能天天上进,就

iOS5 ARC学习笔记:strong、weak等详解

iOS5中加入了新知识,就是ARC,其实我并不是很喜欢它,因为习惯了自己管理内存.但是学习还是很有必要的. 在iOS开发过程中,属性的定义往往与retain, assign, copy有关,我想大家都很熟悉了,在此我也不介绍,网上有很多相关文章. 现在我们看看iOS5中新的关键字strong, weak, unsafe_unretained. 可以与以前的关键字对应学习strong与retain类似,weak与unsafe_unretained功能差不多(有点区别,等下会介绍,这两个新 关键字与

Objective-C学习笔记:defer的实现方法详解

这篇文章会对 libextobjc 中的一小部分代码进行分析,也是如何扩展 Objective-C 语言系列文章的第一篇,笔者会从 libextobjc 中选择一些黑魔法进行介绍. 对 Swift 稍有了解的人都知道,defer 在 Swift 语言中是一个关键字:在 defer 代码块中的代码,会在作用域结束时执行.在这里,我们会使用一些神奇的方法在 Objective-C 中实现 defer. 如果你已经非常了解 defer 的作用,你可以跳过第一部分的内容,直接看 Variable Att