【字符串处理Python实现】字符串模式匹配之暴力、BM算法简介与实现

本文介绍两种常见的字符串模式匹配算法:暴力匹配与Boyer-Moore算法。暴力匹配法通过逐字符对比来查找模式串,而Boyer-Moore算法利用镜像试探和字符跳跃提高搜索效率。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

字符串模式匹配是字符串处理中的一类经典问题,即给定长度为 n n n的字符串text以及长度为 m m m的模式串pattern,期望先确定pattern是否为text的一个子串,如果是,则返回textpattern开始的最小索引j,使得text[j:j+m]等于pattern,或者返回所有满足条件的索引。

实际上,在Python内置的 str 模块中, t.find(p)t.index(p)t.count(p) 以及 t.partition(p)t.split(p)t.replace(p, q) 等方法的实现都依赖于字符串的模式匹配。

本文将分别介绍并实现两种种常见的字符串模式匹配算法:暴力算法、BM算法(Boyer-Moore)。

一、暴力匹配算法

1. 介绍

暴力匹配法是最直观的一种字符串模式匹配算法,这种方法的主要思想是先枚举出所有可能的情况,然后找出符合要求的结果。具体地,将模式串 pattern 从索引为 0 的位置依次和 text 索引在 0 、1 、2 直到 n − m n-m nm 的索引处进行对齐,然后迭代 m m m 次判断是否可以在 text 中找出和 pattern 匹配的子串。

2. 实现

下面给出暴力法实现字符串模式匹配的具体代码:

def find_brute(text, pattern):
    """如果子串pattern存在text中,则返回pattern在text中起始位置索引,否则返回-1"""
    n, m = len(text), len(pattern)
    for i in range(n - m + 1):  # 从索引0至n - m处依次尝试开始匹配
        k = 0
        while k < m and text[i + k] == pattern[k]:  # 第k个字符匹配
            k += 1
        if k == m:  # 判断此轮for循环是否成功匹配
            return i  # 子串text[i:i + m]和pattern匹配
    return -1

3. 复杂度

如暴力法的思想和实现一样,其时间复杂度分析同样直观。该算法包括两个嵌套的循环,外层循环依次迭代每个可能的子串起始索引,内层循环判断此次外层循环的迭代是否成功匹配。显然,在最坏的情况下,外层循环最多执行 n − m + 1 n-m+1 nm+1 次,内层循环最多执行 m m m 次,因此该方法的最坏时间复杂度为 O ( n m ) O(nm) O(nm)

二、Boyer-Moore算法

1. 介绍

上述暴力匹配算法虽然直观,但是在最坏情况下,需要在穷举 text 的所有可能子串,然后和模式串 pattern 逐字符比对,因此效率较低。下面将介绍的 Boyer-Moore(BM)算法则可以跳过 patterntext 的子串比对时相当部分的字符。该方法主要基于下列两个手段:

  • 镜像试探:在将模式串 patterntext 的某子串进行比对时,从右到左进行比较,而不是从左到右(这也是这里镜像二字的含义);
  • 字符跳跃试探:在将模式串 patterntext 的某子串进行比对时,当该子串中某字符 text[i]=c 和模式串对应位置字符 pattern[k] 不匹配时,根据两种不同情况,分别做如下操作:
    • 如果字符 text[i]=c 不在模式串 pattern 中,则将模式串 pattern 向右整体移过 text[i] 所在位置;
    • 如果字符 text[i]=c 模式串 pattern 中,为确保不漏掉可能成功匹配的情况,只能将模式串 pattern 整体向右移动,直到 pattern 中的最后一次1出现的字符 ctext[i] 对齐。

为便于读者理解,结合下图对上述两个过程进行详细解释:

  • 一开始,模式串 'sushi'text 在最左侧对齐,然后将二者从右往左进行比对;
  • 接着,由于 text 中的字符 'e' 和模式串对应位置的字符 'i' 不匹配,且字符 'e' 不在模式串 'sushi' 中,因此将模式串整体向右移过字符 'e' 处;
  • 然后,继续将模式串和 text 从右往左进行比对,虽然此时 text 中的字符 's' 和模式串对应位置字符 'i' 不匹配,但是字符 's' 包含在模式串中,因此将模式串向右移动,直到模式串中右起第一个 's' 字符和 text 中的该字符对齐。

在这里插入图片描述

上图仅描绘了当 pattern 的最后一个字符和 text 对应位置字符不匹配时应该进行的操作。一般地,当二者匹配时,此时算法会接着从 pattern倒数第二个字符开始,尝试继续扩大发生匹配的字符数。此过程会一直继续,直到整个 pattern 成功匹配或发生某个字符不匹配。

如果在上述继续扩大发生匹配字符数的过程中确实发生某字符不匹配,则需要根据下列两种情况做不同处理:

  • 如果 text 中当前不匹配的字符不存在 pattern 中,则类似上图,将模式串 pattern 整体移过该字符;
  • 如果 text 中当前不匹配的字符(假设此时 text 该位置为 'a'pattern 的该位置为 'b'存在 pattern 中,则根据 text 中字符 'a' 最后一次出现在 pattern 中的位置,分别做如下处理:
    • 如果在 pattern 中,字符 'a' 最后一次出现的位置在字符 'b' 之前,则向右整体移动 pattern ,使得二者在字符 'a' 处对齐;
    • 如果在 pattern 中,字符 'a' 最后一次出现的位置在字符 'b' 之后,则向右整体移动 pattern 一个字符。

为便于后续的编码实现,下面结合示意图对上述两种情况进行量化分析:

为便于分析,下图2中的索引 ikj 的含义分别为:

  • i:表示继续扩大匹配字符数的过程中,匹配失败的字符 'a'text 中的索引;
  • k:表示和上述字符 'a' 对齐的字符(此处为 'b' )在 pattern 中的索引;
  • j:表示 pattern 中最后一次出现的字符 'a' 的在 pattern 中的索引。

因此:

  • j < k j\lt{k} j<k ,即 pattern 中字符 'a' 最后一次出现的位置在字符 'b' 之前,则将 pattern 整体向右移动 k − j k-j kj 个字符,此时索引 i 需递增 ( k − j ) + ( m − 1 − k ) = m − ( j + 1 ) (k-j)+(m-1-k)=m-(j+1) (kj)+(m1k)=m(j+1)
  • j > k j\gt{k} j>k ,即 pattern 中字符 'a' 最后一次出现的位置在字符 'b' 之后,则将 pattern 整体向右移动1个字符,此时索引 i 需递增 1 + ( m − 1 − k ) = m − k 1+(m-1-k)=m-k 1+(m1k)=mk

在这里插入图片描述

在实现字符串匹配的 BM 算法之前,为了使读者对上述一系列分析有一个集中的直观了解,下面给出通过BM 算法以模式串 pattern = 'abacab' 匹配字符串 text = 'abacaabadcabacabaabb' 的详细过程:

  • pattern 处于第一个位置时,由于第一次即发生不匹配,且字符 'a' 存在 pattern 中,将 pattern 向右平移一个字符到达第二个位置,累计进行了 1 次匹配操作;
  • pattern 处于第二个位置时,直到累计发生的第 4 次匹配才出现不匹配现象,由于此时 text 中和当前位置 pattern 中字符 'c' 对齐的字符 'a'pattern 中, pattern 中最后一个 'a' 在字符 'c' 后面,则将 pattern 整体向右移动一个字符;
  • pattern 处于第三个位置时,发生第 5 次匹配操作,此时过程和 pattern 处于第一个位置的情况类似;
  • pattern 处于第四个位置时,发生第 6 次匹配操作,由于此时 text 中的字符 'd' 不在 pattern 中,因此将 pattern 整体移过字符 'd'
  • pattern 处于第五个位置时,发生第 7 次匹配操作,此时此时过程和 pattern 处于第一、三个位置的情况类似;
  • pattern 处于第六个位置时,发生第8、9、10、11、12、13匹配,最终匹配成功。

在这里插入图片描述

2. 实现

下面是基于BM算法实现的字符串匹配:

def find_boyer_moore(text, pattern):
    """如果模式串pattern存在text中,则返回pattern在text中起始位置索引,否则返回-1"""
    n, m = len(text), len(pattern)
    if m == 0:
        return 0
    last = {}
    for k in range(m):  # 以pattern中字符为键索引为值创建字典
        last[pattern[k]] = k
    # 初始化索引辅助变量,使得pattern最右侧字符和text索引m - 1处对齐
    i = m - 1
    k = m - 1
    while i < n:
        if text[i] == pattern[k]:
            if k == 0:  # 判断是否连续完成了len(pattern)次成功匹配
                return i  # 成功将pattern和text某子串进行匹配后,text中和pattern相同的子串起始于索引i
            else:  # 继续从右向左比对pattern和text对齐位置字符相同
                i -= 1
                k -= 1
        else:
            j = last.get(text[i], -1)
            if j < k:  # text[i]不存在pattern中即j = -1时,该条件及其操作依然成立
                i += m - (j + 1)
            if j > k:
                i += m - k
            k = m - 1  # 重新从右开始对pattern和text进行匹配
    return -1


if __name__ == '__main__':
    text = 'qwertyqazxswedcvfrtgb'
    pattern = 'zx'
    print(find_boyer_moore(text, pattern))  # 8

  1. 字符串中字符从左到右排列,如一个字符出现次数大于等于1,则位于最右侧的该字符被视为最后一次出现。 ↩︎

  2. 需要注意的是,图中仅示意在发生此次不匹配前, patterntext 在当前位置仅有最后一个字符发生匹配。实际上,图中的情形具有一般性,即适用于所有已发生小于 m m m 个字符匹配的情况。 ↩︎

评论 5
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值