字符串匹配算法主要是两类,最基本的暴力解法,也叫做朴素算法,另一种是KMP算法。本篇给出两种算法的最简单化的写法,便于面试时记忆和书写,当然重点还是理解其算法思想。
朴素匹配算法
被搜索的字符串称为主串,待搜索的字符串称为模式串。朴素模式匹配算法的基本思想:
对主串的每一个字符作为子串开头,与模式串进行匹配。对主串做大循环,每个字符开头做模式串长度的小循环,直到匹配成功或全部遍历完成为止。
Java 实现算法如下:
/** * 朴素匹配算法,找模式串t,在主串s中首次出现的位置 * * @param s 主串 * @param t 模式串 * @return 首次匹配位置 如果不存在返回-1 */
public int simple(String s, String t) {
char[] S = s.toCharArray();
char[] T = t.toCharArray();
int lenS = S.length;
int lenT = T.length;
int i, j;//i遍历S,j遍历T
for (i = 0; i <= lenS - lenT; i++) {
for (j = 0; j < lenT; j++)
if (S[i + j] != T[j])
break;
if (j == lenT)
return i;
}
return -1;
}
时间复杂度分析
朴素模式匹配算法时间复杂度分析如下:(n为主串长度,m为模式串长度)
情况 | 时间复杂度 | 备注 |
---|---|---|
最好情况 | O(1) | 一开始就匹配成功。 |
最坏情况 | O((n-m+1)*m) | 每次不成功的匹配都发生在模式串的最后一个字符。 |
平均情况 | O(n+m) | 根据等概率原则,平均是(n+m)/2次查找 |
朴素匹配算法的空间复杂度为O(1)。
KMP算法
KMP算法全称叫做Knuth-Morris-Pratt Algorithm。
被搜索的字符串称为主串,待搜索的字符串称为模式串。
我们知道朴素模式匹配算法,KMP算法从模式串出发,发现模式串中隐藏的信息来减少比较的次数。
KMP算法的关键在于next数组值的推导。
next数组求解
对于模式串 T和其对应的next数组,next[i]表示了在[0,i]区间构成的字符串的最长公共前后缀。这里其实运用了动态规划法的求解思路。
当i=0 next[i]=0 当T[i]=T[next[i-1]] ,next[i]=next[i-1]+1.关于这个递推关系确实不好理解, 举个例子,
假设T= “abcabc” 假如我们求出了next[4]=2,这个时候求next[5],
只有当前字符 等于第三个字符c时才能 得出next[5]=next[4]+1
也就是当T[5]==T[next[4]]时, next[5]=next[4]+1=3。如果让自己写这个递推关系,估计想破脑袋也难得出来,大神毕竟大神,作为凡人的我们还是尽量记住这个递推关系好了。
//求T串的next数组
//动态规划法 求[0,i]字符串的最长公共前后缀
public int[] getNext(String t) {
char[] T = t.toCharArray();
int len = T.length;
int[] next = new int[len];
//next[i]=k表示第[0,i]的字符串的最长公共前后缀长度为k
next[0] = 0;
// i 遍历[1,len-1]
for (int i = 1; i < len; i++) {
//这个条件记住
//"abcabc" next[4]=2 if(T[5]=T[next[4]]) next[5]=next[4]+1
if (T[i] == T[next[i - 1]]) {
next[i] = next[i - 1] + 1;
}
}
return next;
}
KMP 算法
以下是KMP算法的Java实现,和朴素算法相比,KMP高效在避免了主串中索引i的回溯,i是不断向前递进的。所以在主串S和模式串T存在大量部分匹配的情况下能够显著提高匹配效率。
//最精简的KMP
//平均时间复杂度为O(m+n) 空间复杂度为O(n)
public int kmp(String s, String t) {
char[] S = s.toCharArray();
char[] T = t.toCharArray();
int lenS = S.length;
int lenT = T.length;
int[] next = getNext(t);
print(t + "的next数组为", next);
//i不断往前递进1, j移动根据next数组
for (int i = 0, j = 0; i < lenS; ++i) {
while (j > 0 && S[i] != T[j])
//失去匹配时 j移动到next[j-1]值的位置
j = next[j - 1];
if (S[i] == T[j]) {
j++;
}
if (j == lenT) {
//返回索引
return i - j + 1;
}
}
return -1;
}
时间复杂度分析
KMP整个算法的时间复杂度为O(n+m),相较于朴素模式匹配算法的最坏情况的O((n-m+1)*m)来说,是要好一些。
空间复杂度为O(m),耗费了一个模式串长度的一维数组。
KMP算法只在主串和模式串存在很多部分匹配情况下效率提高的比较明显,因为减少了很多不必要的回溯,但是在其他情况下效率和朴素算法并没有太大差别。