首页 > 编程语言 >子串查找算法KMP

子串查找算法KMP

时间:2024-08-14 19:28:06浏览次数:21  
标签:子串 匹配 后缀 pattern next 算法 查找 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博客

标签:子串,匹配,后缀,pattern,next,算法,查找,KMP
From: https://blog.csdn.net/xy2006860/article/details/141128247

相关文章

  • LeetCode 3 无重复字符的最长子串
    题目描述给定一个字符串s,请你找出其中不含有重复字符的 最长子串 的长度。示例 1:输入:s="abcabcbb"输出:3解释:因为无重复字符的最长子串是"abc",所以其长度为3。示例2:输入:s="bbbbb"输出:1解释:因为无重复字符的最长子串是"b",所以其长度......
  • 【代码随想录】一、数组:1.二分查找
    部分图文参考于:代码随想录-二分查找,本文仅作为个人学习使用,如有侵权,请联系删除。1.概念二分查找也称折半查找(BinarySearch),是一种在有序数组中查找某一特定元素的搜索算法。我们可以从定义可知,运用二分搜索的前提是数组必须是有序的,这里需要注意的是,我们的输入不一定是数组,也......
  • 动态规划:最长回文子串
    目录题目思路解题过程复杂度code 题目        给你一个字符串 s,找到 s 中最长的 回文子串示例1:输入:s="babad"输出:"bab"解释:"aba"同样是符合题意的答案。示例2:输入:s="cbbd"输出:"bb"提示:1<=s.length<=1000s 仅由数字和英文字母组成......
  • 编写一个程序,打开和读取一个文本文件,并统计文件中每个单词出现的次数。用改进的二叉查
    /编写一个程序,打开和读取一个文本文件,并统计文件中每个单词出现的次数。用改进的二叉查找树存储单词及其出现的次数。程序在读入文件后会提供一个有三个选项菜单。第一个选项是列出所有的单词和出现的次数。第二个选项是让用户输入一个单词,程序报告该单词在文件中出现的次数。......
  • 从KMP到exKMP
    KMP(Knuth-Morris-Pratt)用途:用于一个文本串S内查找一个模式串P的出现位置,以及求一个字符串的最小循环元长度和最大循环次数。思路:\(kmp\)是对原始的在文本串S内查找一个模式串P的出现位置的一种优化。原始做法将\(s\)的每一位都与\(p\)的第一位开始匹配。(匹配到\(s\)的......
  • 长度最小的子数组 | LeetCode-209 | 双指针+滑动窗口 | 前缀和+二分查找 | Java详细注
    ......
  • 字符串查找 - 模拟实现strstr 、BF算法 、 KMP算法
    文章目录前言一、模拟实现库函数strstr二、BF算法三、KMP算法总结前言路漫漫其修远兮,吾将上下而求索。一、模拟实现库函数strstrTips:此处采用利用指针+字符串末尾'\0'的判断,当然你可以利用数组的下标;库函数strstr的原型:char*strstr(constchar*str1,......
  • KMP算法的两种实现形式
    以leetcode28.实现strStr()为例:给你两个字符串 haystack 和 needle ,请你在 haystack 字符串中找出 needle 字符串的第一个匹配项的下标(下标从0开始)。如果 needle 不是 haystack 的一部分,则返回  -1 。示例1:输入:haystack="sadbutsad",needle="sad"......
  • 3333:练56.2 查找最接近的元素
    3333:练56.2查找最接近的元素信息学奥赛一本通-编程启蒙(C++版)在线评测系统练56.2查找最接近的元素信息学奥赛一本通-编程启蒙(C++版)在线评测系统01查找最接近的元素01查找最接近的元素_哔哩哔哩_bilibili《信息学奥赛一本通》题解_1240_查找最接近的元素《......
  • 【408DS算法题】009进阶-二维数组的查找
    Index题目实现代码分析题目在一个二维数组array中(每个一维数组的长度相同),每一行都按照从左到右递增的顺序排序,每一列都按照从上到下递增的顺序排序。请完成一个函数,输入这样的一个二维数组和一个整数,判断数组中是否含有该整数。​进阶要求——时间复杂度:......