算法-字符串匹配KMP算法

1.问题描述

引自:https://leetcode-cn.com/problems/implement-strstr/

这是leetcode上的一道算法题,原题如下:

实现 strStr() 函数。

给你两个字符串 haystack 和 needle ,请你在 haystack 字符串中找出 needle 字符串出现的第一个位置(下标从 0 开始)。如果不存在,则返回 -1 。

解这道题有两种思路:暴力匹配,KMP算法

Java源码中String.indexOf(String str)即采用暴力匹配的方式,这里不再介绍。我们主要来讲KMP算法。

2.问题分析

举个例子:在主串haystack="ABABCABABD"中查找子串needle="ABABD"第一次匹配的位置下标。

  1. 首先取主串和子串的第一个字符进行比较,此时i=0,j=0,H[i]==N[i],匹配成功,比较下一位。
i | 0
  | A B A B C A B A B D
  | A B A B D
j | 0
  1. 接下来发现前4位均匹配成功,在匹配第5位时,i=4,j=4,此时H[i]!=N[i],
i |         4
  | A B A B C A B A B D
  | A B A B D
j |         4

我们需要向右移动子串来继续匹配。通过观察发现,向右移动2位是比较合适的,原因是子串的前2位恰好能够匹配主串中i=4的前2位:

i |         4
  | A B A B C A B A B D
  |     A B A B D
j |         2
  1. 接下来继续做比较,此时i=4,j=2,H[i]!=N[i],需要继续向右移动子串。通过观察发现,只能把子串移到起始位置,即向右移动2位:
i |         4
  | A B A B C A B A B D
  |         A B A B D
j |         0
  1. 此时,i=4,j=0,H[i]!=N[i],主串向左移动1位:
i |           5
  | A B A B C A B A B D
  |           A B A B D
j |           0
  1. 此时,i=5,j=0,H[i]==N[i],比较后续位发现均匹配,返回下标5。

在上述移动子串的过程中,我们是通过观察规律来进行移动的,现在总结一下这些规律:

  1. 子串"ABABD"中包含能够匹配子串起始N个字符的子子串。

比如"ABABD"的开始N[0,1]="AB",在D前面的2个字符N[2,3]="AB",即存在子子串"AB"能够匹配"ABABD"起始2个字符"AB"。

  1. 当主串第i个字符与子串第j个字符不匹配时,找到主串i之前的N个字符,能够与子串起始N个字符匹配,然后快速移动子串。

比如当i=4,j=4时,找到主串i=4之前的2个字符H[2,3]="AB",与子串起始2个字符N[0,1]="AB"匹配,快速移动子串使其与主串对齐。

  1. 两者的关联是,当主串第i个字符与子串第j个字符不匹配时,子串的前j个字符与主串i之前的j个字符是匹配的。

比如当i=4,j=4时,子串的前4个字符N[0,3]="ABAB"与主串'C'前的4个字符H[0,3]="ABAB"是相同的。

此时问题转换成,从主串找i之前的N个字符,变为从子串找j之前的N个字符,与子串起始N个字符匹配。

总结这些规律,我们发现需要一个数组,记录子串中第j个位置,能够最多匹配子串起始位置多少个字符。这就是next数组的含义。

3.next数组

首先,我们生成子串needle="ABABD"的next数组:

      | A B A B D
next  | 0 0 1 2 0

解释一下这个数组的含义:
next[0] = 0,这是固定的,本身没有意义。
next[1] = 0,因为N[1]=’B‘不匹配N[0]='A',所以是0。
next[2] = 1,因为N[2]=’A‘匹配N[0]='A',所以是1。
next[3] = 2,因为N[2,3]='AB'匹配N[0,1]='AB',所以是2。
next[4] = 0,因为N[2,4]='ABD'不匹配N[0,2]='ABA',且N[4]='D'不匹配N[0]='A',所以是0。

看起来很简单,再给一个示例,生成子串"AABAAAB"的next数组:

      | A A B A A A B
next  | 0 1 0 1 2 2 3

为什么next[5]=2呢?因为存在子子串N[4,5]=N[0,1]='AA',所以是2。
为什么next[6]=3呢?因为存在子子串N[4,6]=N[0,2]='AAB',所以是3。

看到这如果你能手动计算next[]数组的话,说明是真正理解了next[]数组的含义。同样就能理解为什么可以使用公共前后缀来计算next数组。

接下来我们要从代码实现的角度来计算next[]数组。

首先,next[0]=0,你可以理解成第1位没有前后缀,所以为0。

接下来我们要计算next[j]的值。我们先来分析下next[j]与next[j-1]之间的关系:

  1. 假设next[j-1]=0,表示N[j]之前没有连续字符匹配起始位置字符,此时只需要与N[0]比较,如果N[0]==N[j],则next[j]=1,否则next[j]=0。

  2. 假设next[j-1]=K(K>0),表示N[j]之前有K个连续字符匹配起始位置K个字符,此时分两种情况:

  • 如果N[K]==N[j],则next[j]=K+1,也就是next[j]=next[j-1]+1。
  • 如果N[K]!=N[j],此时N[0,K-1]==N[j-K,j-1],我们还需要计算是否存在K'(K'<=K),使得N[0,K'-1]==N[j-K'+1,j]。如果不存在,则next[j]=0。

如何计算K'的值呢?此时我们把N[j-K,j]看做一个新的子串,问题转换成求新的子串N[j-K,j]的next'数组。这个next'数组的长度为K+1,且next'[0,K-1]==next[0,K-1],想求next'[K]的值,重复一次上面的步骤即可。

我们把上述的分析转换成代码:

int[] next = new int[needle.length()];
next[0] = 0;
for (int i = 1; i < needle.length(); i++) {
    int K = next[i - 1];
    if (needle.charAt(i) == needle.charAt(K)) {
        next[i] = K + 1;
    } else {
        K = K >= 1 ? next[K - 1] : 0;
        if (needle.charAt(i) == needle.charAt(K)) {
            next[i] = K + 1;
        } else {
            next[i] = 0;
        }
    }
}

再稍加整理优化:

int[] next = new int[needle.length()];
next[0] = 0;
for (int i = 1; i < needle.length(); i++) {
    int j = next[i - 1];
    while (j >= 1 && needle.charAt(i) != needle.charAt(j)) {
        j = next[j - 1]
    }
    if (needle.charAt(i) == needle.charAt(j)) {
        j++;
    }
    next[i] = j;
}

4.KMP算法

字符串比较的部分逻辑很简单,直接给出代码:

int i = 0;
int j = 0;
while (i < haystack.length()) {
    if (haystack.charAt(i) == needle.charAt(j)) {
        i++;
        j++;
    } else if (j >= 1) {
        j = next[j - 1];
    } else {
        i++;
    }
    if (j == needle.length()) {
        return i - j;
    }
}

你可能感兴趣的:(算法-字符串匹配KMP算法)