KMP算法快速理解

目录

    • 一、最长公共子缀
    • 二、next数组
    • 三、基于next数组的文本匹配
    • 四、KMP优化原理
    • 五、求next数组代码
    • 六、KMP整体代码

一、最长公共子缀

公共子缀分为前缀和后缀
前缀:总是包含第一个字符的子串(不包括父串本身)
后缀:总是包含最后一个字符的子串(不包括父串本身)
————————————————————————————
举个例子:
父串ABCDEAB
前缀:A、AB、ABC、ABCD、ABCDE、ABCDEA
后缀:B、AB、EAB、DEAB、CDEAB、BCDEAB
最长公共子缀就是指完全相同的最长前缀和后缀
在上述的例子中最长公共子缀是AB,长度为2。
如下图所示,最长公共子缀就是看前、后有多少完全相同的字符。
KMP算法快速理解_第1张图片

二、next数组

next数组的值其实就是【当前位置之前的字符串】的最长公共子缀长度
————————————————————————————
举个例子:
父串ABAABACA
当我们在计算C字符对应的next数组值时,我们其实就是要计算ABAABA这个字符串的最长公共子缀。
ABAABA的前缀有A、AB、ABA、ABAA、ABAAB
                  后缀有A、BA、ABA、AABA、BAABA
最长的公共子缀是ABA,长度为3,所以C这个位置的next数组值就是3。
由于第一个字符之前没有任何字符,所以我们规定第一个字符处的next数组值为-1;第二个字符之前只有1个字符,但是我们规定子缀是不能包含父串本身的,所以最长公共子缀为0;其他位置的按照标准计算即可。
根据定义,父串ABAABACA的next数组值为:

i 0 1 2 3 4 5 6 7
str[i] A B A A B A C A
next[i] -1 0 0 1 1 2 3 0

三、基于next数组的文本匹配

KMP算法解决的问题是在父串中查找子串是否存在。
————————————————————————————
我们举例来说明KMP的工作过程
父串txt=BBCABCDABEABCDABCDABDE
子串str=ABCDABD
(1)求出子串str的next数组

i 0 1 2 3 4 5 6
str[i] A B C D A B D
next[i] -1 0 0 0 0 1 2

(2)指针定位到父串的开头和子串的开头,匹配第一个字符
KMP算法快速理解_第2张图片
(3)子串第一个字符就不匹配,子串不动,父串移动,继续和子串第一个字符匹配
KMP算法快速理解_第3张图片
(4)子串第一个字符仍然不匹配,子串不动,父串移动,继续和子串第一个字符匹配
KMP算法快速理解_第4张图片
(5)子串第一个字符仍然不匹配,子串不动,父串移动,继续和子串第一个字符匹配
KMP算法快速理解_第5张图片
(6)子串第一个字符匹配了,则子串和父串同时向后移动,继续匹配,直到匹配到子、父串不相等(或者子串匹配完成,则查找结束)
KMP算法快速理解_第6张图片
(7)匹配到不相等的位置,该位置在子串中的下标是i,则父串不动,子串移动到next[i]的位置,比如图中位置D在子串中下标是6,next[6]=2,则子串移动2这个位置。
KMP算法快速理解_第7张图片
(8)继续比较当前位置的子串是否和父串相等,如果等则子串、父串一起后移;如果不等则重复上面的过程,也就是移动到next[i]这个位置
KMP算法快速理解_第8张图片
(9)左移完之后继续比较子串是否和父串相等,发现不等,这时发现子串已经回到了头部,再左移没有位置移动了,所以此时只移动父串,继续和子串第一个位置比较
KMP算法快速理解_第9张图片
(10)发现父串和子串第一个位置相同,则父串、子串一起向后移动,直到匹配到不相同的字符(或者子串匹配完成,则查找结束)
KMP算法快速理解_第10张图片
(11)发现了不相等的字符,则子串移动到next[i]的位置
KMP算法快速理解_第11张图片
(12)移动之后继续匹配,如果相等就父串、子串一起往后动;如果不等就子串移到next[i]的位置;如果已经回退到子串头部了,因为next[0]=-1,所以那就可以父串、子串一起移动一下就好
KMP算法快速理解_第12张图片
(13)子串已经匹配完了,查找结束。

所以KMP的基本工作原理就是:

  1. 若子串、父串相等,则同时往后移动
  2. 若子串、父串不等,则子串移动到next[i]这个位置
  3. 若i=-1时子串、父串同时往后移动

四、KMP优化原理

你应该发现,KMP的核心要义其实就是左移到next[i]这个部分了,那为什么这个部分可以帮助我们减少比较的工作量呢?
传统的匹配算法如下图:
KMP算法快速理解_第13张图片
在匹配到不相同的字符时,父串回退到刚才开始匹配位置的下一个位置,子串回退到头部,从头开始匹配。
我们明显觉得很麻烦,甚至要喊:你要退也退到一个跟子串开头字符相等的位置吧,干嘛非得一位一位动?

就比如下图,我们在父串E位置匹配失败了,如果要重新匹配,那肯定也是从刚才匹配过的字符中找一个跟子串头位置相等的位置,比如标红的A处开始重新匹配吧,那为什么移动next[i]就正好是这个位置呢?
KMP算法快速理解_第14张图片
回顾一下next数组的含义:next[i]指的是【i之前的字符串】的最长公共子缀长度。

next数值其实告诉了我们两个信息,一是子串从哪个位置开始和头部相等;二是从那个位置开始,到当前位置结束,一共有多少个相同的字符和头部是匹配的。
比如上图位置D,next[6]=2,就代表str[6-2]=str[4]这个位置是跟头部字符是相同的;而从str[4]这个位置开始,有2个字符和从头部开始有2个字符是相同的。也就是,从子串开头开始有X0X1两个字符和D前面两个字符X4X5相等,既然X0X1=X4X5,现在X6和父串不相等,那我们下一步就应该移到X3,看它和父串对应位置是否相等。
左移到next[i]可以让头部的A占据D之前的A的位置,让头部的B占据B之前的B的位置,然后开始匹配后面的数据。
KMP算法快速理解_第15张图片
通过这样的方法,我们可以节省大量的时间,做到父串不回退。

五、求next数组代码

next数组的原理上面已经讲了,现在说说怎么写next数组的求解代码。
首先能想到的一个暴力方法是,每一位在求解时,都把它之前的子串的前后子缀由长到短逐渐比较。
————————————————————————————
比如AAABBADA,我们在求解D所对应的next数组值时,将前面的子串AAABBA提出来:

  1. 先比较AAABB和AABBA
  2. 不等,则比较AAAB和ABBA
  3. 不等,则比较AAA和BBA
  4. 不等,则比较AA和BA
  5. 不等,则比较A和A
  6. 发现等,所以next数组值是1

想也知道这是一个多么繁琐的过程,所以我们势必要进行优化,我们再看一个例子:
————————————————————————————
比如要求下面数组的next数组内容,首先0和1的位置固定是-1和0不会变。
KMP算法快速理解_第16张图片
在这里插入图片描述
KMP算法快速理解_第17张图片
KMP算法快速理解_第18张图片
KMP算法快速理解_第19张图片
不知道你看出规律了没有!
每一项都正好比前一项多出了一条新的匹配出来,也就是说前一项的next值可以给后面一项提供参考。
比如说:
如果i=3时,计算出next[3]=1,也就是X0=X2,那在计算next[4]时,我们可以直接去匹配X1和X3,而不用再重新匹配X0和X2了,因为next[3]=1其实就已经告诉我们这两相等了,如果X1=X3,那next[4]=next[3]+1=2。
你会奇怪,计算next[4]时,不是应该先看[0-2]和[1-3]是否相等吗?为啥直接跳过了然后就去算第二种情况了?这是因为next[3]=1≠2,不等于2其实就告诉我们X0X1≠X1X2,所以对于next[4]来说就不用计算X0X1X2?X1X2X3了,因为肯定不等。
而且你会发现,当我们在计算i+1这一项的next数组值时,比i多出来的这一条对比项其实是看Xi?Xnext[i]的关系,比如i=4时next[i]=next[4]=2,那计算i+1,也就是next[5]时,我们就可以直接匹配X4?Xnext[4],也就是X4和X2,发现相等,所以next[5]=next[4]+1=3。
所以next计算的第一条规律就是:
在已经知道next[i]的情况下,如果要计算next[i+1],我们令m=next[i],那我们可以直接去匹配Xi?Xm的关系,如果相等,则next[i+1]=next[i]+1。

那问题来了,如果Xi和Xnext[i]不等怎么办?
我们先说结论:如果Xi≠Xnext[i],那就去比较Xi和Xnext[next[i]]
比如:假设next[9]=3,那计算next[10]时应该匹配看X9和X3是否相等,如果不等,则去匹配X9和Xnext[3]是否相等,如果不等,则继续匹配X9和Xnext[next[3]]是否相等,直到X9和X0都匹配过了,没得退了,说明next[10]=0。
KMP算法快速理解_第20张图片
如上图所示,我们假设next[i]=m,那就表示从0开始有m个字符和从i-1开始往前数m个字符是相同的,也就是X0X1…Xm-1=Xi–mXi-m+1…Xi-1
那在计算next[i+1]时,应该先匹配Xi和Xm,看是否相等,如果不等,如下图所示,那我们就应该缩减红色框的范围,看更小的范围内能不能找到一个位置k,在k这个位置X0X1…Xk-1=Xi–kXi-k+1…Xi-1,那这样我们就可以直接匹配Xk和Xi,看是否相等,如果相等那就说明next[i+1]=next[k]+1。
KMP算法快速理解_第21张图片
那为什么这个位置k=next[m],而不是直接m-1呢?
如下图所示,我们需要找一个位置k,让【从0开始数k个字符】和【从i-1往前k个字符】相等,也就是X0X1…Xk-1=Xi–kXi-k+1…Xi-1,那因为next[i]=m,所以根据对称原则,从【m-1往前数k个字符】应该和【从i-1往前数k个字符】是一模一样的,也就是说【从0开始数k个字符】和从【m-1往前数k个字符】应该是一模一样的,那这个k不就是next[m]吗?!
KMP算法快速理解_第22张图片
所以next计算的第二条规律就是:
在已知next[i]的前提下要计算next[i+1],我们令m=next[i],那我们可以直接去匹配Xi?Xm的关系,如果不等,则我们令m=next[m],接着去匹配Xi?Xm的关系,以此类推直到两个字符相等,这时我们知道next[i+1]=m+1;或者m已经退到头部了还不等,这时m=next[0]=-1,next[i+1]=m+1=0正好。

写成代码就是:

 public void calNext(int[] next,String str){
        next[0]=-1;
        if(next.length==1){
            return;
        }
        next[1]=0;
        int i=2;
        int m=next[i-1];
        while(i<str.length()){
            if(m==-1 || str.charAt(i-1)==str.charAt(m)){
                next[i]=m+1;
                m++;
                i++;
            }else{
                m=next[m];
            }
        }
    }

六、KMP整体代码

有了求next数组的代码,KMP的代码就简单多了,我们直接按照上面的标准来写:
所以KMP的基本工作原理就是:

  1. 若子串、父串相等,则同时往后移动
  2. 若子串、父串不等,则子串移动到next[i]这个位置
  3. 若i=-1时子串、父串同时往后移动
public int KMP(String txt,String str){
        if(null==txt || null==str){
            return -1;
        }
        if(str.equals("")){
            return 0;
        }
        int[] next=new int[str.length()];
        calNext(next,str);
        int i=0,j=0;
        while(i<str.length() && j<txt.length()){
            if(i==-1 || str.charAt(i)==txt.charAt(j)){
                i++;
                j++;
            }else{
                i=next[i];
            }
        }
        if(i==str.length()){
            return j-i;
        }else{
            return -1;
        }
    }

你可能感兴趣的:(算法)