字符串匹配:BF算法 | KMP算法 | Z函数

什么是字符串匹配?

给你一个字符串str,问你这个字符串中是否包含字符串sub。例如:str="abcdef",sub="cdef",问str中是不是有sub。

一.BF算法

BF算法(Brute Force),翻译成中文就是暴力匹配算法。

暴力匹配其实很好想,不就让我们判断str中有没有sub嘛,直接一个一个来。定义两个指针,一个指str,一个指sub。如果两个字符相同,继续匹配下一个;不同,sub的指针从头再来,str的指针指到开始位置的下一个(因为开始位置已经匹配不上了,肯定要从下一个开始啦)。

boolean BruteForce(String str,String sub){
    //定义两个指针,cur1指向str,cur2指向sub
    int cur1=0,cur2=0;
    int n=str.length();
    int m=sub.length();
    while(cur1<n && cur2<m){
        if(str.charAt(cur1)==sub.charAt(cur2)){
            cur1++;
            cur2++;
        }else{
            cur1=cur1-cur2+1;
            cur2=0;
        }
    }
    if(cur2==m){
        return true;
    }else{
        return false;
    }
}

我给出的代码只是一个思路,实际使用根据情况来。比如我们实际需要能跟str匹配上sub的起始下标,那方法返回的就是整形就行。

BF算法很简单,而且它不是本文的重点,所有就不过多赘述了。

二.KMP算法

KMP算法是本文的重中之重。关于KMP算法的由来,历史什么的这里就不说了,感兴趣的可以自己查一下。

我们先前使用的BF算法匹配,这个算法有个致命的缺点,那就是每次没匹配上,都要从头再来,很烦,时间复杂度很高(O(n*m))。

我之前写过一篇文章介绍过马拉车(Manacher)算法,马拉车(Manacher)算法就是重复利用了回文串的特性来优化时间复杂度的。其实这个的思路也是一样的,我们使用BF算法的时候,没有很好的利用字符串告诉我们的信息。

要想理解KMP算法,有个很重要的东西要先介绍一下,部分匹配表(Partial Match Table)。其实大家不理解KMP算法的主要原因就是这个部分匹配表。这个表中记录了什么数据这里先不管,我先让大家明白这里的数据是怎么来的。

这里其实有点对称意思,什么意思呢,看这个图:

我们在匹配到了a的时候发现,完了,sub是f,匹配失败了。这里我们要充分利用字符串的信息,我们看f前面是不是有个abc,注意啊,str上面也有一个abc,这是跟f前面abc一一对应的(已经匹配上的)。如果说sub的开头也是这个话,那是不是我们就不用让str从开始位置的下一个开始了呢,直接从这个位置开始(也就是图上箭头指向a的位置)。

为什么?我们画线部分的内容都是匹配好的,都是一一对应的 --> sub的f前有一个abc,str的a前也有一个abc --> sub的开头也是abc --> str的a前的abc直接就与sub开头的abc匹配好了:

我们下次匹配str直接从a开始,sub直接从e开始,大大提高了效率。

有人可能会问,这不凑巧了吗?恰好sub的开头是sub。对!要的就是这个“巧”,要的就是凑巧。

那我们怎么去知道有没有“凑巧”这种情况呢?下面先引出前缀和后缀的概念。

先举个例子:abcde,它的前缀:a,ab,abc,abcd,它的后缀:e,de,cde,bcde。很好理解吧,注意这里的前缀和后缀没有包含其本身即abcde。

说这个有什么用呢?其实我们的部分匹配表就是通过这个求出的。部分匹配值就是前缀和后缀的最长的共有元素的长度。

举个例子:abcdabc,它的前缀:a,ab,abc,abcd...,它的后缀:c,bc,abc,dabc...。从中我们可以得到最长的共有元素的长度为3,即abc。

如果我们用上面图的例子,可以得到sub的部分匹配表值如下:

回到上面我说的这里有点对称的意思,可以前缀和后缀的最长的共有元素的长度,不就是对称的分布在字符串的头和尾,不过这个对称可能不是严谨的对称,只是我们对这个现象的简单总结。

那部分匹配表有什么用呢?它可以求出我们sub应该后退到的位置。比如上面的c下面的3,意味着在f这里不匹配,我们可以回退到e的位置。

为什么在实际写代码的时候会定义一个next数组来存储部分匹配表。

//next数组的求取方法
public int[] getNext(String sub){
    int m=sub.length();
    int[] next = new int[m];

    for(int i = 1 , j = 0; i < m ; i ++ ){
        while(j > 0 && sub.charAt(i) != sub.charAt(j)) {
            j = next[j - 1];
        }

        if(sub.charAt(i) == sub.charAt(j)) {
            j++;
        }

        next[i] = j;
    }
    return next;
}

有了next数组后相当于我们知道了当不匹配时,sub的指针j回退到什么位置。思路与上面的BF算法的思路相同,但是这里我们不用回退str的指针i,同时,sub的指针j不用回退到0的位置。

//匹配str中的sub,把匹配成功的下标返回
public List<Integer> KMP(String str,String sub){
    int n=str.length();
    int m=sub.length();
    List<Integer> result=new ArrayList<>();
    int[] next = getNext(sub);

    for(int i = 0 ,j = 0 ; i < n ; i ++ ){
        while(j > 0 && str.charAt(i) != sub.charAt(j)) {
            j = next[j - 1];
        }
        //相等就++,往后找
        if(str.charAt(i) == sub.charAt(j)) {
            j++;
        }

        if(j == m){
            //sub遍历完,记录下标,同时回退sub
            result.add(i - m + 1);
            j = next[j-1];
        }
    }
    return result;
}

三.Z函数(扩展KMP)

Z函数,又叫扩展KMP,但其实跟KMP没有任何关系。Z函数也是用来处理字符串匹配问题的方法。我们定义一个z数组,z[i]表示从 s[i] 到 s[n−1] 的子串与整个字符串s的最长公共前缀的长度。

举个例子,abeabc:

当我们求下标为3的z(图上箭头指向的位置)时,我们用红色画线与蓝色画线(整个字符串)进行匹配,最长公共前缀是ab,长度是2,所以z[3]=2。

public int[] getZ(String s) {
    int n=s.length();
    int[] z=new int[n];
    int l = 0, r = 0;
    for (int i = 1; i < n; i++) {
        if (i <= r) {
            z[i] = Math.min(z[i - l], r - i + 1);
        }
        while (i + z[i] < n && s.charAt(z[i]) == s.charAt(i + z[i])) {
            l = i;
            r = i + z[i];
            z[i]++;
        }
    }
    return z;
}

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值