串(Sequence)

目录

串(Sequence)

串匹配算法

蛮力(Brute Force)

蛮力算法1

蛮力1 – 执行过程

蛮力1 – 实现

蛮力1 – 优化

蛮力1 – 优化实现

蛮力算法2

蛮力2 – 执行过程

蛮力2 – 实现

蛮力 – 性能分析

蛮力 vs  KMP

KMP

KMP – next表的使用

KMP – 核心原理

KMP – 真前缀后缀的最大公共子串长度

KMP – 计算next表

KMP – -1的精妙之处

KMP – next表的实现

KMP – 为什么是“最大“公共子串长度?

KMP – next表的构造思路

KMP – next表的优化实现

KMP – next表的不足之处

KMP – next表的优化思路

KMP – next表的优化实现

KMP – next表的优化效果

KMP – 性能分析


串(Sequence)

字符串 thank 的前缀(prefix)、真前缀(proper prefix)、后缀(suffix)、真后缀(proper suffix)

串匹配算法

  • 蛮力(Brute Force)
  • KMP
  • Boyer-Moore
  • Karp-Rabin
  • Sunday

蛮力(Brute Force)

以字符为单位,从左到右移动模式串,直到匹配成功

蛮力算法1

蛮力1 执行过程

蛮力1 实现

/**
 * 蛮力匹配1
 */
public static int indexOf1(String text, String pattern){
    if(text == null || pattern == null) return -1;
    char[] tChars = text.toCharArray();
    char[] pChars = pattern.toCharArray();
    int tlen = tChars.length;
    int plen = pChars.length;
    if(tlen == 0 || plen == 0 || tlen < plen) return -1;
    int ti = 0, pi = 0;
    //当ti和pi都未到达字符串尾部,那么字符串匹配还未完成
    while(ti < tlen && pi < plen){
        if(pChars[pi] == tChars[ti]){
            pi++;
            ti++;
        }else{
            ti = ti - pi + 1;  //下一次比较时ti的起始位置, ti-pi: 本次比较中ti的起始位置
            pi = 0;
        }
    }
    return pi == plen ? ti-pi: -1;
}

蛮力1 优化

此前实现的蛮力算法,在恰当的时候可以提前退出,减少比较次数

因此,ti 的退出条件可以从 ti < tlen 改为:

  •    ti – pi <= tlen – plen
  •    ti – pi 是指每一轮比较中 text 首个比较字符的位置

蛮力1 优化实现

/**
 * 蛮力匹配1 -- 优化
 */
public static int indexOf1_1(String text, String pattern){
    if(text == null || pattern == null) return -1;
    char[] tChars = text.toCharArray();
    char[] pChars = pattern.toCharArray();
    int tlen = tChars.length;
    int plen = pChars.length;
    if(tlen == 0 || plen == 0 || tlen < plen) return -1;
    int ti = 0, pi = 0;
    int tmax = tlen-plen;
    //当ti和pi都未到达字符串尾部,那么字符串匹配还未完成
    while(ti-pi <= tmax && pi < plen){
        if(pChars[pi] == tChars[ti]){
            pi++;
            ti++;
        }else{
            ti = ti - pi + 1;
            pi = 0;
        }
    }
    return pi == plen ? ti-pi: -1;
}

蛮力算法2

蛮力2 执行过程

蛮力2 实现

/**
 * 蛮力匹配2
 */
public static int indexOf2(String text, String pattern){
    if(text == null || pattern == null) return -1;
    char[] tChars = text.toCharArray();
    char[] pChars = pattern.toCharArray();
    int tlen = tChars.length;
    int plen = pChars.length;
    if(tlen == 0 || plen == 0 || tlen < plen) return -1;
    int ti = 0, tmax = tlen-plen;
    //当ti和pi都未到达字符串尾部,那么字符串匹配还未完成
    for (;ti < tmax;ti++){
       int pi = 0;
       for(;pi < plen; pi++){
           if(tChars[ti + pi] != pChars[pi]) break;
       }
       if(pi == plen) return ti;
    }
    return -1;
}

性能分析

最好情况:
   只需一轮比较就完全匹配成功,比较 m 次( m 是模式串的长度)
   时间复杂度为 O(m)

最坏情况(字符集越大,出现概率越低) :

  • 执行了 n – m + 1 轮比较( n 是文本串的长度)
  • 每轮都比较至模式串的末字符后失败( m – 1 次成功,1 次失败)
  • 时间复杂度为 O(m ∗ (n − m + 1)),由于一般 m 远小于 n,所以为 O(mn)

vs  KMP

对比蛮力算法,KMP的精妙之处:充分利用了此前比较过的内容,可以很聪明地跳过一些不必要的比较位

KMP

KMP next表的使用

KMP会预先根据模式串的内容生成一张 next 表(一般是个数组

KMP 核心原理

当 d、e 失配时,如果希望 pattern 能够一次性向右移动一大段距离,然后直接比较 d、c 字符
前提条件是 A 必须等于 B
所以 KMP 必须在失配字符 e 左边的子串中找出符合条件的 A、B,从而得知向右移动的距离
向右移动的距离:e左边子串的长度 – A的长度,等价于:e的索引 – c的索引
且 c的索引 == next[e的索引],所以向右移动的距离:e的索引 – next[e的索引]

总结:
如果在 pi 位置失配,向右移动的距离是 pi – next[pi],所以 next[pi] 越小,移动距离越大
next[pi] 是 pi 左边子串的真前缀后缀的最大公共子串长度

KMP 真前缀后缀的最大公共子串长度

KMP  计算next

将最大公共子串长度都向后移动 1 位,首字符设置为 负1,就得到了 next 表

KMP  -1的精妙之处

相当于在负1位置有个假想的通配字符(哨兵), 匹配成功后 ti++、pi++

KMP next表的实现

/**
 * KMP
 */
public static int indexOf(String text, String pattern){
    if(text == null || pattern == null) return -1;
    char[] tChars = text.toCharArray();
    char[] pChars = pattern.toCharArray();
    int tlen = tChars.length;
    int plen = pChars.length;
    if(tlen == 0 || plen == 0 || tlen < plen) return -1;
    int[] next = next_better(pChars);
    int ti = 0, pi = 0;
    int imax = tlen-plen;
    //当ti和pi都未到达字符串尾部,那么字符串匹配还未完成
    while(ti-pi <= imax && pi < plen){
        if(pi < 0 || pChars[pi] == tChars[ti]){
            pi++;
            ti++;
        }else{
            pi = next[pi];
        }
    }
    return pi == plen ? ti-pi: -1;
}

KMP 为什么是“最大“公共子串长度?

假设文本串是AAAAABCDEF,模式串是AAAAB

KMP next表的构造思路

已知 next[i] == n
(1) 如果 pattern.charAt(i) == pattern.charAt(n)
            那么 next[i + 1] == n + 1
(2) 如果 pattern.charAt(i) != pattern.charAt(n)
      已知 next[n] == k
      如果 pattern.charAt(i) == pattern.charAt(k)
            那么 next[i + 1] == k + 1
      如果 pattern.charAt(i) != pattern.charAt(k)
            将 k 代入 n ,重复执行(2)

KMP next表的优化实现

private static int[] next(char[] pChars) {
    int len = pChars.length;
    int[] next = new int[len];
    int i = 0;
    int n = next[0] = -1;
    int imax = len-1;
    while(i < imax){
        if(n < 0 || pChars[i] == pChars[n]){
            next[++i] = ++n;
        }else{
            n = next[n];
        }
    }
    return next;
}

KMP next表的不足之处

假设文本串是 AAABAAAAB, 模式串是 AAAAB

在这种情况下,KMP显得比较笨

KMP next表的优化思路

已知:next[i] == n,next[n] == k

如果 pattern[i] != d,就让模式串滑动到 next[i](也就是n)位置跟 d 进行比较
如果 pattern[n] != d,就让模式串滑动到 next[n](也就是k)位置跟 d 进行比较
如果 pattern[i] == pattern[n],那么当 i 位置失配时,模式串最终必然会滑到 k 位置跟 d 进行比较
所以 next[i] 直接存储 next[n](也就是k)即可

KMP next表的优化实现

private static int[] next_better(char[] pChars) {
    int len = pChars.length;
    int[] next = new int[len];
    int i = 0;
    int n = next[0] = -1;
    int imax = len-1;
    while(i < imax){
        if(n < 0 || pChars[i] == pChars[n]){
            i++;
            n++;
            if(pChars[i] == pChars[n]){
                next[i] = next[n];
            }else{
                next[i] = n;
            }
        }else{
            n = next[n];
        }
    }
    return next;
}

KMP next表的优化效果

KMP 性能分析

KMP 主逻辑
最好时间复杂度:O(m)
最坏时间复杂度:O(n), 不超过O(2n)

next 表的构造过程跟 KMP 主体逻辑类似, 时间复杂度:O(m)

KMP 整体
最好时间复杂度:O(m)
最坏时间复杂度:O(n + m)
空间复杂度:O(m)

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值