KMP算法介绍及实现步骤解析

KMP算法介绍及实现步骤解析

一、引言

在字符串匹配问题中,常常需要在一个较长的文本字符串中查找一个较短的模式字符串。朴素的字符串匹配方法通常通过逐一比较文本字符串和模式字符串的字符来完成匹配,这种方法的时间复杂度是O(mn),其中m为模式串的长度,n为文本串的长度。这种算法在某些情况下效率较低,尤其是当模式串和文本串较长时,可能导致时间浪费。

为了提高效率,KMP(Knuth-Morris-Pratt)算法应运而生。KMP算法通过预处理模式串,避免了重复的字符比较,极大提高了匹配效率,最终将时间复杂度降至O(m+n),其中m为模式串的长度,n为文本串的长度。

二、KMP算法的核心思想

KMP算法的核心思想是通过模式串的部分匹配表(也称为前缀函数,或失配函数),来减少匹配失败时的回退次数。具体来说,KMP算法避免了对已经匹配过的部分的重复比较。它的核心步骤是:

  1. 通过预处理模式串,构建部分匹配表(或前缀函数)
  2. 利用部分匹配表,在模式串和文本串失配时,直接跳过已经匹配过的部分,继续匹配,从而提高效率。

1. 部分匹配表(前缀函数)

部分匹配表用于记录模式串中每个位置前缀和后缀的最长匹配长度。具体来说,对于模式串中的每一个位置i,记录的是该位置之前的子串中,最长的既是前缀也是后缀的部分的长度。

例如,对于模式串"ABABAC",我们可以通过分析各个位置的前后缀来构建部分匹配表:

模式串: A B A B A C
前缀表: 0 0 1 2 3 0

这里,第一个位置的前缀表值为0,表示没有前后缀匹配;第二个位置的前缀表值为0,表示没有匹配的前后缀;第三个位置的前缀表值为1,表示存在一个前缀A和后缀A匹配,依此类推。

2. 失配跳跃

当进行匹配时,若发生了字符不匹配,我们可以利用前缀表的信息,将模式串从已匹配的部分跳跃到另一个可能的匹配位置,而不必回退文本串指针,极大地减少了无谓的比较。

三、KMP算法的实现步骤

KMP算法主要包括两个步骤:

  1. 构建部分匹配表
  2. 利用部分匹配表进行高效的匹配过程

1. 构建部分匹配表(前缀函数)

我们需要为模式串构建一个数组lps(Longest Prefix Suffix),其中lps[i]表示模式串从位置0到位置i的子串的最长前后缀的长度。

构建lps数组的算法:

  1. 初始化lps[0] = 0
  2. 使用两个指针,i表示当前字符的位置,length表示当前匹配的前后缀的长度。
  3. 如果当前字符匹配,则更新lps[i] = length + 1,并继续检查下一个字符。
  4. 如果当前字符不匹配,则通过lps[length - 1]来跳过部分匹配的字符,调整length,继续匹配。

代码实现:

```python
def compute_lps(pattern):
m = len(pattern)
lps = [0] * m
length = 0 # length of previous longest prefix suffix
i = 1

while i < m:
    if pattern[i] == pattern[length]:
        length += 1
        lps[i] = length
        i += 1
    else:
        if length != 0:
            length = lps[length - 1]
        else:
            lps[i] = 0
            i += 1
return lps

```

示例:

对于模式串"ABABAC",调用compute_lps函数将得到以下前缀表:

模式串: A B A B A C
前缀表: 0 0 1 2 3 0

2. 利用部分匹配表进行匹配

利用部分匹配表,我们可以在匹配失败时跳过已经匹配的部分,避免重复计算。

KMP匹配算法步骤:

  1. 初始化文本指针i = 0,模式串指针j = 0
  2. 比较text[i]pattern[j],如果相同,则继续匹配i++j++
  3. 如果text[i]pattern[j]不匹配:
  4. 如果j不为0,说明模式串部分已经匹配过,可以通过lps[j-1]来跳到新的位置j = lps[j-1],不需要回退文本指针i
  5. 如果j == 0,则文本指针i++,继续向后移动。

代码实现:

```python
def KMP_search(text, pattern):
n = len(text)
m = len(pattern)

# Step 1: 计算模式串的部分匹配表
lps = compute_lps(pattern)

i = 0  # 文本串指针
j = 0  # 模式串指针

while i < n:
    if text[i] == pattern[j]:
        i += 1
        j += 1

    if j == m:  # 找到匹配
        print(f"Pattern found at index {i - j}")
        j = lps[j - 1]  # 根据部分匹配表跳跃

    elif i < n and text[i] != pattern[j]:  # 失配
        if j != 0:
            j = lps[j - 1]
        else:
            i += 1

```

示例:

假设文本串为"ABABABAC",模式串为"ABABAC",我们执行KMP_search("ABABABAC", "ABABAC"),可以得到输出:

Pattern found at index 0

四、KMP算法的时间复杂度分析

  1. 计算部分匹配表:构建前缀表的时间复杂度是O(m),其中m是模式串的长度。
  2. 匹配过程:在匹配过程中,文本指针i和模式指针j分别最多移动n和m次,所以匹配的时间复杂度是O(n)。

因此,KMP算法的整体时间复杂度为O(m + n),比朴素匹配算法的O(m * n)要高效得多,尤其是在模式串和文本串都比较长时,优势尤为明显。

五、总结

KMP算法通过使用部分匹配表(前缀函数)来优化字符串匹配过程,避免了重复的字符比较。其时间复杂度为O(m + n),比朴素算法的O(m * n)要高效得多。通过预处理模式串并利用失配时的跳跃机制,KMP算法大大提高了字符串匹配的效率,是经典的字符串匹配算法之一。

THE END