子字符串查找之KMP

目录

小引——暴力查找

模式ABABAC
字符串文本ABABAABABAC

当我们需要从文档中查找某个关键词时,就用到了子字符串查找技术。比如在某个数据库导出文档中想要查找所有用户的密码,想在一个学长给的word题库中查找你正在做的检测题的答案。就像上边这个表格,我们想要在字符串文本中查找模式所在位置,并返回这个位置给用户。这个功能是怎么实现的呢?
我们可以简单暴力的来实现,从头开始一个字符一个字符的比较字符串文本和模式,如果匹配失败,再从字符串文本的下一个位置开始跟模式从头比较,重复这个过程,如果成功,则返回模式在字符串中的起始位置。

public class ForceSearch {
    public int search(String txt,String pat){
        int N = txt.length();
        int M = pat.length();
        for (int i=0;i<=N-M;i++){
            int j;
            for (j=0;j<M;j++){
                if (txt.charAt(i+j)!=pat.charAt(j))break;
            }
            if (j == M)return i;
        }
        return N;
    }
}

按文章开头的字符串举例,模式是ABABAC,字符串文本是ABABAABABAC。当我们匹配到第5个字符的时候,模式的第5个字符是C,字符串文本的第5个字符是A,发现匹配失败。按照暴力算法的逻辑,我们需要将模式向右移一位,也就是模式的第0个字符和字符串文本的第1个字符对齐开始一个一个比对。在匹配失败之前,我们已经比较到了文本的第5个字符,而失败之后我们不得已回退到第1个开始比对。但此时我们可以发现,回退之后,又发生了一次失败,于是再次回退,当经过五次回退之后,也就是当字符串文本的第5个字符跟模式的第0个字符对齐比较之后,才匹配成功。

也就是说我们这前4次回退都是徒劳。也许就有人说了,没有前4次回退的失败,我们怎么会知道第5次回退的成功呢?答案是:可以知道。这就是KMP(Knuth-Morris-Pratt这三个人发明了这个算法,于是取他们名字的首字母作为算法的名字)。

KMP

KMP把关注点放在了模式本身上,如上边这个例子,当我们比较到第5个字母的时候,发生了匹配失败,但无可置疑的是前5个字符是匹配的。也就是说字符串文本的前5个字符和模式的前5个字符是一样的,当我们回退进行重新比较时,其实就是模式和模式本身的某段字符串进行比较。也就是说,回退到匹配成功那部分字符串进行的比较,我们只需要模式自己就可以完成。对于文本字符串并不需要任何回退,通过模式自身的信息,我们可以得出,字符串文本的第5个字符应该跟模式的第几个字符串进行比较。从而字符串和模式两者的回退,成为了模式本身自己进行的回退。每当出现匹配失败的情况,我们就可以根据模式自己的信息计算出和匹配失败的字符进行再次匹配的字符在模式中的相应位置

现在唯一的问题就是这个位置是怎么计算出来的,《算法4》中引入了这么个概念——确定有限状态自动机(DFA)。为了方便说明,我们用i来指示字符串文本中字符的位置,j来指示模式中的字符位置。如图
《子字符串查找之KMP》
确定有限状态自动机我们就称它为自动机吧,它的本质就是个二维数组,行指示的是某种字符,比如我们这个例子中有三种字符(A,B,C),于是这个二维数组就有三行;列指示的是模式中字符的位置,这个例子中模式有6个字符,于是这个二维数组就有6列。每个元素的值就是我们上边提到的位置。比如说A行3列存的值X,就是当我们模式中的第3个位置的字符和字符串文本中的第i字符匹配失败后,就应该让字符串文本中的第i+1个字符和模式中的第X个字符进行比较。

刚才的难题是位置如何计算,现在又说位置在自动机中存着。那下一个问题就来了,自动机是怎么构建出来的?先上代码(来自《算法4》)

        dfa[pat.charAt(0)][0] = 1;
        for (int X = 0,j=1;j<M;j++){
            for (int c=0;c<R;c++){
                dfa[c][j]=dfa[c][X];
            }
            dfa[pat.charAt(j)][j] = j+1;//A
            X = dfa[pat.charAt(j)][X];//B
        }

dfa这个数组就是自动机,我们首先初始化了模式第一个字符所在行的0列元素为1,也就是说当字符串文本传来的比较字符跟模式第一个字符相等的时候,下一个比较的就是模式中位置为1的元素(第二个元素)。然后进入for循环,这个for循环初始化X=0,j=1,并且会循环M次(M是模式的长度),里边套了一个内循环,内循环会循环R次,R对应这我们例子中的3(A,B,C,3种字符)。这个循环将所有行的第1列位置元素全部初始化成了dfa[c][X],可以发现对应不同的c,这个值是不同的,而且此时的X等于0,相当于将第0列的元素值对应的移到了第1列。下一句也就是A行更新了第1列中pat.charAt(j)这个字符所在行的元素为j+1,也就是说匹配成功的这个情况,会跳到下一个元素进行比较,而匹配失败会跳到对应的dfa[c][X]位置进行比较。此时的X=0,然后进行下一行也就是B行,会进行X的更行,X就是一个重启的状态记录,X更新为dfa[pat.charAt(j)][X],至于X为什么要更新到这个值,这是一个递归的思想。还以文章开头的那个例子来说为了方便阅读,我把模式和字符串复制下来

模式ABABAC
字符串文本ABABAABABAC

当我们匹配到第5个字符的时候匹配失败,此刻我们需要找到重启的位置,也就是dfa[A][5],而这个值是在内for循环中更新的,内for循环会给这个元素赋值为dfa[A][X],而此时的X是上一个X,而上个X又是上上个X来构建的,最终初始的X=0。这个递归是最难理解的部分,但也是自动机十分精巧的部分。除了自动机还有一个办法就是构建next数组,总之也是指示匹配失败的情况应该将j置为何值。为了大家能更好的理解这个算法,下面给出几个参考链接,可以博采众长。

KMP算法中关于构造DFA部分的纠结:https://blog.csdn.net/albert0420/article/details/45075817
Sedgewick的算法4中的KMP算法有不少细节我看不懂,请大家帮我下?:https://www.zhihu.com/question/24382751
next数组版本:https://blog.csdn.net/v_july_v/article/details/7041827

    原文作者:KMP算法
    原文地址: https://blog.csdn.net/qq_33240946/article/details/82773275
    本文转自网络文章,转载此文章仅为分享知识,如有侵权,请联系博主进行删除。
点赞