子串查找算法KMP

什么是子串查找

        字符串子串查找是一种在较长的字符串(通常称为"主串"或"文本")中寻找一个较短字符串(称为"模式串"或"子串")的过程。这是计算机科学中的一个基本问题,在文本编辑、信息检索、生物信息学等多个领域都有广泛应用。

主要的子串查找算法包括:

  1. 暴力匹配法(Brute Force)
  2. KMP算法(Knuth-Morris-Pratt)
  3. Boyer-Moore算法
  4. Rabin-Karp算法
  5. Sunday算法

每种算法都有其特点和适用场景。例如,暴力匹配法简单直观但效率较低,而KMP算法通过预处理模式串来提高匹配效率。接下来我们将讲解暴力匹配法和KMP算法。

暴力查找算法

        子串查找的暴力匹配法(Brute Force)是最直观、最简单的字符串匹配算法。这种方法通过逐个比较主串中的字符与模式串中的字符来实现匹配。

算法步骤如下:

  1. 从主串的第一个字符开始,逐一与模式串的第一个字符比较。
  2. 如果相匹配,则继续比较主串和模式串的下一个字符。
  3. 如果不匹配,则主串向右移动一位,重新从步骤1开始。
  4. 重复上述步骤,直到找到完全匹配的子串或主串中剩余的字符数小于模式串的长度。

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算法的核心思想包括:

  1. 预处理模式串,生成部分匹配表(next数组)。
  2. 利用这个表在匹配失败时快速滑动模式串,减少不必要的比较。

KMP算法的主要步骤:

  1. 预处理阶段:计算模式串的部分匹配表。
  2. 匹配阶段:利用部分匹配表进行快速匹配。

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”,

patternabab
最大前缀后缀公共元素长度0012
next-1001

        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数组为:

patternababadc
next-100123

,当 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

KMP算法及其改进图文详解_改进的kmp算法-CSDN博客

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值