RodneyX

博客园 首页 新随笔 联系 订阅 管理

笔者在两年前初学KMP算法时遇到非常大的障碍,关键是没看懂转移方程的原理,现在先谈谈这个(本文可能存在某些不标准的叙述)

首先明确一下,对于构造next[]数组我们并不关心主串的信息,KMP的精髓在于利用模式串可能存在的前后自相似特征来决定模式串指针的回溯方式

考虑一个模式串p[] = "aabaabb",假设下标从0开始,我们在下标6处失配(也就是模式串最后一个字符处失配),这表明了此时主串这个对应字符不为'b',又注意到p[0..2]p[3...5]相等(模式串前后缀是相同的),那么主串这个字符会不会是a呢,也就是说现在我们将模式串指针回溯到3就行了,并不需要重头开始匹配

很显然,在匹配过程中,之前部分成功匹配的信息能够帮助我们减少模式串指针回溯距离,并且这一过程中,主串指针不会发生回退

针对之前的讨论,我们可以说在p[6]处失配,那么next[6] = 3next[]数组指明了在p[i]处失配时模式串指针i应当回溯到next[i]处,即i = next[i]

而这个移动的依据从何而来的呢?事实上假设在p[j]处失配,现在有p[0...k] == p[j-k-1...j-1](如之前的例子)那么我们就应当回溯到next[j] = k + 1,另外如p = "aabaac",这里"aabaa" == "aabaa"我们不能说当'c'失配时让指针回溯到自身,这是没用的

并且我们应当尽量少的回溯,也就是说k尽量大,因为回溯多了无外乎是作无用功多匹配一下,但这些信息既然已经得出何必再去匹配

依据这些讨论,我们可以给出如下的动态规划转移方程

next[j] = {
	-1, if j == 0 //因为此时是模式串第一个字符失配,没办法回溯指针,这个-1取得非常巧妙,看了代码就知道
	 max{k} + 1, if p[0...k] == p[j - k - 1... j - 1] //当下表为j的字符失配时,取最大的前后缀,跟前述讨论一致,注意此时最大前后缀长度为k+1,因为下标从0开始
	 0, others	//其他情况,此时只能从头开始匹配故取0
}

那么,有了这些讨论,我们如何求next[j] = k + 1的情况呢?事实上,我们也就是对模式串前后缀作匹配工作,这样就会引出两个指针指向模式串,或者说模式串也是主串。

于是有如下代码

void get_next(char *pattern, int *next, int length) {
    int i = 0, j = -1;	//i作主串指针,我们假设在i处会发生失配
    next[0] = -1; //动态规划初始条件
    while(i < length - 1) {	//这里循环中会先确定增加一个字符会否导致前后缀增加,而这说下一个字符发生失配时的指针回溯依据,因此i < length - 1,注意此处字符串采用C语言中的字符串
        if(-1 == j || pattern[i] == pattern[j]) {   //j = -1时直接自增,此时 j = 0, i = 1,此时若p[1]失配,则之前匹配成功的仅有1个元素,显然为其他情况,于转移公式一致
            ++i;                                    //注意到i指向为模式串后部,j为前部,目标是检查当p[i]失配后,应该转移到何处
            ++j;                                    //这里有个大前提,或者说最优子结构,就是在p中的前后部分别存在两个连续的j个字符构成的相等串
            next[i] = j;                            //事实上,这就是情况2
        }                                           //因此如果说这里追加一个字符也相等,那么显然,此时就是这个j值
        else
            j = next[j];                            //当不相等时,则可以看作此时失配,那么依据next数组的含义,就可以回溯模式串前部指针作新的匹配
    }   //这里的动态规划方式是从前往后的,并且注意到对于next[j]我们只关心前面0...j-1的字符情况所以说这里会对i和j自增
}

于是有如下的字符串匹配算法

int kmp(char *S, char *pattern, int *next, int Slength, int Plength) {
    int i = 0, j = 0;
    while(i < Slength && j < Plength) { //显然的循环成立判断条件
        if(j == -1 || S[i] == pattern[j]) { //按照next数组求法来作所谓依据前后缀自相似的匹配,但是这里不需要修改next数组了
            ++i;
            ++j;
        }
        else
            j = next[j];
    }
    if(j == Plength)    //返回下标索引
        return i - Plength;
    else
        return - 1;
}

至于nextval[]数组这个就比较简单了
直接给出代码

//get_nextval的方法是在get_next的基础上,每一次确定next[i]时,必须要考察p[i]与p[next[i]]的是否一致,当不一致时next[i]就选取此值,否则往前回溯一次,这里注意只回溯一次因为前面是最优子结构,这里比较显然不展开叙述论证
//本质上nextval构造不仅考虑了之前的串匹配情况还综合了当前所指向的这个字符的情况,于是有如下实现
void get_nextval(char *pattern, int *nextval, int length) {
    int i = 0, j = -1;
    nextval[0] = -1;
    while(i < length - 1) {
        if(j == -1 || pattern[i] == pattern[j]) {
            ++i;
            ++j;
            if(pattern[i] != pattern[j])	//因为是最优子结构,只需一次if判断就OK
                nextval[i] = j;
            else
                nextval[i] = nextval[j]; 	//因为是最优子结构pattern[i]必与pattern[next[j]]不等
        }
        else
            j = nextval[j];
    }
}
posted on 2025-07-21 11:56  RodneyX  阅读(24)  评论(0)    收藏  举报