Knuth-Morris-Pratt 字符串查找算法(常简称为“KMP算法”)可在一个主“文本字符串”s
内查找一个“词”m
的出现位置。此算法通过运用对这个词在不匹配时本身就包含足够的信息来确定下一个匹配将在哪里开始的发现,从而避免重新检查先前匹配的字符。
初始化变量
待查找的字符串:s
要匹配的字符串:m
普通的做法
字符串匹配算法最常规的思路是在s中一个字符一个字符的与m中的第一个字符比较,匹配了说明找到了源头(i)这个i接下来很可能会与m完全匹配的,再比较各自接下来一个字符,如果匹配不上,说明上次找的i不是我们想要的源头,接着怎么办?从i+1开始,看i+1是否有可能是这个源头.
普通做法的缺点
时间复杂度高,造成这个的原因是因为在匹配过程中,虽然没有完全匹配上.但是很可能已经匹配了一部分,已经匹配上了的这一小部分并没有被充分利用起来;
KMP算法
充分利用在匹配过程中,没有完全匹配上但是已经匹配上一部分的资源.
几个概念:
下面的概念都是针对一个字符串而言的,eg:一个字符串为abc
前缀:a,ab,包含首字符,但不包含末字符的字符串;
后缀:c,bc,包含末字符,但不包含首字符的字符串;
具体步骤
构造辅助数组
- 拿到m字符串,生成一个与m等长的整型数组next[]
- 初始化next[0] = -1,next[1] = 0
- 从2开始遍历m,next[i]的值就是 字符串 m[0~i-1] 的相同的最长前缀和最长后缀的长度
匹配过程
- 开始匹配,定义两个游标,si和mi,初始均为0
- 如果能匹配上,si++,mi++
- 如果匹配不上,注意此时的隐含条件是mi之前的已经匹配上了,我们想下次移动的时候不是把m移动到上次匹配s的起止位置之后的一个字符处,而是看m[0~i-1]中的前缀和后缀是不是有一样的地方,这样就可以重复利用,这不就是next数组的作用吗?因此查看此时的next数组,将mi = next[mi];接下来当然从si和mi继续匹配,看能否匹配了
- 如果next[mi] == -1,表明连m的第一个字符都匹配不上,那么只能si++;
源代码
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73
|
public class KMP { public static int getIndexOf(String s, String m) { if (s == null || m == null || m.length() < 1 || s.length() < m.length()) { return -1; } char[] ss = s.toCharArray(); char[] ms = m.toCharArray(); int si = 0; int mi = 0; int[] next = getNextArray(ms); while (si < ss.length && mi < ms.length) { if (ss[si] == ms[mi]) { si++; mi++; } else if (next[mi] == -1) { si++; } else { mi = next[mi]; } } return mi == ms.length ? si - mi : -1; }
public static int[] getNextArray(char[] ms) { if (ms.length == 1) { return new int[]{-1}; } int[] next = new int[ms.length]; next[0] = -1; next[1] = 0; int pos = 2; int cn = 0; while (pos < next.length) { if (ms[pos - 1] == ms[cn]) { next[pos++] = ++cn; } else if (cn > 0) { cn = 0; } else { next[pos++] = 0; } } return next; }
public static void main(String[] args) { String str = "abcabcababaccc"; String match = "ababa";
System.out.println(getIndexOf(str, match)); System.out.println(str.indexOf(match)); }
}
|
next[]数组深入分析
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
| 分析 match = "ababa" => next[5] next[i]的含义:match[0~i-1]组成的字符串,最长前缀(不包含最后一个字符)和最长后缀(不包含第一个字符)匹配上的长度; 初始化: next[0] = -1;显然match[0~0-1]无意义,令其等于-1,代表第一个字符都匹配不上,match直接右移一位 next[1] = 0;显然match[0~1-1]就是match[0],只有一个字符,没有前缀也没有后缀;所以next[1]=0; 接下来开始匹配了,match[2]这个位置前面已经有两个字符了,可能前两个字符相等,那么next[2]=1,不相等,next[2]=0 可以发现的是,第一次匹配一定是match[i-1]和match[0]匹配;接下来如果再能匹配,因为match[i-1]和match[0]已匹配,match[i]和match[1]也能匹配了
ababa的next数组; 0 1 2 3 4 a b a b a -1 0 0 1 2
接下来拿ababcccc与ababe来匹配,ababe的next数组为{-1,0,0,1,2},和上面的ababa是一样的; 一开始是可以匹配的,当si = mi = 4 时,匹配不上了,这个时候的隐含条件就是m中的[0~mi-1]是都能匹配上的 这时候找next[mi],next[4]=2,说明mi之前的字符串中前两个和最后两个可以匹配上,所以下次比的时候,si = 4,mi = 2,从m[3]字符处开始匹配;
|
本文作者:
Zachaxy
版权声明:
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。