【算法笔记】BF 和 KMP 算法

1. BF 算法

1.1 介绍

BF 算法,即暴力算法,是普通的模式匹配算法。BF 算法的思想就是将主串 S 的第一个字符与子串 T 的第一个字符进行匹配。若相等,则继续比较 S 和 T 的第二个字符;若不等,则比较 S 的第二个字符和 T 的第一个字符。依次比较下去,直到得出最后的匹配结果。BF 算法是一种蛮力算法。

1.2 模拟过程(思想)

我们可以先假设主串为:ababcabcdabcde

再假设子串为:abcd

接下来我们就模拟一遍如何在主串中找到和子串匹配的字符串,并返回其其位置

在这里插入图片描述

在上述字符串中,i 和 j 分别是主串和子串的的索引,都从0开始。

  • i == j 时,i 和 j 分别往后走一步。

    如上述字符串,当 i == j == 0 时,子串字符等于主串字符,故 i 和 j 分别往后走一步。当 i == j == 1 时,子串字符等于主串字符,故 i 和 j 分别往后走一步。

  • i != j 时,此时 j == 0i = i - j + 1(原因是 i == j 时,i 和 j 每次都是一起走一步)

    如上述字符串,当 i == j == 3 时,子串字符不等于主动字符,此时子串就要从头开始遍历,即 j == 0。而主串遍历的位置就要回到这次遍历时的第一个索引的下一个索引位置,由于子串和主串都是一步一步走的,所以回到该次遍历时索引的位置是 i-j,而到这个位置的下一个索引是 i-j+1。

  • 遍历结束时,当 j == 子串长度时,则在主串中找到了子串,返回主串中找到的子串的位置 i = i-j

  • 遍历结束时,当 j != 子串长度时,则没有在主串中找到子串,可以返回-1来表示查询失败

1.3 示例代码

我们按照上述模拟过程及其思想可以写一个 BF 算法

public static int BF(String str,String sub,int pos){
    
    
    // 首先对主串和子串的特殊条件,以及主串初始查询的位置进行处理
    if(str==null || sub==null){
    
    
        return -1;
    }
    int lenStr=str.length();
    int lenSub=sub.length();
    if(lenStr==0 || lenSub==0){
    
    
        return -1;
    }
    if(pos<0 || pos>=lenStr){
    
    
        return -1;
    }
    int i=pos;  // i 表示主串的起始坐标
    int j=0;    // j 表示子串的起始坐标
    // 遍历,进行主串对子串的查询
    while(i<lenStr && j<lenSub){
    
    
        if (str.charAt(i) == sub.charAt(j)) {
    
    
            i++;
            j++;
        }else{
    
    
            i=i-j+1;
            j=0;
        }
    }
    if(j==lenSub){
    
    
        return i-j;
    }
    return -1;
}

补充:

charAt(index) 方法是 String 类中的方法,作用是返回该字符串指定索引处的字符

1.4 时间复杂度

BF 算法的时间复杂度最坏为:O(m*n),其中:m 是主串长度,n 是子串长度

2. KMP 算法

2.1 介绍

KMP 算法是一种改进的字符串匹配算法,由 D.E.Knuth、J.H.Morris 和 V.R.Pratt 提出,因此人们称它为克努特—莫里斯—普拉特操作(简称为 KMP 算法)。KMP 算法的核心是利用匹配失败后的信息,尽量减少子串与主串的匹配次数,已到达快速匹配的目的。具体实现就是通过一个 next 数组实现,数组本身包含了子串的匹配局部信息

2.2 模拟过程(思想)

2.2.1 整体思路

我们可以先假设主串为:abcababcabc

再假设子串为:abcabda

接下来我们就使用 KMP 算法的思想来模拟一遍在主串中查询子串

在这里插入图片描述

在上述字符串中,i 和 j 分别是主串和子串的的索引,都从0开始。

  • i == j 时,i 和 j 分别往后走一步(这里和 BF 算法一样)

    如上述字符串,当 i == j == 0 时,子串字符等于主串字符,故 i 和 j 分别往后走一步。当 i == j == 1 时,子串字符等于主串字符,故 i 和 j 分别往后走一步。

  • i != j 时,此时 子串索引 j 回退到一个指定的值,主串索引 i 不回退(KMP 和 BF 唯一的不同)

    上述便是 KMP 算法与 BF 算法的不同之处,至于为何主串的索引不回退,为何子串的索引要返回一个指定的值,下面具体分析

  • 遍历结束时,当 j == 子串长度时,则在主串中找到了子串,返回主串中找到的子串的位置 i = i-j

  • 遍历结束时,当 j != 子串长度时,则没有在主串中找到子串,可以返回-1来表示查询失败

2.2.2 主串的索引不回退

通过上图分析,当 i == j == 5 时,主串就不能与子串匹配成功了,这时按照暴力求解的思路,主串的索引 i 就要回退到该次匹配时起始位置的下一个位置,也就是要 i == 1

但是 i 其实不需要回退,分析如下:

  • i == j == 5 时,之前主串与子串的值是匹配的,那么只要后面能匹配成功,匹配成功的几个位置也绝对要覆盖 i == 5 或者在 i == 5 的后面,因此此时 i 的值可以不动
  • 由于 i 的值不进行回退,那么我们就要调动子串 j 的值,但是此时 j 的值不能直接回退到初始的 0 位置,一是因为这是主串不回退带来的影响,二是还有更巧妙的方法

主串不回退带来的影响:

由于主串不回退,因此主串只需要遍历一遍,相比于 BF 算法有了一定的提升!

2.2.3 子串的索引回退的位置

其实子串的每个位置都是有固定的匹配失败后回退的位置的,并且这些回退的位置都可以存储到一个 next 数组中。KMP 的精髓就是这个 next 数组

接下来我们来探索如何求得子串的每个位置匹配失败后回退的位置,以下图为例

在这里插入图片描述

  • 首先当 i == j == 5 时,第一次匹配失败,因此我们要找一个子串的回退位置,并且当 j 回退后,主串的索引 i 之前部分位置的字符能与子串的新移过来的对应位置的字符所匹配

  • 当我们匹配失败后,因为匹配失败位置前的字符都是匹配成功的,因此,我们需要以子串的 0 位置为起点往后找一个串C,并在子串匹配失败的位置前面那个位置为结尾往前找一个串B,如果能够找到串C与串B相等,那么我们就可以用相等的这两个串的长度,作为 j 回退的位置

  • 如上图,串C和串B就是我们找到的两个串,故当子串位置为5匹配失败时,我们就让子串退回到下标为2的位置

  • 当我们找到退回的位置后,我们可以发现此时子串的串C与主串的串A部分是一样的,这样我们就不需要暴力的将主串的索引回退以及将子串的索引回退到0 [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-oxEuvFKP-1640202191031)(C:\Users\bbbbbge\Pictures\魔王爱算法\Snipaste_2021-12-23_02-12-26.png)]

  • 根据以上规则,我们继续用这个思想进行字符串匹配,此时 i == 5,j == 2,对应串的字符其实又不相等,我们要继续在在子串的 0位置为起始位置找一个串C,并在匹配失败的前一个位置为结尾找一个串B,但这次我们不能找到串C和串B能相等的两个串,因此 j 要退回到起始 0 位置 在这里插入图片描述

2.2.4 next 数组的实现方法

通过上面,我们会发现,KMP 算法和 BF 算法的唯一不同就是:i 不回退,j 回退的位置是指定的

并且子串匹配失败回退的位置,其实可以通过子串本身来找到,并且提前放置到一个 next 数组中

next 数组实现方法如下:

  • 设定 next[0] = -1,next[1] = 0
  • 定义一个 k = 0,表示 j == 2 前面位置的 next 值
  • j == 2 开始,如果子串 j - 1 位置的字符与 k 位置的字符相等,那么 next[j] = k + 1,否则 next[j] 位置的字符就要与 k 的 next 位置的字符比较 … 一直到相等,或者后者的位置为0

2.2.5 next 数组的优化

假设一个子串为:aaaaaaaab,那么它的 next 数组为:{-1,0,1,2,3,4,5,6,7}

当主串为 i 位置的字符与子串中的最后一个 a 字符不等时,匹配的子串字符会回退到下标为6的位置,此时子串的字符还是 a,与主串依旧不匹配,需要回退,直到回退到第一个位置的字符 a 时,才能结束

对于这种情况,我们可以做一个优化,当回退位置的字符 x1 与当前字符 x2 相同时,则继续回退至 x1 的 next 值处,直到回退到与字符 x2 不相等或者为0时才结束

因此就可以得到一个优化后的 nextVal 数组:{-1,-1,-1,-1,-1,-1,-1,-1,7},这样就可以再次提升效率

2.3 示例代码

我们按照上述模拟过程及其思想可以写一个 KMP 算法

public static int KMP(String str,String sub,int pos){
    
    
    if(str==null || sub==null){
    
    
        return -1;
    }
    int lenStr=str.length();
    int lenSub=sub.length();
    if(lenStr==0 || lenSub==0){
    
    
        return -1;
    }
    if(pos<0 || pos>=lenStr){
    
    
        return -1;
    }
    int i=pos;
    int j=0;
    int[] next=new int[lenSub];
    getNext(sub,next);
    while(i<lenStr && j<lenSub){
    
    
        if((j==-1)||str.charAt(i)==sub.charAt(j)){
    
    
            i++;
            j++;
        }else{
    
    
            j=next[j];
        }
    }
    if(j==lenSub){
    
    
        return i-j;
    }
    return -1;
}
public static void getNext(String sub,int[] next){
    
    
    if(sub==null || sub.length()==0){
    
    
        return;
    }
    next[0]=-1;
    next[1]=0;
    int i=2;
    int k=0;    // 前一项的返回值,即next[1]返回值
    while(i<sub.length()){
    
    
        if((k==-1) || sub.charAt(i-1)==sub.charAt(k)){
    
    
            next[i]=k+1;
            i++;
            k++;
        }else{
    
    
            k=next[k];
        }
    }
}

2.4 时间复杂度

KMP 算法的时间复杂度为:O(m+n),其中:m 是主串长度,n 是子串长度

猜你喜欢

转载自blog.csdn.net/weixin_51367845/article/details/122098687