什么是子串查找
字符串子串查找是一种在较长的字符串(通常称为"主串"或"文本")中寻找一个较短字符串(称为"模式串"或"子串")的过程。这是计算机科学中的一个基本问题,在文本编辑、信息检索、生物信息学等多个领域都有广泛应用。
主要的子串查找算法包括:
- 暴力匹配法(Brute Force)
- KMP算法(Knuth-Morris-Pratt)
- Boyer-Moore算法
- Rabin-Karp算法
- Sunday算法
每种算法都有其特点和适用场景。例如,暴力匹配法简单直观但效率较低,而KMP算法通过预处理模式串来提高匹配效率。接下来我们将讲解暴力匹配法和KMP算法。
暴力查找算法
子串查找的暴力匹配法(Brute Force)是最直观、最简单的字符串匹配算法。这种方法通过逐个比较主串中的字符与模式串中的字符来实现匹配。
算法步骤如下:
- 从主串的第一个字符开始,逐一与模式串的第一个字符比较。
- 如果相匹配,则继续比较主串和模式串的下一个字符。
- 如果不匹配,则主串向右移动一位,重新从步骤1开始。
- 重复上述步骤,直到找到完全匹配的子串或主串中剩余的字符数小于模式串的长度。
Python实现示例如下:
def brute_force_search(main_string, pattern):
m = len(main_string)
n = len(pattern)
for i in range(m - n + 1):
j = 0
while j < n and main_string[i + j] == pattern[j]:
j += 1
if j == n:
return i # 找到匹配,返回起始索引
return -1 # 未找到匹配
# 测试
main = "ABABCABCABABABD"
pat = "ABABD"
result = brute_force_search(main, pat)
print(f"Pattern found at index: {result}")
很明显,这个算法的时间复杂度是O(m*n),其中m是主串的长度,n是模式串的长度。在最坏情况下,可能需要比较主串中的每个字符与模式串的每个字符。
暴力匹配法的优点是简单易懂、容易实现,对于短文本或者不经常进行的匹配操作来说是可以接受的。但对于长文本或频繁的匹配操作,这种方法的效率较低,可能需要考虑使用更高效的算法。
什么是KMP
KMP算法(Knuth-Morris-Pratt算法)是一种高效的字符串匹配算法,由Donald Knuth、James H. Morris和Vaughan Pratt在1977年共同发明。它通过利用已经部分匹配这个有效信息,避免了暴力匹配中的大量回溯,从而显著提高了匹配效率。
KMP算法的核心思想包括:
- 预处理模式串,生成部分匹配表(next数组)。
- 利用这个表在匹配失败时快速滑动模式串,减少不必要的比较。
KMP算法的主要步骤:
- 预处理阶段:计算模式串的部分匹配表。
- 匹配阶段:利用部分匹配表进行快速匹配。
KMP算法的优势很明显,它的时间复杂度只有 O(m + n),其中m是主串长度,n是模式串长度;不需要像暴力匹配那样回溯主串,而是利用部分匹配表向前滑动子串;当你了解了next数组(部分匹配表,或者叫 fail表、转移数组)后,它对于包含大量重复子串的字符串特别有效。
KMP代码解析
def build_next(pattern):
m = len(pattern)
next = [0] * m
next[0] = -1
j = -1
i = 0
while i < m - 1:
if j == -1 or pattern[i] == pattern[j]:
i += 1
j += 1
next[i] = j
else:
j = next[j]
return next
def kmp_search(text, pattern):
n = len(text)
m = len(pattern)
next = build_next(pattern)
i = 0 # 指向text
j = 0 # 指向pattern
while i < n and j < m:
if j == -1 or text[i] == pattern[j]:
i += 1
j += 1
else:
j = next[j]
if j == m:
return i - j # 返回匹配的起始位置
else:
return -1 # 没有找到匹配
if __name__ == '__main__':
# 使用示例
text = "ABABDABACDABABCABAB"
pattern = "ABABCABAB"
result = kmp_search(text, pattern)
if result != -1:
print(f"Pattern found at index {result}")
else:
print("Pattern not found")
从代码中可以看到,kmp_search和暴力查找的区别是 1. i 不会回溯,即text文本不回溯;2. 当匹配失败时,j 会通过部分匹配表(next数组)进行转移。所以,next数组是什么以及next数组如何计算就是KMP的核心问题。
next数组是什么
定义:next[i]表示子串p[0]~p[i]的最长相等前后缀的长度
前缀:除了最后一个字符外,一个字符串的所有头部子串。
后缀:除了第一个字符外,一个字符串的所有尾部子串。
用途:当匹配失败时,next数组告诉算法应该将模式串向右滑动多少位。
长度:next数组的长度与模式串相同。
构建过程:通过分析模式串本身的结构来生成,不需要主串参与。
next数组如何计算
在计算next数组时,一定要注意“next数组”和“最大前缀后缀公共元素长度数组”的区别。举例说明。字符串 “abab”,
pattern | a | b | a | b |
最大前缀后缀公共元素长度 | 0 | 0 | 1 | 2 |
next | -1 | 0 | 0 | 1 |
next 数组存储的是除当前字符外的最长相同前缀后缀,所以计算出各个前缀后缀的公共元素的最大长度后,将其值整体右移一位,然后初值赋为-1,即是next数组。
理解 build_next函数中的 j = next[j]
或者换一个问题,为什么build_next中也有 j = next[j]
1. j的含义:
在构建next数组时,j表示当前正在比较的前缀结束位置。它也代表了已经匹配的长度。
2. next[j]的含义:
next[j]存储的是长度为j的前缀子串中,最长相等前后缀的长度。换句话说,它告诉我们如果在j位置失配,应该跳转到哪个位置继续比较。
3. j = next[j]的作用:
当在位置j发生失配时,我们不需要回到起始位置重新开始,而是可以跳转到next[j]指示的位置。这个位置是最长的可能还能匹配的位置。
4. 为什么这样做是正确的:
- 如果在j处失配,意味着0到j-1的部分是匹配的。
- next[j]告诉我们这个匹配部分的最长相等前后缀长度。
- 将j更新为next[j]后,我们实际上是将模式串向右滑动,使其前缀对齐于主串中已匹配部分的后缀。
5. 举例说明:
对于模式串"ABABC",假设我们在C处失配,此时j=4(因为ABAB已匹配)。
next[4] = 2,意味着"ABAB"的最长相等前后缀是"AB"。
执行j = next[j],即j = next[4] = 2,我们就跳转到了模式串的第三个字符(索引2)。
这相当于将模式串向右滑动了2位,使其前缀"AB"对齐于主串中已匹配部分"ABAB"的后缀"AB"。
6. 优化效果:
- 避免了不必要的比较。我们利用已知的匹配信息,跳过了肯定会匹配的部分。
- 保证了算法的线性时间复杂度。每次失配都能快速找到下一个可能的匹配位置,而不是回到起始位置重新比较。
7. 递归性质:
如果在新的位置还是失配,我们会继续执行j = next[j],直到找到一个匹配或者j变为0。
j = next[j] 这个操作体现了KMP算法的核心思想:利用已经得到的部分匹配信息,避免重复比较,从而加速整个匹配过程。这是KMP算法相比暴力匹配法的主要优势所在。从这个过程来看,build_next的过程,其实也是一个KMP算法的过程,而且还利用了前面已经计算得知的部分next值。例如字符串 “ababadc”,当进行到字符“d”的时候,此时的next数组为:
pattern | a | b | a | b | a | d | c |
next | -1 | 0 | 0 | 1 | 2 | 3 |
,当 i 指向 “c”时,此刻 j 指向 “b”,实际上是在将前缀 “abab” 匹配 后缀 “abad”,显然不匹配,此时 j=3,next[j] = next[3] = 1, 其含义为将“abab”中的前缀“ab”与“abad”中的后缀“ad”对齐,此时再比较“d” 和 “b”,发现不相等,此时 j = 1 ,next[1] = 0, 其含义为将“abab”中的前缀“a”与“abad”中的后缀“d”对齐,比较发现不相等,此时 j = 0, next[0] == -1, 很显然没有公共前后缀,next['c'],也就是next[6] = 0。
KMP的优化
接上一节最后的例子,“ababadc”,当字符进行到最后一个,也就是“c”的时候,我们发现 “abab” 和“abad”不匹配,然后有继续回溯到“ab” 和 “ad”不匹配。这时我们发现,前面“abab”和“abad”匹配的时候,我已经知道“b” 和“d”不匹配了,后面为什么还要再来一次匹配比较呢?这就引出了KMP的优化,
def build_next(pattern):
m = len(pattern)
next = [0] * m
next[0] = -1
j = -1
i = 0
while i < m - 1:
if j == -1 or pattern[i] == pattern[j]:
i += 1
j += 1
if pattern[i] != pattern[j]:
next[i] = j
else:
next[i] = next[j]
else:
j = next[j]
return next
这个判断的含义是,当回溯字符等于当前字符时,需要继续回溯,不然就会白白多一次判断(前面已经判断了不相等,回溯后的字符相同,再判断一次也还是不相等)。
参考文档
https://www.cnblogs.com/zzuuoo666/p/9028287.html
标签:子串,匹配,后缀,pattern,next,算法,查找,KMP From: https://blog.csdn.net/xy2006860/article/details/141128247