重温数据结构与算法之KMP算法

news2025/4/4 22:09:18

文章目录

  • 前言
  • 一、原理
    • 1.1 暴力法
    • 1.2 最长公共前后缀
  • 二、代码实现
    • 2.1 next数组
    • 2.2 可视化next
    • 2.3 KMP
  • 三、总结
    • 3.1 优点
    • 3.2 缺点
  • 参考

前言

​ 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

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/693812.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

LangChain 任意命令执行(CVE-2023-34541)

漏洞简介 LangChain是一个用于开发由语言模型驱动的应用程序的框架。 在LangChain受影响版本中&#xff0c;由于load_prompt函数加载提示文件时未对加载内容进行安全过滤&#xff0c;攻击者可通过构造包含恶意命令的提示文件&#xff0c;诱导用户加载该文件&#xff0c;即可造…

【数据结构与算法】4、双向链表(学习 jdk 的 LinkedList 部分源码)

目录 一、双向链表二、node(int index) 根据索引找节点三、clear()四、add(int, E&#xff09;五、remove(int index)六、双向链表和单链表七、双向链表和动态数组八、jdk 官方的 LinkedList 的 clear() 方法 一、双向链表 &#x1f381; 单链表的节点中只有一个 next 指针引用…

1754_C语言assert函数功能初探

全部学习汇总&#xff1a; GreyZhang/c_basic: little bits of c. (github.com) 最近学习的过程中遇到了C语言中的assert函数&#xff0c;弄不明白这个函数到底是什么用。简单查了一下总结内容如下&#xff1a; 首先&#xff0c;此函数的声明头文件在assert.h中&#xff0c;使…

数据结构之哈夫曼树和哈夫曼编码

切入正题之前&#xff0c;我们先了解几个概念&#xff1a; 路径&#xff1a;从树的一个结点到另一个结点分支所构成的路线路径长度&#xff1a;路径上的分支数目树的路径长度&#xff1a;从根结点出发到每个结点的路径长度之和带权路径长度&#xff1a;该结点到根结点的路径长…

Spring概念:容器、Ioc、DI

目录 什么是容器&#xff1f; 什么是 IoC&#xff1f; 传统程序的开发 理解 Spring IoC DI 总结 我们通常所说的 Spring 指的是 Spring Framework&#xff08;Spring 框架&#xff09;&#xff0c;它是⼀个开源框架&#xff0c;有着活跃⽽庞⼤的社区&#xff0c;这就是它…

从C语言到C++_22(继承)多继承与菱形继承+笔试选择题

目录 1. 继承 1.1 继承的概念 1.2 继承的定义格式 1.3 访问限定符和继承方式 1.4 继承中的赋值 1.5 继承中的作用域 2. 子类(派生类)的默认成员函数 2.1 子类的构造函数 2.2 子类的拷贝构造函数 2.3 子类的赋值重载 2.4 子类的析构函数 2.5 小总结 3. 继承与友元…

欧洲运输业的创新能力评估报告(英)(附下载)

5月&#xff0c;联合研究中心(JRC)在交通研究与创新监测与信息系统&#xff08;TRIMIS&#xff09;上发布了一份报告&#xff0c;提供了对欧盟运输部门创新能力的最新评估。TRIMIS通过欧盟层面的宏观指标分析&#xff0c;对运输创新能力进行定期评估。 该报告分析了研究与开发(…

Tuxera NTFS2023Mac电脑免费U盘硬盘读写工具

Mac用户在使用NTFS格式移动硬盘时&#xff0c;会遇到无法写入硬盘的情况。要想解决无法写入的问题&#xff0c;很多人选择使用Mac读写软件。面对市面上“众多”的读写硬盘软件&#xff0c;用户应该怎么选择呢&#xff1f;初次接触移动硬盘的伙伴可能不知道移动硬盘怎么和电脑连…

探析ModaHub魔搭社区中文文本生成图片AI模型的现状、趋势和未来发展方向

目录 一、现状分析 二、趋势分析 三、未来预测 ModaHub魔搭社区是一个专注于AI模型开发和分享的平台&#xff0c;其中文本生成图片AI模型是其中的一个重要领域。本文将通过对ModaHub魔搭社区中文文本生成图片AI模型排行榜的数据分析&#xff0c;来探讨该领域的现状、趋势和未…

Linux安装nginx 反向代理 负载均衡 动静分离 高可用等使用

随着软件需求的发展&#xff0c;现在很多的系统都需要保证高可用、高并发&#xff0c;在此需求之下就需要部署的服务能够不间断的提供服务即避免单点故障问题因此系统需要做集群部署同时还能提升qps、tps等指标&#xff1b;集群部署后的服务就需要对用户的请求能够负载均衡&…

12 通用同步异步收发器(USART)

目录 通用同步异步收发器&#xff08;USART&#xff09; 理论部分 USART概览 STM32和PC通信模型 STM32和PC通过RS-232标准通信 RS-232标准介绍 RS-232协议电平标准对比 RS-232标准的物理接口规定及接口标号 RS-232标准下接口标号的作用 RS-232标准数据传输协议层 协议…

探索数字化前沿:数字化产品引领科技创新风潮

随着数字化时代的到来&#xff0c;国内数字化产品市场蓬勃发展&#xff0c;涌现出许多引领行业变革的产品。本文将介绍几个在数字孪生和人工智能领域取得突破的国内产品&#xff0c;带大家了解数字化产品的创新应用和影响力。 山海鲸可视化&#xff1a;山海鲸可视化是一款强大…

Linux 常用命令记录

Linux(Ubuntu) 常用命令的总结 总结工作中用到的ubuntu命令&#xff0c;和添加一些常见的Linux的命令; 1.文件操作&常见命令操作 前置补充&#xff1a;Linux 终端提示符 && 命令语法 Centos[rootoldboy_python ~] ## [用户名主机机器名 路径] 提示符# 用户名 ro…

《计算机系统与网络安全》 第十章 防火墙技术

&#x1f337;&#x1f341; 博主 libin9iOak带您 Go to New World.✨&#x1f341; &#x1f984; 个人主页——libin9iOak的博客&#x1f390; &#x1f433; 《面试题大全》 文章图文并茂&#x1f995;生动形象&#x1f996;简单易学&#xff01;欢迎大家来踩踩~&#x1f33…

Three.js快速入门

Three.js快速入门 1、threejs文件包下载和目录简介 下载地址&#xff1a; 网盘链接:https://pan.baidu.com/s/1_Ix8TiOScypNcQe3BIl5vA?pwdrrks 提取码:rrksthreejs文件资源目录介绍 three.js-文件包 └───build——three.js相关库&#xff0c;可以引入你的.html文件中…

Frida遍历启动App所有Activity/Service

说明&#xff1a;仅供学习使用&#xff0c;请勿用于非法用途&#xff0c;若有侵权&#xff0c;请联系博主删除 作者&#xff1a;zhu6201976 一、需求 在一些大型App中&#xff0c;往往注册了大量的Activity和Service&#xff0c;这在App的AndroidManifest.xml文件可以清晰呈现。…

Centos7完整安装

一、前言 由于使用VMware workstation使用典型安装以及默认安装方式进行安装的Centos多存在组件不足的问题&#xff0c;这使得使用上述方式进行安装的系统在一些特定情况下需要安装组件&#xff0c;致使系统不是很便利&#xff0c;本文提供了在VMware workstation下进行完整安装…

Verdi之nTrace/nSchema

目录 3.nTrace介绍 3.1 启动Verdi 3.2查看Verdi中的设计结构 3.3查看Verdi中的验证结构 3.4 查找模块和trace信号 3.5 查找string 3.6 信号drive/load 3.7 快速查看设计有哪些信号 4 nSchema 4.1 如何打开原理图 4.2 如何查找 nShema window中器件的源码 4.3 如何显示原理…

【Java面试题】框架篇——Redis

文章目录 Redis的使用场景Redis支持的数据类型如何在Redis中实现分布式锁Redis和Mysql的事务有什么区别&#xff1f;Redis缓存穿透如何解决&#xff1f;Redis缓存雪崩如何解决&#xff1f;什么是缓存击穿&#xff0c;如何解决&#xff1f;Redis是单线程模式的&#xff0c;为什么…

chatgpt赋能python:Python超大数计算

Python超大数计算 介绍 在日常编程中&#xff0c;我们常常需要处理大量数据。这些数据可以是普通的整数或浮点数&#xff0c;但有时候我们需要计算的数据可能会超出计算机处理的数值范围。这时&#xff0c;我们需要使用更为高级的算法来进行超大数计算。 Python语言因为其简…