1、基本概念
串是由零个或多个字符组成的有限序列,又名字符串。
1.1 串大小的比较
给定两个串
s=”
a0
a1
…
an
”
t=”
b0
b1
…
bm
”
当满足以下条件之一,s<t
(1)n<m 且
ai
=
bi
(i=0,1…n) 如s=”hap” ,t=”happy”
(2)存在某个值k<=min(m,n) 使得
ai
=
bi
(i=0,1…k-1) 且
ak
<
bk
如 happen < happy
2、串的匹配
主串S=”goodgoogle” , 子串T=”google”
2.1 朴素的匹配匹配算法
子串匹配主串,由子串的第一个字符与主串的第一个字符开始比较,若相同,比较下一个。
如果完全相同,匹配成功。
如果有一个不同,用子串的第一个字符与主串的第二个字符比较。如此循环。时间复杂度为O(m*n)
public static int match(String s, String t){
if(s==null || t==null || s.length()<=0 || t.length()<=0 || s.length()<t.length()){
System.out.println("输入参数不正确");
return -1;
}
int i=0,j=0;
while(i<s.length() && j<t.length()){
if(s.charAt(i) == t.charAt(j)){ //如果相同,比较下一个
i++;
j++;
} else { //如果有一个不同,i回溯到匹配前加1的位置
i = i-j+1;
j=0;
}
}
if(j == t.length()){//如果匹配的子串长度与子串相同,则完成匹配
return i-j;
}
return -1;
}
2.2 KMP算法
朴素匹配算法会一直回溯主串,导致很多不必要的比较。KMP算法就是保证主串不回溯,从而提高效率。
预处理时间复杂度为O(m),匹配时间复杂度为O(n)
2.2.1 KMP算法的简单理解
主串s=”BBC ABCDAB ABCDABCDABDE”
子串t=”ABCDABD”
2.2.2 子串的预处理
首先需要知道前缀和后缀的概念
前缀:除了最后一个字符以外,一个字符串的全部头部组合;
后缀:除了第一个字符以外,一个字符串的全部尾部组合。
预处理就是获取前缀和后缀的最长的共有元素的长度
子串处理的步骤
“A”的前缀和后缀都为空集,共有元素的长度为0;
“AB”的前缀为[A],后缀为[B],共有元素的长度为0;
“ABCD”的前缀为[A, AB, ABC],后缀为[BCD, CD, D],共有元素的长度为0;
“ABCDA”的前缀为[A, AB, ABC, ABCD],后缀为[BCDA, CDA, DA, A],共有元素为”A”,长度为1;
“ABCDABD”的前缀为[A, AB, ABC, ABCD, ABCDA, ABCDAB],后缀为[BCDABD, CDABD, DABD, ABD, BD, D],共有元素的长度为0。
如果将上述的元素组成数组 [0, 0, 0, 0, 1, 2, 0],成为next数组
Max即是共有元素的最大长度
2.2.3 匹配步骤
a、开始匹配
子串的第一个与主串比较,如果不同则使用主串的后一个与子串比较
b、匹配到部分相同
如图,主串的第4个元素与子串匹配相同,直到第11个不同。现在相同的部分为ABCDAB。通过上面对子串的预处理可以知道,子串有公共的元素为AB,长度为2
现在已知的条件:
主串与子串相同的部分为ABCDAB
子串有相同的部分AB
所以再次比较时 子串的AB不再与主串的ABCDAB比较 直接使用子串第一个AB后的字符C与主串第二个AB后的字符 空字符 比较。因为AB是已知相同的。这就是KMP算法的核心原理了
c、失配时再次比较
上一次子串的字符C和主串的空字符不匹配,则使用子串的第一个字符进行比较。如此循环,知道完成
步骤b 匹配到部分相同,子串C与主串空字符比较时,跳过的步骤为匹配元素的长度6 - 相同部分AB的长度2 = 4
public class Main {
public static int[] getNext(String sub) {
int[] next = new int[sub.length()];
next[0] = -1;
int k = -1;
int j = 0;
while (j < sub.length()-1) {
//k表示前缀,j表示后缀
if (k == -1 || sub.charAt(j)==sub.charAt(k)) {
++k;
++j;
next[j] = k;
} else {
k = next[k];
}
}
return next;
}
public static int indexOf(String src, String ptn) {
int i = 0, j = 0;
int sLen = src.length();
int pLen = ptn.length();
int[] next = getNext(ptn);
System.out.println(Arrays.toString(next));
while (i < sLen && j < pLen) {
// 如果j = -1,或者当前字符匹配成功(src.charAt(i)==ptn.charAt(j)),都让i++,j++
if (j==-1 || src.charAt(i)==ptn.charAt(j)) {
i++;
j++;
} else {
// 如果j!=-1且当前字符匹配失败,则令i不变,j=next[j],即让pattern模式串右移j-next[j]个单位
j = next[j];
}
}
if (j == pLen)
return i - j;
return -1;
}
public static void main(String[] args) {
System.out.println(""+indexOf("BBC ABCDAB ABCDABCDABDE","ABCDABD"));
}
}
2.2.4 Next 数组的优化
public static int[] getNext(String sub) {
int[] next = new int[sub.length()];
next[0] = -1;
int k = -1;
int j = 0;
while (j < sub.length()-1) {
//k表示前缀,j表示后缀
if (k == -1 || sub.charAt(j)==sub.charAt(k)) {
++k;
++j;
if(sub.charAt(j) != sub.charAt(k))
next[j] = k;
else//如果有与以前字符相同的字符,则使用以前字符的标记
next[j] = next[k];
} else {
k = next[k];
}
}
return next;
}
参考:
next数组的解释:http://www.cnblogs.com/fuck1/p/6059736.html
长篇大论:http://blog.csdn.net/v_july_v/article/details/7041827#
代码:http://www.jianshu.com/p/e2bd1ee482c3