1. 背景

项目中经常用到字符串模糊匹配,这里就用到了字符串的匹配算法,
例如,我们有字符串A=“abcabcdhijk”,B=“abce”,求字符串B在字符串A中的位置,这种子串的定位操作通常称作串的匹配模式。我们把字符串A称为主串,子串B称为模式串。

2. 朴素模式匹配算法

假如让我们求上面那个例子中,字串B在主串A中是否存在,若存在,求在主串A中的什么位置

2.1 图片分析

我们先从第一个字符去匹配,如果相同则继续匹配下一个字符

hive 字符串相似 hive字符串模糊匹配_java


如果遇到不匹配的,就像下图中的字符A与字符E

hive 字符串相似 hive字符串模糊匹配_字符串_02


此时让下标i移到1位置,模式串下标j从0位置开始,继续去匹配,重复上面两个步骤,直到完全匹配为止。

hive 字符串相似 hive字符串模糊匹配_hive 字符串相似_03

2.2 代码实现

public static int getIndex(String s,String t) {
        char[] array1=s.toCharArray();
        char[] array2=t.toCharArray();
        int i=0;
        int j=0;
        //判断字符串长度
        while(i<s.length()&&j<t.length()) { 
            //从第i个下标比
            if(array1[i]==array2[j]) {
                i++;
                j++;
            //如果不一样的话,就从上次开始比较位置的后一位开始比,并且子串从0开始    
            }else { //goodgoogle  google
                i=i-j+1;
                j=0;
            }

        } 
        //判断j是否等于子串的长度,相等即返回在主串中的位置
        if(j>=array2.length) {
            return i-array2.length;
        }
        return -1;
    }

上面的这种实现方式就是朴素模式匹配算法,比较简单粗暴,但是这需要我们去挨个遍历主串A,而通过图片我们可以知道:

在我们第一次匹配到不同字符AE时,第二次匹配时,我们不需要从主串下标i=1位置开始,因为模式串B中下标为j=1、j=2的字符与主串A相匹配,且模式串中第一个字符A不与自己后面的子串中的任一字符相等(很重要的前提,可以预处理先比较自身),所以再次比较的位置应该是下图所示,这就省去了很多步骤。

hive 字符串相似 hive字符串模糊匹配_算法_04


实际上我们保持主串A不动,仅对模式串B进行移动匹配即可,那我们如何去定位模式串B的下标位置呢,这就用到了KMP算法。

3. KMP算法

3.1 KMP算法简介

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

3.2 KMP算法实现原理

KMP算法提高效率其实就是确定这次匹配失败后,下次不用再重复的回溯到前面去匹配我们已经知道不可能相同的位置,而是直接跳到某个位置接着进行匹配。KMP算法通过一个“有用信息“,这个“有用信息”就是用所谓的“前缀函数(很多书中提到的的next函数)”来存储的。这个函数能够反映出现失配情况时,系统应该跳过多少无用字符而进行下一次检测。

通过上面分析我们知道,KMP算法最重要的两个难点:

一是这个前缀函数的求法。
二是在得到前缀函数之后,怎么运用这个函数所反映的有效信息跳过不必要的判断。

首先我们得明白”前缀”和”后缀”的概念。 “前缀”指除了最后一个字符以外,一个字符串的全部头部组合;”后缀”指除了第一个字符以外,一个字符串的全部尾部组合。例如”ABCD”的前缀为[A, AB, ABC],后缀为[BCD, CD, D].

我们前面讲过需要对子串自身进行预处理,这样就可以省去很多比较步骤,我们把处理结果保存在一个Next[]数组中。next[i]表示的就是前i个字符组成的这个子串最长的相同前缀后缀的长度。例如字符串aababaaba的相同前缀后缀有a和aaba,那么其中最长的就是aaba,即为4。

那么我们就可以得到子串的Next数组。以“ABCDABD”为例

"A"的前缀和后缀都为空集,共有元素的长度为0;
   
   "AB"的前缀为[A],后缀为[B],共有元素的长度为0;
   
   "ABC"的前缀为[A, AB],后缀为[BC, C],共有元素的长度0;
   
   "ABCD"的前缀为[A, AB, ABC],后缀为[BCD, CD, D],共有元素的长度为0;
   
   "ABCDA"的前缀为[A, AB, ABC, ABCD],后缀为[BCDA, CDA, DA, A],共有元素为"A",长度为1;
   
   "ABCDAB"的前缀为[A, AB, ABC, ABCD, ABCDA],后缀为[BCDAB, CDAB, DAB, AB,
   B],共有元素为"AB",长度为2;
   
   "ABCDABD"的前缀为[A, AB, ABC, ABCD, ABCDA, ABCDAB],后缀为[BCDABD, CDABD,
   DABD, ABD, BD, D],共有元素的长度为0。 
   
   所以next[]="0000120"

举个例子:例如主串S=”abcabcdabcdabx”,T=”abcdabx” ,我们先算出T的next数组,next[]=”0000120”

前缀函数next[]我们已经知道了,那接下来我们看如何根据前缀函数来匹配字符串。

第一步:我们先从0开始匹配,匹配到子串第3位(下标为0开始)不匹配,则计算失配的前一个位置的next值,即next[3-1]=0
故接下来让子串的0位置和主串的当前位置对齐比较,重点:主串是不变的,因为当前主串的位置是3,所以让子串从0开始和主串当前位置对齐然后比较

1	abcabcdabcdabx
2	   abcdabx

第二步:接着匹配,匹配到第子串第6位又不匹配,next[6-1]=2,这时我们应该让子串2位置和主串当前位置对齐,然后从2开始比较

1	abcabcdabcdabx
2	       abcdabx

接下来完全匹配,返回位置。

3.3 代码实现

/*
	* 测试
	*/
	public static void main(String[] args){
        SpringApplication.run(KMPString.class,args);
        String tStr = "ababcxvdababcdert";
        String pStr = "ababc";
        int[] next = getNext(pStr);
        int index = getIndex(tStr, pStr, next);
        System.out.println(index);
    }

	/*
	* 获取匹配的下标值,若不匹配则返回-1
	*/
    private static int getIndex(String t, String p, int[] next) {
        char[] tChars = t.toCharArray();
        char[] pChars = p.toCharArray();
        int tLength = t.length();
        int plength = p.length();
        int tIndex, pIndex = 0;
        if(tLength >= plength){
            for(tIndex = 0; tIndex < tLength; tIndex++){
                if(tChars[tIndex] != pChars[pIndex] && pIndex > 0){
                    pIndex = next[pIndex - 1];
                }else if(tChars[tIndex] == pChars[pIndex]){
                    pIndex++;
                }
                if(pIndex == plength){
                    return tIndex-pIndex+1;
                }
            }
        }
        return -1;
    }
	/**
	* 前缀函数NEXT获取
	*/
    private static int[] getNext(String s){
        char[] array = s.toCharArray();
        int index, value=0;
        int[] kmpArr = new int[array.length];
        kmpArr[0] = 0;
        for(index=1; index<array.length; index++){
            if(array[index] != array[value] && value>0){
                kmpArr[index] = 0;
                value = 0;
            }else if(array[index] == array[value]){
                kmpArr[index] = ++value;
            }else{
                kmpArr[index] = 0;
            }
        }
        return kmpArr;
    }

总的来说KMP算法还是比较难的,在学习的时候用实例去辅助,能够加深理解,文章中有错误的地方还望指正!