百度360必应搜狗淘宝本站头条
当前位置:网站首页 > 技术文章 > 正文

一文读透,Python暴力(BF)字符串匹配算法到 KMP 算法之间的变化

itomcoil 2025-05-22 10:57 23 浏览

1. 字符串匹配算法

所谓字符串匹配算法,简单地说就是在一个目标字符串中查找是否存在另一个模式字符串。如在字符串 " ABCDEFG " 中查找是否存在 “ EF ” 字符串。

可以把字符串 " ABCDEFG " 称为 原始(目标)字符串 ,“ EF ” 称为 子字符串模式字符串

本文试图通过几种字符串匹配算法的算法差异性来探究字符串匹配算法的本质。

常见的字符串匹配算法:

  • BF(Brute Force,暴力检索算法)
  • RK (Robin-Karp 算法)
  • KMP (D.E.Knuth、J.H.Morris、V.R.Pratt 算法)

2. BF(Brute Force,暴力检索)

BF 算法是一种原始、低级的穷举算法。

2.1 算法思想

下面使用长、短指针方案描述 BF 算法:

  1. 初始指针位置:长指针指向原始字符串的第一个字符位置、短指针指向模式字符串的第一个字符位置。这里引入一个辅助指针概念,其实可以不用。
  2. 辅助指针是长指针的替身,替长指针和短指针所在位置的字符比较。
  3. 每次初始化长指针位置时,让辅助指针和长指针指向同一个位置。
  1. 如果长、短指针位置的字符不相同,则长指针向右移动(短指针不动)。如果长、短指针所指位置的字符相同,则用辅助指针替代长指针(长指针位置暂不动)和短指针位置的字符比较,如果比较相同,则同时向右移动辅助指针和短指针。
  1. 如果辅助指针和短指针位置的字符不相同,则重新初始化长指针位置(向右移动),短指针恢复到最原始状态。
  1. 使用重复或者递归的方式重复上述流程,直到出口条件成立。
  2. 查找失败: 长指针到达了原始字符串的尾部。当 长指针位置=原始字符串长度 - 模式字符串长度+1 时就可以认定查找失败。
  • 查找成功: 短指针到达模式字符串尾部。

2.2 编码实现

使用辅助指针:

# 原始字符串
src_str = "thismymyre"
# 长指针
sub_str = "myr"
# 长指针 :在原始字符串上移动
long_index = 0
# 短指针:在模式字符串上移动
short_index = 0
# 辅助指针
fu_index = long_index 
# 原始字符串长度
str_len = len(src_str)
# 模式字符串的长度
sub_len = len(sub_str)
# 是否存在
is_exist = False
while long_index < str_len-sub_len+1:
    # 把长指针的位值赋给辅助指针
    fu_index = long_index
    # 短指针初始为原始位置
    short_index = 0
    while short_index < sub_len and src_str[fu_index] == sub_str[short_index]:
        # 辅助指针向右
        fu_index += 1
        # 短指针向右
        short_index += 1
    if short_index == sub_len:
        is_exist = True
        break
    # 比较不成功,则长指针向右移动    
    long_index += 1

if not is_exist:
    print("{0} 不存在于 {1} 字符串中".format(sub_str, src_str))
else:
    print("{0} 存在于 {1} 的 {2} 位置".format(sub_str, src_str, long_index))

使用一个增量:

# 原始字符串
src_str = "thisismymyrdodmyrd"
# 子子符串
sub_str = "myrd"
# 长指针
long_index = 0
# 短指针
short_index = 0
# 原始字符串长度
str_len = len(src_str)
# 模式字符串的长度
sub_len = len(sub_str)
is_exist = False
while long_index < str_len:
    i = 0
    short_index = 0
    while short_index < sub_len and src_str[long_index + i] == sub_str[short_index]:
        i += 1
        # 短指针向右
        short_index += 1
    if short_index == sub_len:
        is_exist = True
        break
    long_index += 1

if not is_exist:
    print("{0} 不存在于 {1} 字符串中".format(sub_str, src_str))
else:
    print("{0} 存在于 {1} 的 {2} 位置".format(sub_str, src_str, long_index))

使用或不使用辅助指针的代码逻辑是一样。

在原始字符串和模式字符串齐头并进逐一比较时,最好不要修改长指针的位置,否则,在比较不成功的情况下,则修正长指针的逻辑就没有单纯的直接向右移动那么好理解。

如下直接使用长指针和短指针进行比较:

# 原始字符串
src_str = "thisismymyrdodmyrd"
# 子子符串
sub_str = "myrd"
# 长指针
long_index = 0
# 短指针
short_index = 0
# 原始字符串长度
str_len = len(src_str)
# 模式字符串的长度
sub_len = len(sub_str)
is_exist = False
while long_index < str_len:
    short_index = 0
    # 直接使用长指针和短指针位置相比较
    while short_index < sub_len and src_str[long_index] == sub_str[short_index]:
        long_index+=1
        # 短指针向右
        short_index += 1
    if short_index == sub_len:
        is_exist = True
        break
    # 修正长指针的位置
    long_index = long_index-short_index+1

if not is_exist:
    print("{0} 不存在于 {1} 字符串中".format(sub_str, src_str))
else:
    print("{0} 存在于 {1} 的 {2} 位置".format(sub_str, src_str, long_index-short_index))

使用字符串切片实现:使用 Python 的切片实现起来更简单。但不利于初学者理解 BF 算法的细节。

# 原始字符串
src_str = "thisismymyrdodmyrd"
# 子子符串
sub_str = "myrd"
# 原始字符串长度
str_len = len(src_str)
# 模式字符串的长度
sub_len = len(sub_str)
is_exist = False
for index in range(str_len - sub_len + 1):
    if src_str[index:index + sub_len] == sub_str:
        is_exist = True
        break
if not is_exist:
    print("{0} 不存在于 {1} 字符串中".format(sub_str, src_str))
else:
    print("{0} 存在于 {1} 的 {2} 位置".format(sub_str, src_str, index))

BF 算法的时间复杂度:

BF 算法直观,易于实现。但代码中有循环中嵌套循环的结构,这是典型的穷举结构。如果原始字符串的长度为 m ,模式字符串的长度为 n。时间复杂度则是 O(m*n),时间复杂度较高。

3. RK(Robin-Karp 算法)

RK算法 ( 指纹字符串查找) 在 BF 算法的基础上做了些改进,基本思路:

在模式字符串和原始字符串的字符准备开始逐一比较时,能不能通过一种算法,快速判断出本次比较是没有必要。

3.1 RK 的算法思想

  • 选定一个哈希函数(可自定义)。
  • 使用哈希函数计算模式字符串的哈希值。
  • 如上计算 thia 的哈希值
  • 再从原始字符串的开始比较位置起,截取一段和模式字符串长度一样的子串,也使用哈希函数计算哈希值。
  • 如上计算 this 的哈希值
  • 如果两次计算出来的哈希值不相同,则可判断两段模式字符串不相同,没有比较的必要。
  • 如果两次计算的哈希值相同,因存在哈希冲突,还是需要使用 BF 算法进行逐一比较。

RK 算法使用哈希函数算法减少了比较次数。

3.2 编码实现:

# 原始字符串
src_str = "thisismymyrdodmyrd"
# 子子符串
sub_str = "myrd"
# 长指针
long_index = 0
# 短指针
short_index = 0
# 辅助指针
fu_index = 0
# 原始字符串长度
str_len = len(src_str)
# 模式字符串的长度
sub_len = len(sub_str)
is_exist = False
for long_index in range(str_len - sub_len + 1):
    # 这里使用 python 内置的 hash 函数
    if hash(sub_str) != hash(src_str[long_index:long_index + sub_len]):
        # 哈希值一样就没有必要比较了
        continue
    # 把长指针的位置赋给辅助指针
    fu_index = long_index
    short_index = 0
    while short_index < sub_len and src_str[fu_index] == sub_str[short_index]:
        # 辅助指针向右
        fu_index += 1
        # 短指针向右
        short_index += 1
    if short_index == sub_len:
        is_exist = True
        break

if not is_exist:
    print("{0} 不存在于 {1} 字符串中".format(sub_str, src_str))
else:
    print("{0} 存在于 {1} 的 {2} 位置".format(sub_str, src_str, long_index))

RK 的时间复杂度:

RK 的代码结构和 BF 看起来一样,使用了循环嵌套。但内置循环只有当哈希值一样时才会执行,执行次数是模式字符串的长度。如果原始子符串长度为 m,模式字符串的长度为 n。则时间复杂度为 O(m+n),如果不考虑哈希冲突问题,时间复杂度为 O(m)。

很显然 RK 算法比 BF 算法要快很多。

4. KMP算法

算法的本质都是穷举,这是由计算机的思维方式决定的。我们在谈论"好"和“坏” 算法时,所谓好就是想办法让穷举的次数少一些。比如前面的 RK 算法,通过一些特性提前判断是否值得比较,这样可以省掉很多不必要的内循环。

KMP也是一样,也是尽可能减少比较的次数。

4.1 KMP 算法思路:

KMP的基本思路和 BF 是一样的(字符串逐一比较),BF 算法中,如果比较不成功,长指针每次只会向右移动一位。如下图:辅助指针和短指针对应位置字符不相同,说明比较失败。

长指针向右移一位,短指针恢复原始状态。重新逐一比较。

KMP算法对长、短指针的移位做了优化。

  • 没有必要再使用辅助指针。
  • 直接把长指针和短指针所在位置的字符逐一比较。
  • 比较失败后,长指针位置不动。根据 KMP 算法中事先计算好的 “ 部分匹配表(PMT:Partial Match Table) ” 修改短指针的位置。

如上图比较失败后,长指针位置保持不变,只需要移动短指针。短指针具体移动哪里,由 PMT 表决定。上图灰色区域就是根据 PMT 表计算出来的可以不用再比较的字符。

在移动短指针之前,先要理解 KMP 算法中 的 " 部分匹配表(PMT) " 是怎么计算出来的。

先理解与 PMT 表有关系的 3 个概念:

  • 前缀集合:
  • 如: ABAB 的前缀(不包含字符串本身)集合 {A,AB,ABA}
  • 后缀集合:
  • 如: ABAB 中后缀(不包含字符串本身)集合 { BAB,AB,B }
  • PMT值:前缀、后缀两个集合的交集元素中最长元素的长度。
  • 如:先求 {A,AB,ABA}{ BAB,AB,B } 的交集,得到集合 {AB} ,再得到集合中最长元素的长度, 所以 ABAB 字符串的 PMT 值是 2 。

如前面图示,原始字符串和模式字符串逐一比较时,前 4 位即 ABAB 是相同的,而 ABAB 存在最大长度的前缀和后缀 ‘AB’ 子串。意味着下一次比较时,可以直接让 模式字符串的前缀 和原始字符串中 已经比较的字符串的后缀 对齐,公共部分不用再比较。

所以, KMP 算法的核心是得到 PMT 表,现使用手工方式计算 ABABCAPMT 值:

  • 当仅匹配第一个字符 A 时,A 没有前缀集合也没有后缀集合,所以 PMT[0]=0,短指针要移到模式字符串的 0 位置。
  • 当仅匹配前二个字符 AB 时,AB的前缀集合{A},后缀集合是{B},没有交集,所以 PMT[1]=0,短指针要移到模式字符串的 0 位置。
  • 当仅匹配前三个字符 ABA 时,ABA 的前缀集合{A,AB} ,后缀集合{BA,A},交集{A},所以 PMT[2]=1,短指针要移到模式字符串 1 的位置。
  • 当仅匹配前四个字符 ABAB 时,ABAB 的前缀集合 {A ,AB,ABA },后缀集合{BAB,AB,B},交集{AB},所以 PMT[3]=2,短指针要移到模式字符串 2 的位置。
  • 当仅匹配前五个字符 ABABC 时,ABABC 的前缀集合{ A,AB,ABA,ABAB },后缀集合{ C,BC,ABC,BABC },没有交集,所以PMT[4]=0,短指针要移到模式字符串的 0 位置。
  • 当全部匹配后,ABABCA 的前缀是{A,AB,ABA,ABABC,ABABCA},后缀是{A,CA,BCA,ABCA,BABCA} 交集是{A},PMT[5]=1。

其实在 KMP 算法中,本没有直接使用 PMT 表,而是引入了next 数组的概念,next 数组中的值是 PMT 的值向右移动一位。

KMP算法实现:先不考虑 next 数组的算法,先以上面的手工计算值作为 KMP 算法的已知数据。

src_str = 'ABABABCAEF'
sub_str = 'ABABCA'
# next 数组,现在不着急讨论 next 数组如何编码实现,先用上面手工推演出来的结果
p_next = [-1, 0, 0, 1, 2, 0]
# long_index 指向原始字符的第一个位置
long_index = 0
# short_index 指向模式字符串的第一个
short_index = 0
# 原始字符串的长度
src_str_len = len(src_str)
# 模式字符串的长度
sub_str_len = len(sub_str)
# 保存长指针、短指针位置有效 当长指针越界时,说明查找失败,当短指针越界,说明查找成功
while long_index < src_str_len and short_index < sub_str_len:
    # 理论上 当长指针和短指针所在位置的字符相同时,长、短指针向右移动
    # 如果长指针和短指针所在位置的字符不相同时,这里 -1 就起到神奇的作用,长指针可以前进,短指针会变成 0 。
    # 下次比较时,如果还是不相同 short_index 又变回 -1, 长指针又可以前进,短指针还是指向 0 位置
    if short_index == -1 or src_str[long_index] == sub_str[short_index]:
        long_index += 1
        short_index += 1
    else:
        short_index = p_next[short_index]
if short_index == sub_str_len:
    print(long_index - short_index)

上面的代码是没有通用性的,因为 next 数组的值是固定的,现在实现求解 netxt 数组的算法:

求 next 也可以认为是一个字符串匹配过程,只是原始字符串和模式字符串都是同一个字符串,因第一个字符没有前缀也没有后缀,所以从第二个字符开始。

# 求解 next 的算法
def getNext(p):
    i, j = 0, -1
    m = len(p)
    pnext = [-1] * m
    while i < m - 1:
        if j == -1 or p[i] == p[j]:
            i += 1
            j += 1
            pnext[i] = j
        else:
            j = pnext[j]
    return pnext

KMP算法的时间复杂度为 O(m+n)

5. 总结

字符串匹配算法除了上述几种外,还有 Sunday算法、Sunday算法。从暴力算法开始,其它算法可以尽可能减少比较的次数。加快算法的速度。

原文参考:
https://www.cnblogs.com/guo-ke/p/16056222.html

相关推荐

最强聚类模型,层次聚类 !!_层次聚类的优缺点

哈喽,我是小白~咱们今天聊聊层次聚类,这种聚类方法在后面的使用,也是非常频繁的~首先,聚类很好理解,聚类(Clustering)就是把一堆“东西”自动分组。这些“东西”可以是人、...

python决策树用于分类和回归问题实际应用案例

决策树(DecisionTrees)通过树状结构进行决策,在每个节点上根据特征进行分支。用于分类和回归问题。实际应用案例:预测一个顾客是否会流失。决策树是一种基于树状结构的机器学习算法,用于解决分类...

Python教程(四十五):推荐系统-个性化推荐算法

今日目标o理解推荐系统的基本概念和类型o掌握协同过滤算法(用户和物品)o学会基于内容的推荐方法o了解矩阵分解和深度学习推荐o掌握推荐系统评估和优化技术推荐系统概述推荐系统是信息过滤系统,用于...

简单学Python——NumPy库7——排序和去重

NumPy数组排序主要用sort方法,sort方法只能将数值按升充排列(可以用[::-1]的切片方式实现降序排序),并且不改变原数组。例如:importnumpyasnpa=np.array(...

PyTorch实战:TorchVision目标检测模型微调完

PyTorch实战:TorchVision目标检测模型微调完整教程一、什么是微调(Finetuning)?微调(Finetuning)是指在已经预训练好的模型基础上,使用自己的数据对模型进行进一步训练...

C4.5算法解释_简述c4.5算法的基本思想

C4.5算法是ID3算法的改进版,它在特征选择上采用了信息增益比来解决ID3算法对取值较多的特征有偏好的问题。C4.5算法也是一种用于决策树构建的算法,它同样基于信息熵的概念。C4.5算法的步骤如下:...

Python中的数据聚类及可视化分析实践

探索如何通过聚类分析揭露糖尿病预测数据集的特征!我们将运用Python的强力工具,深入挖掘数据,以直观的可视化揭示不同特征间的关系。一同探索聚类分析在糖尿病预测中的实践!所有这些可视化都可以通过数据操...

用Python来统计大乐透号码的概率分布

用Python来统计大乐透号码的概率分布,可以按照以下步骤进行:导入所需的库:使用Python中的numpy库生成数字序列,使用matplotlib库生成概率分布图。读取大乐透历史数据:从网络上找到大...

python:支持向量机监督学习算法用于二分类和多分类问题示例

监督学习-支持向量机(SVM)支持向量机(SupportVectorMachine,简称SVM)是一种常用的监督学习算法,用于解决分类和回归问题。SVM的目标是找到一个最优的超平面,将不同类别的...

25个例子学会Pandas Groupby 操作

groupby是Pandas在数据分析中最常用的函数之一。它用于根据给定列中的不同值对数据点(即行)进行分组,分组后的数据可以计算生成组的聚合值。如果我们有一个包含汽车品牌和价格信息的数据集,那么可以...

数据挖掘流程_数据挖掘流程主要有哪些步骤

数据挖掘流程1.了解需求,确认目标说一下几点思考方法:做什么?目的是什么?目标是什么?为什么要做?有什么价值和意义?如何去做?完整解决方案是什么?2.获取数据pandas读取数据pd.read.c...

使用Python寻找图像最常见的颜色_python 以图找图

如果我们知道图像或对象最常见的是哪种颜色,那么可以解决图像处理中的几个用例,例如在农业领域,我们可能需要确定水果的成熟度。我们可以简单地检查一下水果的颜色是否在预定的范围内,看看它是成熟的,腐烂的,还...

财务预算分析全网最佳实践:从每月分析到每天分析

原文链接如下:「链接」掌握本文的方法,你就掌握了企业预算精细化分析的能力,全网首发。数据模拟稍微有点问题,不要在意数据细节,先看下最终效果。在编制财务预算或业务预算的过程中,通常预算的所有数据都是按月...

常用数据工具去重方法_数据去重公式

在数据处理中,去除重复数据是确保数据质量和分析准确性的关键步骤。特别是在处理多列数据时,保留唯一值组合能够有效清理数据集,避免冗余信息对分析结果的干扰。不同的工具和编程语言提供了多种方法来实现多列去重...

Python教程(四十):PyTorch深度学习-动态计算图

今日目标o理解PyTorch的基本概念和动态计算图o掌握PyTorch张量操作和自动求导o学会构建神经网络模型o了解PyTorch的高级特性o掌握模型训练和部署PyTorch概述PyTorc...