我所知道的十大常用算法之字符串匹配问题(KMP、暴力匹配)

28640

前言需求


前面讲解了动态规划的思想,今天我们要说的是涉及字符串匹配问题(KMP)

现在前来看我们的问题

1.字符串 str1 = "BBC ABCDAB ABCDABCDABDE"

2.字符串 str2 = "ABCDABD"

现在要判断 str1 是否含有 str2, 如果存在,就返回第一次出现的位置, 如果没有,则返回-1

那么你觉得你会是怎么做的呢?

一、小明同学的思路想法

小明同学:我先找到符合str2 字符串的第一个'A'的位置

小明同学:将str2 的字符串一个一个的进行匹配否相等

那么小明同学的这种思路呢,我们称为:暴力匹配

我们假设现在str1匹配到 i 位置,子串str2匹配到 j 位置,则会:

1.匹配成功(即str1[i] == str2[j]),则i++,j++,继续匹配下一个

2.匹配失配(即str1[i]! = str2[j]),令i = i - (j - 1),j = 0。相当于每次匹配失败时,i 回溯,j 被置为0

public static  int violenceMatch(String str1,String str2){
     char[] s1 = str1.toCharArray();
     char[] s2 = str2.toCharArray();
     int s1Len = s1.length;
     int s2Len = s2.length;
     int i = 0;//i索引指向s1
     int j = 0;//j索引指向s2
     while(i<s1Len && j<s2Len){
        if(s1[i] == s2[j]){
            i++;
            j++;
         }else{
             //如果匹配失败
             i = i - (j - 1);
             j = 0;
         }
     }
            //判断是否匹配成功
     if(j == s2Len){
        return i - j;
     }else{
        return -1;
     }   
}

我们可以使用demo 测试一下这种方法

public static void main(String[] args) {
     String str1 = "BBC ABCDAB ABCDABCDABDE";
     String str2 = "ABCDABD";
     int index = violenceMatch(str1,str2);
     System.out.println(index);
}

运行结果如下:
15

二、小王同学的思路想法

小王同学:小明同学的方法虽然是可以完成找到,但是也有不好的地方

1.当匹配失败时候,就回溯重新进行匹配,这样的方法很复杂同时效率很慢

图片.png
图片.png
图片.png
图片.png
图片.png
图片.png

2.当匹配成功时候,才直接把i 的位置 - j的位置

小王同学:我知道一个算法,它比较适合做这样的事情:KMP 算法

三、什么是KMP 算法

Knuth-Morris-Pratt 字符串查找算法,简称为 “KMP算法”,常用于在一个文本串S内查找一个模式串P 的出现位置

KMP方法算法就利用

1.之前判断过信息,通过一个next数组保存模式串中前后最长公共子序列的长度

2.每次回溯时,通过next数组找到,前面匹配过的位置,省去了大量的计算时间

ok,什么意思?不懂没关系,我们一步一步来分析。

图解分析暴力匹配引入KMP

按照小明同学的思路,字符串Str1 与 字符串 Str2匹配 ,不满足则下一个匹配

图片.png

图片.png

一直重复匹配,直到Str1 与Str2 有一个字符匹配符合为止

图片.png

接着比较下一个字符串,还是匹配符合

图片.png

直到遇到Str1有一个字符串与Str2 对应的字符串(D与空格)不符合

图片.png

这个时候就回溯,又进行重新的匹配,从 B 与 A 匹配

图片.png

其实这是很不明智的,因为此时BCD已比较过了,没有必要做重复的工作,当空格与D不匹配的时候,其实你已知道前面六个字符是“ABCDAB”

KMP 算法的想法是:设法利用这个已知信息,不要把”搜索位置”移回已经比较过的位置,继续把它向后移,这样就提高了效率

步骤图解分析KMP的想法

这里怎么理解呢,一起画图来解释看看,当我们回溯重新匹配的时候

图片.png

发现 B 与A不符合 那么进行下一个,C与A匹配

图片.png

发现 C 与A不符合 那么进行下一个,D与A匹配....以此类推

图片.png

当A与A匹配的时候,那么又执行了B与B比,空格与C比,这样的重复

我们发现没有,BCD就已比较过了,所以我们不要把”搜索位置”移回已经比较过的位置
图片.png
图片.png
图片.png
图片.png

步骤图解分析的问题发现

如果我们知道这个A和后面的BCD是不相同的,那么这样的比较就没有意义的

那么有个问题需要思考一下:我们怎么知道这些比较会不相同呢?

这里就涉及到一个部分搜索词:字符串前缀与后缀公共的部分

我们可以创建一个搜索词(部分匹配表)

图片.png

在此之前,我们知道0000120是什么?怎么来的?为什么是这样的

这就要先说说字符串的前缀与后缀了

KMP的搜索词(部分匹配表)分析

我们采用一个字符串:bread 来分析前缀、后缀
图片.png

接下里我们在以刚刚的式子:ABCDABD 拆开来分析看看吧
图片.png

图片.png

现在你知道0000120 是怎么来的吗?每个匹配值你应该知道对应的是什么

图片.png

ok,回到刚刚我们提出的那个问题:我们怎么知道这些比较会不相同呢?

现在根据部门匹配值 你是有点思路的想法嘛?

四、通过一坨代码来验证思想KMP

首先我们先将字符串的部分匹配表弄出来,一起来看看代码是怎么做的?

public static  int[] kmpNext(String dest){

    //创建一个next数组保存部门匹配值
    int[] next = new int[dest.length()];
    
    //dest的字符个数只有一个,则直接 = 0;
    next[0] = 0;
    
    for(int i = 1,j = 0;i < dest.length() ; i++){        
        //kmp 算法的思想 相当于是回溯,一直往前比较
        while(j>0 && dest.charAt(i) != dest.charAt(j)){
            j = next[j-1];
        }
        if(dest.charAt(i) == dest.charAt(j)){
            j++;
        }
        next[i] = j;
    }
    return next;
}

好,这代码是什么情况?为什么dest.charAt(i) == dest.charAt(j)?

我们刚刚表格是从字符串的头开始分析的,还记得吗? 代入进去我们验证一下

图片.png

这个时候,有的小伙伴就有点懵逼了,为什么要执行while循环多一层判断呢?

这里的目的是要回溯,不然的话j++ 会造成以下的效果

图片.png

通过例子来体会KMP思想

我们使用一个例子来demo与查找方法体会一下

public static int kmpSearch(String str1,String str2){

    for(int i = 0,j = 0;i<str1.length();i++){

        if(str1.charAt(i) == str2.charAt(j)){
            j++;
        }
        if(j == str2.length()){
            return i - j +1;
        }
    }
    return -1;
}
public static void main(String[] args) {

    String str1 = "BBC ABCDAB ABCDABCDABDE";
    String str2 = "BBC";
    System.out.println("index = "+ kmpSearch(str1,str2));
}

运行结果如下:
index = 0

1.当我们进行匹配BBC的时候,charAt(i) == charAt(j) 是满足的,j++

2.当j =3 时,也等于BBC字符串的长度,这时我们代表找到了,这时i = 2

3.因为charAt(2) = C,只需要使用i - j + 1 则代表开始index坐标

问题发现

public static void main(String[] args) {

    String str1 = "BBC ABCDAB ABCDABCDABDE";
    String str2 = "ABCDABD";
    System.out.println("index = "+ kmpSearch(str1,str2));
}

运行结果如下:
index = 8

当我们将BBC换成ABCDABD的时候,结果就出现问题了index = 8

为什么会这样呢?

那是因为我们没有考虑不相等的情况,只考虑的相等的情况++

1.当我们不断的判断满足的时候,str2字符串只剩下D没匹配

2.str1 对应的i 字符:空格、A、B、C都不满足,一直到D 这时j++

图片.png

3.这时的条件满足了,但是不是我们想要的结果,所以我们需要考虑不相等

public static int kmpSearch(String str1,String str2,int next[]){

    for(int i = 0,j = 0;i<str1.length();i++){

        while(j>0 && str1.charAt(i) != str2.charAt(j)){
            j = next[j-1];
        }
        if(str1.charAt(i) == str2.charAt(j)){
            j++;
        }
        if(j == str2.length()){
            return i - j +1;
        }
    }
    return -1;
}
public static void main(String[] args) {
    String str1 = "BBC ABCDAB ABCDABCDABDE";
    String str2 = "ABCDABD";
    int [] next = kmpNext(str2);
    System.out.println("index = "+ kmpSearch(str1,str2,next));
}

五、步骤图演示KMP算法

那么接下来我们使用KMP算法图解的方式走一遍

首先先求出Str2 的部分匹配值

图片.png

这时,我们从这里开始讲解,一步一步的来,A与A匹配,此时j++
图片.png

下一个匹配,B与B匹配,此时j++..以此类推..
图片.png

直到遇到Str1有一个字符串与Str2 对应的字符串(D与空格)不符合
图片.png

此时(D与空格)不符合,那么J回溯到对应的匹配值,继续匹配

图片.png

此时(C与空格)不符合,那么J回溯到对应的匹配值,继续匹配

图片.png

此时(A与空格)不符合,所以接着Str1 字符串 i++ 进行下一个匹配,匹配上就j++ 以此类推

图片.png
图片.png

因为此时BCD已比较过了,没有必要做重复的工作,当空格与D不匹配的时候,其实已知道前面六个字符是“ABCDAB”

直到遇到Str1有一个字符串与Str2 对应的字符串(D与C)不符合

图片.png

此时(D与C)不符合,那么J回溯到对应的匹配值,继续匹配

图片.png

下一个匹配,B与B匹配,此时j++..以此类推..

图片.png

此时我们的回溯是在部分匹配表里进行的,而不是从Str1 进行回溯,根据图解你能看出小明同学的思路与小王同学的思路区别么?

阅读 205

我所知道系列:数据结构与算法
怕什么真理无穷进一寸有一寸的欢喜

心有多大,舞台就有多大

79 声望
12 粉丝
0 条评论

心有多大,舞台就有多大

79 声望
12 粉丝
宣传栏