子字符串查找算法(BF、KMP、BM、RK)

子字符串查找算法(BF、KMP、BM、RK)

BF 子字符串查找算法

在长度为 N 的文本中查找长度为 M 的模式

暴力子字符串查找

  1. 指针 i 遍历文本串每次匹配的起始位置
  2. 指针 j 遍历模式串中的字符,每次重置为 0
  3. 比较文本串中的第 i+j 个字符和模式串中第 j 个字符是否匹配

最坏情况:文本串和模式串都是一连串的A接单个B的形式,则每次都在模式串的最后一个字符处匹配失败。可能的匹配位置为 N - M + 1,每次都需要匹配模式串中的所有字符,则总比较次数为 M(N-M+1)。

// 暴力字符串查找算法
// 可能匹配的位置为: txtLen - patLen + 1
static int BruteForceSubStringSearch(String pat, String txt) {
   

    // 指针 i 跟踪文本串中的单个字符, 表示每次匹配的起始位置
    for (int i = 0; i < txtLen - patLen + 1; i++) {
   
        // 指针 j 跟踪模式串中的单个字符
        int j;
        for (j = 0; j < patLen; j++) {
   
            // 比较文本串中的字符和模式串中的字符是否匹配, 不匹配时跳出循环
            // i+j: 文本串中从 i 开始的第 j 个字符
            if (txt.charAt(i + j) != pat.charAt(j)) {
   
                break;
            }
        }
        // 如果模式串匹配结束, 则保存该次模式串出现位置的起始下标
        if (j == patLen) {
   
            return i;
        }
    }
    return txtLen;
}

显式回退的暴力子字符串查找

  1. 指针 i 遍历文本串每次匹配的位置

  2. 指针 j 遍历模式串每次匹配的位置

  3. 如果匹配,则两个指针同时 +1;

    如果不匹配,则文本串指针回退到本次匹配的起始位置,模式串指针回退到模式的开头

// 显式回退的暴力子字符串查找
static int ExplicitFallbackSubStringSearch(String pat, String txt) {
   
    int i, j;
    // i 为每次匹配的字符, 等价于上段代码中的i + j
    for (i = 0, j = 0; i < txtLen && j < patLen; i++) {
   
        // 文本串中的字符和模式串中的字符串相等时, 文本串和模式串中的指针同时后移
        if (txt.charAt(i) == pat.charAt(j)) {
   
            j++;
        } else {
   
            // i 在遍历的过程中指针文本中已经匹配的字符序列的末端
            // 指针字符不匹配时, 需要同时回退这两个指针
            // i-j 指向本次匹配的起始位置, 在下一次循环时指向后一个位置
            i -= j;
            // j 指向模式的开头
            j = 0;
        }
    }
    // 如果模式串匹配结束, 则保存该次模式串出现位置的起始下标
    if (j == patLen) {
   
        return i - patLen;
        // 未找到匹配
    } else {
   
        return txtLen;
    }
}

KMP 子字符串查找算法(DFA)

最大前后缀长度

例如串 abcdef:

  • 前缀(不包括尾字符):a、ab、abc、abcd、abcde

  • 后缀(不包括首字符):f、ef、def、cdef、bcdef

  • 最大前后缀长度(前缀和后缀中最大的相同串的长度):0(因为其前缀与后缀没有相同的)

例如串 ababa:

  • 前缀:a、ab、aba、abab
  • 后缀:a、ba、aba、baba
  • 最大前后缀长度:3(aba,长度3)

模式指针的回退

固定文本串不动,从左到右滑动模式字符串直到所有重叠的字符都相互匹配(或没有相匹配的字符)时才停止。在这个过程中,文本指针 i 不会回退,模式指针 j 使用数组 dfa 来更新。

i = i+1

j = dfa[txt.charAt(i)] [j]

dfa[文本串中指针 i 对应的字符 c ] [模式串中的指针 j ]:

  • 文本串的第 i 个字符 c 和模式串的第 j 个字符比较之后,模式串中应该和 文本串的下一个(第 i+1 个)字符 比较的 字符位置

  • 表示比较后模式字符串的指针索引,同时为已匹配字符的个数

  • 匹配时:

    文本串:AABAABAAAA

    模式串:AABAAA

    • txt.charAt(i) == pat.charAt(j),即模式串的前 j 个字符和文本串对应相同

      则应该继续比较模式串和文本串的下一个字符

    • j = dfa[txt.charAt(i)] [j] = j+1 ,即文本串的第 i+1 个字符和模式串的第 j+1 个字符比较

  • 不匹配时:

    文本串:AABAABAAAA

    模式串:AABAAA

    • txt.charAt(i) != pat.charAt(j),即模式串的前 j-1 个字符和文本串对应相同

      即已知文本串的 [i-j+1, … , i-1, i] 个字符

    • j = dfa[txt.charAt(i)] [j] = 模式串(从位置 1 开始)和文本串(到位置 i 结束)的最大前后缀长度

      文本串:AABAABAAAA

      模式串: AABAAA

      文本串取后缀 [i-j+2, … , i-1, i],模式串取前缀[1, … , j-1],找最大前后缀长度

总结:

  • 在匹配失败时,如果 模式字符串中的某处 可以和 匹配失败处的正文 相匹配(最大前后缀),那么就不应该完全跳过所有已经匹配的所有字符(i = i-j+1 和 j = 0为跳过)

  • i = i+1

    • 匹配成功:j = dfa[txt.charAt(i)] [j] = j+1
    • 匹配失败:j = dfa[txt.charAt(i)] [j] = 模式串和文本串的最大前后缀长度

    在匹配失败时总是能够将 j 设为某个值以使 i 不回退

  • 需要提前判断如何重新开始查找,而这种判断只取决于模式本身

    匹配失败时,模式串的前 j-1 个字符和文本串对应相同,则由模式串自身可以得到重新回退的位置

KMP查找算法

继续比较的字符指针 文本串 i 模式串 j
匹配成功 i = i+1 j = j+1
匹配失败(暴力) i = i-j+1 j = 0
匹配失败(KMP) i = i+1 j = dfa[txt.charAt(i)] [j]

为什么使用 dfa[] [] 记录指针 j 后,指针 i 不用回退?

当 i 和 j 所指向的字符匹配失败时,如果是从文本串 i-j+1 处(模式串头部)开始继续检查匹配情况

则文本串中可能和模式串匹配的下一个位置应该为 i-dfa[txt.charAt(i)] [j]

又文本串中从该位置开始的 dfa[txt.charAt(i)] [j] 个字符和模式串的前 dfa[txt.charAt(i)] [j] 应该相同

因此将 j 设为 dfa[txt.charAt(i)] [j] 后,模式串的前 j 个字符和文本串已经匹配,

则无需回退指针 i ,只需要将 i 设为 i+1 ,继续比较即可

这与指针 i 和 j 所指向的字符匹配成功时的行为相同

DFA模拟

DFA(确定有限状态自动机):
  • 每个模式字符串都对应着一个自动机(由保存了所有转换的 dfa[] [] 数组表示)

  • 状态:数字标记的圆圈(每个状态都表示模式字符串中的索引值)

    开始状态:0

    中间状态 j :表示已经有 j 个字符匹配

    停止状态:M (模式串的长度),不会进行任何转换

  • 转换:带标签的箭头(文本串对应字母表中的每个字符)

    只有一条是匹配转换(即从 j 到 j+1,标签为 pat.charAt(j))

    其他的都是非匹配转换(指向左侧)

    所有状态转换都和字符比较相对应

自动机运行过程:
  1. 从状态 0 开始,每次从左向右在文本中读取一个字符并移动到一个新的状态

  2. 停留在状态 0 并扫描文本,直到找到一个和模式首字母相同的字符,移动到下一个状态并开始运行

  3. 当状态 j 检查文本中的第 i 个字符时,沿着转换 dfa[txt.charAt(i)] [j] 前进

    • 匹配转换:j = dfa[txt.charAt(i)] [j] = j+1,向右移动进入下一个状态

      等价于增大模式字符串的指针 j

    • 非匹配转换:j = dfa[txt.charAt(i)] [j] = 最大前后缀长度,向左移动回到较早的状态

      等价于将模式字符串的指针 j 变为一个较小值

    并继续检查下一个字符(i+1)

  4. 结果判断

    1. 到达状态 M :识别到该模式(从文本中找到了和模式相匹配的一段子字符串)

    2. 文本结束时未达到状态M,则文本中不存在匹配该模式的子字符串

KMP子字符串查找算法(DFA模拟):
// 在 txt 中找到 pat 的出现位置
public int search(String txt){
   
    int i, j, N = txt.length(), M = pat.length();
    for(i=0, j=0; i<N && j<M;i++){
   
        j = dfa[txt.charAt(i)][j];
    }
    // 找到匹配
    if(j
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值