字符串匹配算法

字符串匹配算法:KMP

情景

    现在有如下字符串 S 和 P,判断 P 是否为 S 的子串。

在这里插入图片描述
    我们仍然按照原来的方式进行比较,比较到 P 的末尾时,我们发现了不匹配的字符。

在这里插入图片描述
    注意,按照原来的思路,我们下一步应将字符串 P 的开头,与字符串 S 的第二位 C 重新进行比较。而 KMP 算法告诉我们,我们只需将字符串 P 需要比较的位置重置到图中 j 的位置,S 保持 i 的位置不变,接下来即可从 i,j 位置继续进行比较。

5.png
    为什么?我们发现字符串 P 有子串 ACT 和 ACY,当 T 和 Y 不匹配时,我们就确定了 S 中的蓝色 AC 并不匹配 P 右侧的 AC,但是可能匹配左侧的 AC,所以我们从位置 i 和 j 继续比较。

    换句话说,Y 对应下标 2,表示下一步要重新开始的地方。

    既然如此,如果每次不匹配的时候,我们都能立刻知道 P 中不匹配的元素,下一步应该从哪个下标重新开始,这样不就能大大简化匹配过程了吗?这就是 KMP 的核心思想。

    KMP 算法中,使用一个数组 next 来保存 P 中元素不匹配时,下一步应该重新开始的下标。由于计算机不能像我们人类一样,通过视觉来得出结论,因此这里有一种适合计算机的构造 next 数组的方法。
    以上内容来自LeetCode

next数组

    现有字符串数组S,模板字符串P, next数组保存的是P 中元素不匹配时,下一步应该重新开始的下标。 假定当前位置为i,next数组用N表示。
    next数组中 N [ i ] = t N[i] = t N[i]=t 表示模板从0位置开始,i位置的字符首次出现不匹配的情况时,t位置之前的字符是不需要匹配的,因为字符串当前位置之前t个字符与模板前t个字符一致,借助next数组可以直接省去这部分过程。
    首先假定在字符串S的j位置时,与之正在匹配的是模板字符串数组P中i位置的字符。此时有:
S [ j − i , j − 1 ] = P [ 0 : i − 1 ] , i ≥ 1 (1) S[j-i,j-1] = P[0:i-1] ,i\ge1 \tag{1} S[ji,j1]=P[0:i1],i1(1)
    此时又有:
N [ i ] = t N[i]=t N[i]=t
    这表示假如 S [ j ] ≠ P [ i ] S[j]\neq P[i] S[j]=P[i],那么再比较 S [ j ] S[j] S[j] P [ t ] P[t] P[t]的值。那如果 S [ j ] ≠ P [ t ] S[j]\neq P[t] S[j]=P[t]呢?
     N [ t ] N[t] N[t]给出了新的开始位置,即在不匹配时,可以不断地使用 t = N [ t ] t=N[t] t=N[t]来更新重新匹配的位置。

构造next数组

     在对字符串数组S和模板P进行字符匹配时,假如已经匹配到模板P的i位置,有公式(1)。如果有:
N [ i ] = t , t < i N[i]=t,t<i N[i]=t,t<i
    不仅意味着S[j]之前t个字符与P前t个字符相同:
S [ j − t : j − 1 ] = P [ 0 : t − 1 ] S[j-t:j-1]=P[0:t-1] S[jt:j1]=P[0:t1]
    由于公式(1)表示的两个子段完全相同,说明P[i]之前t个字符与自身前t个字符也相同
P [ i − t : i − 1 ] = P [ 0 : t − 1 ] P[i-t:i-1]=P[0:t-1] P[it:i1]=P[0:t1]
    我们希望的 N [ i ] = t N[i]=t N[i]=t表示 S [ i ] S[i] S[i]之前与 P P P前t个字符相同,但我们可以借助模板本身,即假如S[i]正在与P[i]匹配,那P[i]前面t个字符与P前t个字符相同的话,等效于 S [ i ] S[i] S[i]之前与 P P P前t个字符相同。这一性质表明完全可以脱离字符串S,只通过P就可以构造一个可以对任何字符串S快速匹配的next数组。也就是说构造next数组的过程是next数组i位置字符前面子段与相对应的next数组0位置开始的子段的比较的过程,而且KMP告诉我们构造next数组的过程与借助next数组匹配字符串的过程类似,不需要很麻烦的去比较。
    假如对i已经有:
N [ i ] = t N[i]=t N[i]=t
    那么:
P [ i − t : i − 1 ] = P [ 0 : t − 1 ] , t < i P[i-t:i-1]=P[0:t-1], t<i P[it:i1]=P[0:t1],t<i
    假如:
P [ i ] = P [ t ] P[i]=P[t] P[i]=P[t]
    意味着:
P [ i − t : i ] = P [ 0 : t ] P[i-t:i]=P[0:t] P[it:i]=P[0:t]
    也就是:
N [ i + 1 ] = t + 1 N[i+1]=t+1 N[i+1]=t+1
    同样的问题来到了模板P上,也就是如果 P [ i ] ≠ P [ t ] P[i] \neq P[t] P[i]=P[t]呢?与之前描述的一样, N [ t ] N[t] N[t]给了答案:
t ← N [ t ] t←N[t] tN[t]
    性质已经给出,最后梳理下构造next数组的大致过程:
    1. 初始化数组N,N.size()=P.size(),i初始化为1
    2. 取 t = N [ i ] t=N[i] t=N[i]比较 P [ i ] P[i] P[i] P [ t ] P[t] P[t],如果相等,则 N [ i + 1 ] = t + 1 N[i+1]=t+1 N[i+1]=t+1,否则 t = N [ t ] t=N[t] t=N[t]
    对next数组的构造过程实际上是通过当前N[i]计算N[i+1],那么N[0]呢?
    显然,i=0时如果不匹配当然要对S下一位置开始,与模板0位置字符开始重新匹配。如果N[0]=0,会引入一个问题,因为P[i=0]=P[t=0],所以N[1]=1。即当S[j]与P[1]位置不匹配时,继续匹配S[j]和P[1],这显然不合理,因为 i = 0 , 1 i=0,1 i=0,1 N [ i ] = t N[i]=t N[i]=t都不满足 t < i t<i t<i。先说下我一开始的构造方法,我选择了对i=0做特殊对待:
    1. 构造next数组时指定N[0] = 0,N[1] = 1。
    2. 从i=1开始循环。

int* buildNext1(const char* P) {
    int str_len = strlen(P);
    int* next_array = new int[str_len];
    int i = 1, t = 0;
    next_array[0] = 0;
    next_array[1] = 0;
    while (i < str_len - 1) {
        if (P[i] == P[t]) {
            ++i;
            ++t;
            next_array[i] = t;
        }
        else if (t == 0) {
            ++i;
            next_array[i] = t;
        }
        else
            t = next_array[t];
    }
    return next_array;
}

    比如模板P为

aabbabcabcdd

    的字符串,其对应的next数组为:

0 0 1 0 0 1 0 0

    由于构造next数组时有了特殊对待,所以在匹配过程中也要有相应的特殊对待:

int match1(const char* P, const char* S) {
    int* next = buildNext(P); 
    int s_len = strlen(S), p_len = strlen(P);
    int i = 0, j = 0;
    while (i < p_len && j < s_len) {
        if (S[j] == P[i]) {
            ++i;
            ++j;
        }
        else if (i == 0) //i = 0时,S上字符要向后移动一位再与P[0]对比
            ++j;
        else
            i = next[i];
    }
    return j - i;
}

    LeetCode这部分给的代码使用我觉得更好的处理方式:既然 N [ i = 0 ] = 0 N[i=0]=0 N[i=0]=0不满足 t < i t<i t<i,那么可以令 N [ 0 ] = − 1 N[0]=-1 N[0]=1,这样一来有以下好处:
    1. N[1]自然而然地等于0,循环可以直接从i=0开始
    2. 所有的 N [ i ] = t N[i]=t N[i]=t都满足 t < i t<i t<i
    3. 代码更简洁
    以下是对应的构造代码和匹配代码:

int* buildNext(const char* P) { // 构造模式串 P 的 next 表
    int m = strlen(P), j = 0; // “主”串指针
    int* N = new int[m]; // next 表
    int  t = N[0] = -1; // 模式串指针
    while (j < m - 1)
        if (0 > t || P[j] == P[t]) { // 匹配
            j++; t++;
            N[j] = t; // 此句可改进为 N[j] = (P[j] != P[t] ? t : N[t]);
        }
        else // 失配
            t = N[t];
    return N;

}
int match(const char* P, const char* S) { // KMP 算法
    int* next = buildNext(P); // 构造 next 表
    int m = (int)strlen(S), i = 0; // 文本串指针
    int n = (int)strlen(P), j = 0; //模式串指针
    while (j < n && i < m) // 自左向右逐个比对字符
        if (0 > j || S[i] == P[j]) // 若匹配,或 P 已移除最左侧
        {
            i++; j++;
        } // 则转到下一字符
        else
            j = next[j]; // 模式串右移(注意:文本串不用回退)
    delete[] next; // 释放 next 表
    return i - j;
}

    对N[0]做特殊对待,体现在代码上是当i==0时,只移动j;而S[j]==P[i]时,同时移动i和j,因此会额外多出一部分不太一样的操作。但当N[0]=-1时,只要j<0或者S[j]==P[i],都可以移动i和j(注意两组代码之间i和j指向的字符串数组不同)。
    为了验证两种方法是否一致,作如下测试:

int main()
{
    const char *P = "ACTGPACY";
    const char *S = "ACTGPACTGKACTGPACY";
    int* N = buildNext(P);
    int* N1 = buildNext1(P);
    for (int i = 0; i < 8; ++i)
        cout << N[i] << ' ';
    cout << endl;
    for (int i = 0; i < 8; ++i)
        cout << N1[i] << ' ';
    cout << endl;
    cout << "my result: " << match1(P, S) << "    " << "result: " << match(P, S) << endl;
    system("Pause");

    输出结果为:

my next array:
-1 0 0 0 0 0 1 2
next array:
0 0 0 0 0 0 1 2
my result: 10
result: 10

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值