重温数据结构与算法之KMP算法-程序员宅基地

技术标签: 算法  python  java  字符串匹配  数据结构与算法  算法可视化  kmp  Python  

前言

​ KMP 算法是一种字符串匹配算法,它可以在一个主串中查找一个模式串的出现位置。在实际应用中,字符串匹配是一个非常常见的问题,比如在搜索引擎中搜索关键词、在文本编辑器中查找字符串等等。

​ KMP 算法的发明者是 Donald Knuth、James H. Morris 和 Vaughan Pratt,他们在1977年发表了一篇论文《Fast Pattern Matching in Strings》,其中Donald Knuth 还是《计算机程序设计艺术》的作者。

​ 相比于暴力匹配算法的时间复杂度 O ( n m ) O(nm) O(nm),KMP 算法的优势在于它的时间复杂度为 O ( n + m ) O(n+m) O(n+m),其中n是文本串的长度,m是模式串的长度。此外,KMP 算法还可以处理一些特殊情况,例如模式串中存在重复的子串。

一、原理

1.1 暴力法

字符串匹配就是有两个字符串,分别是文本串s和模式串p,计算p在s中首次出现位置,未出现返回-1。

暴力法是最简单的字符串匹配算法,它的思路很简单:从主串的第一个字符开始,依次和模式串的每个字符进行比较,如果匹配成功,则继续比较下一个字符,否则从主串的下一个字符开始重新匹配。

暴力法时间复杂度 O ( m ∗ n ) O(m*n) O(mn)

public int bruteForce(String s, String p) {
    
	int lenS = s.length();
	int lenP = p.length();
	if (lenS < lenP) return -1;

    for (int i = 0; i <= lenS - lenP; i++) {
    
        int pos = 0;
        while (pos < lenP) {
    
            if (s.charAt(i + pos) != p.charAt(pos)) {
    
                break;
            }
            pos++;
        }
        if (pos == lenP) return i;
    }
    return -1;
}

1.2 最长公共前后缀

kmp_01.png

以上面例子作为参考,会发现暴力法有许多冗余的比较,因为一旦未匹配上,直接从下一位重新开始比较,KMP 就是在这块做出了优化,不再是下一位,而是由一个next 数组决定跳转的数。

KMP 算法的核心思想是利用已经匹配过的信息,尽量减少模式串与主串的匹配次数。具体来说,KMP算法维护一个next数组,用于记录模式串中每个字符前面的最长公共前后缀的长度。在匹配过程中,如果当前字符匹配失败,则根据next数组的值调整模式串的位置,使得模式串尽可能地向右移动,从而减少匹配次数。

为什么是最长公共前后缀呢,其实用下面的例子可以很好理解

kmp_02.png

当在j=6处时匹配失败,那么你就会知道前6个字符匹配正确,那么如何得到下个跳转的位置呢?

  • 肉眼可见首字母是a,即前缀为a,所以应该跳到下个a字符位置(i=2)

  • 又发现存在和前缀ab相同的,所以更好的位置应该是下个ab的位置(i=4)

  • 依次类推,看起来是寻找和前缀相同的最长字符串,跳转到对应的位置

  • 实际发现跳转的位置是前缀相同的最长字符串对应的位置并不是最优解,最优解跟后缀相关,后缀后面有更多的可能

  • 如果存在和前缀相同的最长字符串 t 并不是后缀,那么前缀+后1个字符 肯定不能匹配 t+后1个字符,浪费一次匹配机会,不是最优的位置

  • 所以最好的位置应该是最长公共前后缀部分。以上述为例:就是 s[0:5]的后缀和p[0:5]的前缀 最长公共部分,由于两者相同,即abacab 的最长公共前后缀ab

注意:KMP算法在模式串具有重复度高的字符才能发挥强大的功力, 如果是全不相同的字符,会退化为暴力算法

二、代码实现

2.1 next数组

next数组的计算是KMP算法的关键,它的定义如下:

next[i]表示模式串中每个位置之前的最长相同前缀后缀的长度,即以第i - 1个字符结尾的子串p[0:i-1]中,既是前缀又是后缀的最长字符串的长度。

具体来说,我们可以通过递推的方式计算next数组,可以按照以下步骤进行:

  1. next[0]=-1
  2. 定义 i表示当前计算的位置,j表示当前位置之前的最长相同前缀后缀的长度
  3. 后面的目标就是找到 p[0 : j-1] == p[i-j : i-1] 如果找到了这样的 j,那么next[i]的值就是j。 如果找不到这样的j,那么next[i]的值就是0
  4. 如果 p[i] == p[j],只要将next[i+1] = next[i] + 1就可以,而 j 是前一个最长相同前缀后缀的长度也就是next[i], 即next[++i] = ++j
  5. 如果p[i] != p[j],需要将 j 回溯到之前的位置next[j]
public static int[] getNext(String pattern) {
    
    int[] next = new int[pattern.length()];
    next[0] = -1;
    int i = 0, j = -1; // j为当前已经匹配的前缀的最长公共前后缀的长度
    while (i < pattern.length() - 1) {
    
        if (j == -1 || pattern.charAt(i) == pattern.charAt(j)) {
    
            next[++i] = ++j; // 长度加1,并且将指针移向下一位
        } else {
    
            j = next[j]; // 回溯
        }
    }
    return next;
}

2.2 可视化next

next数组的生成是理解KMP的重中之重,可视化一下如何生成对理解KMP有很大帮助

import tkinter as tk
import time

def changeColor(canvas, rects, color):
    for rect in rects:
        canvas.itemconfig(rect, fill=color)
def visualize_next(pattern):
    next = [-1] * len(pattern)
    root = tk.Tk()
    root.title("KMP Next Visualization")
    canvas_width = 800
    canvas_height = 600
    canvas = tk.Canvas(root, width=canvas_width, height=canvas_height)
    canvas.pack()
    block_width = 50
    block_height = 50
    x_margin = 50
    y_margin = 50
    nextbox = []
    pbox = []


    x = x_margin
    y = y_margin
    canvas.create_text(x-block_width/2, y+block_height/2, text="索引", font=("Arial", 16))
    for i in range(len(pattern)):
        canvas.create_rectangle(x, y, x+block_width, y+block_height, outline="black")
        canvas.create_text(x+block_width/2, y+block_height/2, text=str(i), font=("Arial", 16))
        x += block_width

    x = x_margin
    y += block_height
    canvas.create_text(x-block_width/2, y+block_height/2, text="p", font=("Arial", 16))
    for i in range(len(pattern)):
        pbox.append(canvas.create_rectangle(x, y, x+block_width, y+block_height, outline="black"))
        canvas.create_text(x+block_width/2, y+block_height/2, text=str(pattern[i]), font=("Arial", 16))
        x += block_width

    x = x_margin
    y += block_height
    canvas.create_text(x-block_width/2, y+block_height/2, text="Next", font=("Arial", 16))

    for i in range(len(pattern)):
        canvas.create_rectangle(x, y, x+block_width, y+block_height, outline="black")
        nextbox.append(canvas.create_text(x+block_width/2, y+block_height/2, text="", font=("Arial", 16)))
        x += block_width
    
    i = 0
    j = -1
    x = x_margin
    y += block_height
    i_rect = canvas.create_rectangle(x, y, x+block_width, y+block_height, fill="red")
    i_text = canvas.create_text(x+block_width/2, y+block_height/2, text=str("i"), font=("Arial", 16))
    y += block_height
    j_rect = canvas.create_rectangle(x - block_width, y, x, y+block_height, fill="blue")
    j_text = canvas.create_text(x- block_width/2, y+block_height/2, text=str("j"), font=("Arial", 16))

    canvas.itemconfig(nextbox[0], text=str("-1"))
    time.sleep(1)
    while i < len(pattern) - 1:
        changeColor(canvas, pbox, '')
        if j == -1 or pattern[i] == pattern[j]:
            i += 1
            j += 1
            canvas.move(i_rect, block_width, 0)
            canvas.move(i_text, block_width, 0)
            canvas.move(j_rect, block_width, 0)
            canvas.move(j_text, block_width, 0)
            canvas.itemconfig(nextbox[i], text=str(j))
            changeColor(canvas, pbox[0:j], 'blue')
            changeColor(canvas, pbox[i-j:i], 'red')
            canvas.update()
            time.sleep(1)
        else:
            tmp = j
            j = next[j]
            canvas.move(j_rect, (j - tmp)*block_width, 0)
            canvas.move(j_text, (j - tmp)*block_width, 0)

            canvas.update()
            time.sleep(1)
    root.mainloop()

if __name__ == "__main__":
    pattern = "abacabb"
    visualize_next(pattern)

kmp_next.gif

2.3 KMP

改造下暴力算法:

public static int kmp(String s, String p) {
    
    int[] next = getNext(p);

    int lenS = s.length();
    int lenP = p.length();
    if (lenS < lenP) return -1;
    int i = 0;

    while (i <= lenS - lenP) {
    
        int pos = 0;
        while (pos < lenP) {
    
            if (s.charAt(i + pos) != p.charAt(pos)) {
    
                break;
            }
            pos++;
        }
        if (pos == lenP) return i;
        i += pos - next[pos];
    }
    return -1;
}

三、总结

3.1 优点

  • KMP算法的时间复杂度为O(n+m),其中n为主串长度,m为模式串长度。相比于暴力法的O(n*m),KMP算法的效率更高。

  • Python中的re模块是用C语言实现的,底层使用了KMP算法来进行正则表达式的匹配。正则表达式通常用于处理大量文本数据,因此对于正则表达式匹配的性能要求比较高。使用KMP算法可以提高正则表达式匹配的效率,因此在Python中的re模块中使用KMP算法来实现正则表达式匹配,可以提高程序的性能。

  • KMP算法可以处理模式串中存在重复子串的情况,而其他字符串匹配算法则无法处理。

3.2 缺点

  • 在字符串小的情况下应用不多,在 JDK 中的 String.indexOf 方法使用了一种基于暴力匹配的算法,而不是KMP。这是因为在实际应用中,字符串的长度通常不会很长,而且KMP算法需要额外的空间来存储前缀表,这会增加内存的使用量。因此,对于较短的字符串,使用暴力匹配算法可以获得更好的性能。 另外,JDK中的String.indexOf方法还使用了一些优化技巧,例如在匹配失败时跳过一定长度的字符,以减少比较的次数。这些优化技巧可以在大多数情况下提高算法的效率,从而满足实际应用的需求。
  • KMP 算法的局限性在于它需要预处理模式串,这个预处理的时间复杂度为 O ( m ) O(m) O(m),因此在模式串很长的情况下,KMP 算法的效率可能会受到影响。
  • KMP 算法只能用于匹配单个模式串,无法处理多个模式串的匹配问题。

参考

  1. (原创)详解KMP算法
  2. KMP Algorithm for Pattern Searching
版权声明:本文为博主原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。
本文链接:https://blog.csdn.net/qq_23091073/article/details/131423272

智能推荐

基于ssm+vue.js+uniapp小程序的北关村基本办公管理系统附带文章和源代码部署视频讲解等-程序员宅基地

文章浏览阅读592次,点赞27次,收藏28次。博主介绍:CSDN特邀作者、985计算机专业毕业、某互联网大厂高级全栈开发程序员、码云/掘金/华为云/阿里云/InfoQ/StackOverflow/github等平台优质作者、专注于Java、小程序、前端、python等技术领域和毕业项目实战,以及程序定制化开发、全栈讲解、就业辅导、面试辅导、简历修改。精彩专栏 推荐订阅2023-2024年最值得选的微信小程序毕业设计选题大全:100个热门选题推荐2023-2024年最值得选的Java毕业设计选题大全:500个热门选题推荐。

Android SDK开发的那些事-程序员宅基地

文章浏览阅读932次,点赞12次,收藏27次。本人从事Android开发已经有十余年,算是一名资深的移动开发架构师了吧。根据我的观察发现,对于很多初中级Android工程师而言,想要提升技能,往往是自己摸索成长,不成体系的学习效果低效漫长且无助。所以在此将我十年载,从萌新小白一步步成长为Android移动开发架构师的学习笔记,从Android四大组件到手写实现一个架构设计,我都有一一的对应笔记为你讲解。当然我也为你们整理好了百度、阿里、腾讯、字节跳动等等互联网超级大厂的历年面试真题集锦。

SSM新冠疫情服务系统 计算机专业毕设源码49727-程序员宅基地

文章浏览阅读97次。疫情服务管理信息系统主要功能模块包括后台首页、轮播图、公告消息、系统用户(管理员、社区居民、医生)交流管理(交流论坛、论坛分类)资源管理(疫情新闻、新闻分类)模块管理(医生信息、社区医院、志愿者管理、发热门诊、防疫物资、抗体水平、投诉建议、在线咨询、咨询回复、预约信息、核酸报告)等功能模块管理,采取面对对象的开发模式进行软件的开发和硬体的架设,能很好的满足实际使用的需求,完善了对应的软体架设以及程序编码的工作,采取MySQL作为后台数据的主要存储单元,采用SSM框架、Java技术、Ajax技术进行业务...

【学习日志】【TCN】时间序列卷积神经网络(1)-程序员宅基地

文章浏览阅读3.6k次,点赞7次,收藏43次。卷积运算就是将一个小的滑动窗口(称为卷积核或过滤器)在一个大的数据(称为输入或特征图)上滑动,并对每个窗口内的数据进行加权求和,得到一个新的数据(称为输出或激活图)。例如,在生物信号处理中,如果我们有1000个时间步长和8个信号通道,则我们可以将其表示为一个1000×8 的数组作为TCN 的输入。TCN还使用了空洞卷积,这是一种在卷积核中插入空白位置(称为膨胀因子)的技术,使得卷积核可以覆盖更长范围的输入,而不增加参数数量。就是CNN的卷积(卷积核在数据上进行的一种滑动运算的操作)。_tcn

ThreadLocal高频面试题-程序员宅基地

文章浏览阅读3k次,点赞6次,收藏38次。前言无论是工作还是面试中,我们都会跟ThreadLocal打交道,今天就跟大家聊聊ThreadLocal的八个关键知识点哈~ThreadLocal是什么?为什么要使用ThreadLocal一个ThreadLocal的使用案例ThreadLocal的原理为什么不直接用线程id作为ThreadLocalMap的key为什么会导致内存泄漏呢?是因为弱引用吗?Key为什么要设计成..._threadlocal面试题

dtoc_dtoc文件-程序员宅基地

文章浏览阅读710次。assume cs:code,ds:data,ss:stackdata segment dw 123,12666,1,8,3,38data endsstack segment db 256 dup (0)stack endscode segmentstart: mov ax,stack mov ss,ax mov sp,256 mov a_dtoc文件

随便推点

细说百度图片栏目——图片展示效果-程序员宅基地

文章浏览阅读315次。2019独角兽企业重金招聘Python工程师标准>>> ..._百度 图片页面的demo

系统蓝屏代码全集-程序员宅基地

文章浏览阅读333次。00000001 不正确的函数。2 0×00000002 系统找不到指定的档案。3 0×00000003 系统找不到指定的路径。4 0×00000004 系统无法开启档案。5 0×00000005 拒绝存取。6 0×00000006 无效的代码。7 0×00000007 储存体控制区块已毁。8 0×00000008 储存体空间不足,无法处理这个指令..._0x00000116 (0xffff8f8a904bd010, 0xfffff8047b98e0c0, 0xffffffffc000009a, 0x00

【点云处理】改进半径滤波实现对激光雷达点云的去噪_pcl 雨雪去噪-程序员宅基地

文章浏览阅读3.1k次。算法要求:若只用半径滤波或者统计滤波,远处的点稀少的点也可能是车辆,行人等重要点云,所以不能当作噪声点去除算法思想:在半径滤波的基础上加上了两个动态阈值,实现对激光雷达点云采集的雨雪天气的去噪。滤波前:滤波后:效果还是很明显的~~还需要数据不断测试,调整参数~~..._pcl 雨雪去噪

mysql查询优化器提示( hint )_mysql hint-程序员宅基地

文章浏览阅读4.8k次。查询优化器提示( hint ) : 一般指改变 mysql 优化器的执行计划, 除非业务需要, 不建议这样做。查询优化器提示( hint ) 1. HIGH_PRIORITY 、 LOW_PRIORITY HIGH_PRIORITY: 提示mysql该语句优先执行 LOW_PRIORITY: 提示mysql该语句处于等待执行, 有可能出现一..._mysql hint

Idea设置自定义注释模板和代码块_idea 方法注释 代码块-程序员宅基地

文章浏览阅读1.6k次。目录前言注释模板和设置类注释方法注释验证前言作为一个合格的开发人员,首先要掌握一款试下主流的开发工具,其实要善用该功能的主要功能,让自己的编码速度和工作效率提升。本文主要介绍基于Idea的注释模板的设置和自定义自主编码快速设置。注释模板和设置在Idea中注释可以通过单项设置,也可以通过文件导入,本文主要介绍单项设置。设置方法:IntelliJ IDEA菜单-->Preferences... --> Editor-->File and Code ._idea 方法注释 代码块

推荐文章

热门文章

相关标签