KMP

Knuth-Morris-Pratt(KMP)算法是一种用于字符串匹配的高效算法。它在比较字符串的过程中,利用了已知信息,避免了不必要的比较。这种算法的核心思想是当子串与目标字符串不匹配时,能知道一部分已经匹配的子串,利用这些信息让子串尽可能地移动到正确的位置再继续进行比较。

def compute_prefix_function(pattern):
  prefix_function = [0] * len(pattern)
  j = 0

  for i in range(1, len(pattern)):
    while j > 0 and pattern[j] != pattern[i]:
    j = prefix_function[j - 1]
    if pattern[j] == pattern[i]:
    j += 1
    prefix_function[i] = j
  return prefix_function

def KMP(pattern, text):
  prefix_function = compute_prefix_function(pattern)
  j = 0
  result = []

  for i in range(len(text)):

  while j > 0 and pattern[j] != text[i]:
    j = prefix_function[j - 1]
  if pattern[j] == text[i]:
    j += 1
  if j == len(pattern):
    result.append(i - (j - 1))
  j = prefix_function[j - 1]

  return result

# example usage
text = "ABABDABACDABABCABAB"
pattern = "ABABCABAB"
print(KMP(pattern, text)) # prints [10], because the pattern starts at index 10 in the text

在这个代码中:

  • compute_prefix_function 函数用来预处理模式字符串,计算出前缀函数。前缀函数对于每一个位置 i,记录了到 i 为止最长的相同前缀后缀的长度。
  • KMP 函数执行实际的 KMP 搜索。它使用前缀函数来决定当在文本中发现不匹配时,应该将模式字符串移动到哪里。

这个算法的时间复杂度是 O(m + n),其中 m 是模式字符串的长度,n 是文本字符串的长度。

 

 

KMP 算法可能确实有些难以理解,尤其是第一次接触。我会尽力解释一下这段代码的主要部分。

在 KMP 算法中,有一个被称为 "前缀函数"(或者 "部分匹配表")的概念。这是一个数组,其中的每一个元素对应于模式串的每一个位置,表示该位置左侧的子串中,最长的相同的前后缀的长度。

例如,对于模式串 "ABABCABAB",它的前缀函数为:[0, 0, 1, 2, 0, 1, 2, 3, 4]。它的解读方式是:

  • 在第一个位置('A'),没有前缀和后缀,所以是 0。
  • 在第二个位置('B'),最长的相同前后缀是 '',长度是 0。
  • 在第三个位置('A'),最长的相同前后缀是 'A',长度是 1。
  • 在第四个位置('B'),最长的相同前后缀是 'AB',长度是 2。
  • 在第五个位置('C'),没有相同的前后缀,所以是 0。
  • ……以此类推。

compute_prefix_function 函数就是用来计算这个前缀函数的。它会扫描整个模式串,每一步中,都会尝试找到最长的相同的前后缀。

有了前缀函数之后,我们就可以执行 KMP 算法了。KMP 算法会扫描整个文本串,每一步中,都会尝试将文本串的一个字符和模式串的一个字符进行比较。如果这两个字符相同,那么就继续向后比较;如果这两个字符不同,那么就利用前缀函数,将模式串向右滑动到一个可能会让它匹配的位置。

具体来说,如果在某个位置 i,文本串和模式串不匹配,那么我们知道在模式串的 0 到 j-1 位置(j 是当前不匹配的位置),都和文本串是匹配的。因此,模式串的 0 到 prefix[j-1] 的部分,会和文本串的 i-prefix[j-1] 到 i 的部分匹配。这样,我们就可以直接将模式串滑动到位置 prefix[j-1],然后继续比较。

这就是 KMP 算法的基本思想,希望我解释得足够清楚。如果你对某个部分还有疑问,欢迎随时向我提问。

 

让我们用一个具体的例子来说明:假设我们有一个文本串 "ABABDABACDABABCABAB",我们要在这个文本串中寻找模式串 "ABABCABAB"。

首先,我们计算模式串的前缀函数:[0, 0, 1, 2, 0, 1, 2, 3, 4]。

然后,我们从左到右开始扫描文本串,与此同时,也从左到右扫描模式串。在开始的时候,都在位置 0,两个 'A' 匹配,所以都向右移动一位。接下来的 'B' 也匹配,再向右移动一位。这样一直到位置 3,文本串上的字符是 'A',模式串上的字符是 'A',依然匹配。但是,当我们再向右移动一位时,就遇到了不匹配——文本串上的字符是 'B',模式串上的字符是 'C'。

此时,我们就需要使用前缀函数了。我们知道在模式串的位置 0 到 3,都和文本串匹配,所以我们查看前缀函数的位置 3,得到 2。这意味着,模式串的位置 0 到 1(长度为 2)和位置 2 到 3 是相同的。所以,我们可以将模式串向右滑动 2 个位置,然后继续比较。

滑动之后,模式串的位置 2 上的字符 'A' 和文本串的位置 4 上的字符 'B' 不匹配,所以我们再次使用前缀函数。前缀函数的位置 2 上的值是 1,所以我们将模式串向右滑动 1 个位置,然后继续比较。

这样一直进行下去,直到我们找到了匹配的模式串或者扫描完了整个文本串。

希望这个文字描述可以帮到你理解 KMP 算法的过程。如果你对某个部分还有疑问,欢迎随时向我提问。

 
 
为什么在判断完j==len(pattern)里面还要加上j = prefix_function[j - 1]
 

这是因为当找到一个匹配(即 j == len(pattern))时,我们不应立即结束搜索,因为可能存在其他的匹配。

以 "ABABCABAB" 在 "ABABDABACDABABCABAB" 中的匹配为例,我们看到第一个 "ABABCABAB" 在索引10的位置开始,但是在索引13的位置,又开始了一个新的 "ABABCABAB",这就是一个重叠的匹配。

j = prefix_function[j - 1] 这一行的目的就是为了处理这样的重叠匹配。当我们找到一个匹配后,我们需要将 j 向左移动,继续查找可能的重叠匹配。我们不能简单地将 j 设为 0,因为这样就丢失了模式串已经匹配的部分。相反,我们应该将 j 设为 prefix_function[j - 1],这样就可以保持模式串已经匹配的部分,并在此基础上继续查找可能的重叠匹配。

例如,对于模式串 "ABABCABAB",当我们在文本串找到一个匹配后,我们不能简单地将 j 设为 0,因为这样就会丢失已经匹配的 "ABAB"。相反,我们应该将 j 设为 prefix_function[8] = 4,这样就可以保持已经匹配的 "ABAB",并在此基础上继续查找可能的重叠匹配。

这就是为什么在 j == len(pattern) 后,我们需要将 j 设为 prefix_function[j - 1] 的原因。

 
 
 
 
posted @ 2023-08-07 11:12  狼太白  阅读(50)  评论(0编辑  收藏  举报