算法 字符串匹配算法

时间:2022-03-03 10:40:02

子串的定位操作通常称为串的模式匹配。

1. 朴素的模式匹配算法Native String Matching Algorithm

朴素的模式匹配算法又称为暴力匹配算法(Brute Force Algorithm),采用遍历的方式进行匹配,滑动窗口总是1,会产生很多重复的比较,容易理解,但效率低。

  • 算法思想
    从目标串的的第一个字符起与模式串的第一个字符比较,若相等,则继续对字符进行后续的比较,否则目标串从第二个字符起与模式串的第一个字符重新比较,直至模式串中的每个字符依次和目标串中的一个连续的字符序列相等为止,此时称为匹配成功,否则匹配失败。

  • 算法复杂度分析
    最好的情况:第一次查找就找到了,O(1),比如在”abc”中查找”ab”;稍差一些,在”abc”中寻找”bc”,此时为O(n + m),n为主串长度,m为子串长度。
    最坏情况:就是每次匹配不成功发生在最后一个字符。即在”abaabbabc”中寻找”abc”,此时为O((n - m + 1) * m) 。
    显然效率很低。

  • 实现代码

class NativeStringMatcher {

static int nativeStringMatching(String s, String t) {
if (s == null || t == null) return -1;

int i, j, sLength = s.length(), jLength = t.length();

for (i = 0; i < sLength; ++i) { //滑动窗口始终为1

j = 0;

while (j < jLength) {
if (i + j < sLength && s.charAt(i + j) == t.charAt(j)) {
++j;//最后一个相等时,j会多加1
} else {
break;// 不相等时,i + 1后重新开始。
}
}

if (j == jLength) { //完全匹配
return i;
}

}

return -1;
}

static int nativeStringMatching2(String s, String t) {
if (s == null || t == null) return -1;

int i = 0, j = 0, sLength = s.length(), jLength = t.length();

while (i < sLength && j < jLength) {
if (s.charAt(i) == t.charAt(j)) {
++i;
++j;
} else {
i = i - j + 1;
j = 0;
}
}

if (j == jLength) {
return i - j;
}

return -1;
}
}

2. KMP模式匹配算法

KMP算法是由D.E.Knuth、J.H.Morris和V.R.Pratt共同提出的一个改进算法,消除了朴素的模式匹配算法中回溯问题,完成串的模式匹配。

  • 算法思想
    KMP 算法的基本思路就是设法利用已知信息,不要把 “搜索位置” 移回已经比较过的位置,而是继续把它向后面移,这样就提高了匹配效率。
    即i值不回溯,只移动j的值,j值的多少取决于模式串的前后缀的相似度,即部分匹配表。其中优化的地方在于:移动位数 = 已匹配的字符数 - 对应的部分匹配值。而不是朴素匹配中的1。

  • 前缀、后缀
    前缀指除了最后一个字符以外,一个字符串的全部头部组合
    后缀指除了第一个字符以外,一个字符串的全部尾部组合

  • 部分匹配表(即next数组,获取新的j的值的表,非部分匹配值)
    对于模式串中的任何位置,我们都希望能够查询那个位置前(不包括那个位置)有可能的W的最长初始字段的长度,即j从几开始。
    而不是从W[0]开始失配的整个字段,这长度就是我们查找下一个匹配时j回退的距离。

  • 算法复杂度分析
    如果文本串的长度为n,模式串的长度为m,那么匹配过程的时间复杂度为O(n),算上计算next的O(m)时间,KMP的整体时间复杂度为O(m + n)

  • 代码实现

class KMPStringMatcher {

private static int[] next;

static int KMPStringMatcher(String s, String t) {
if (s == null || t == null) return -1;

int i = 0, j = 0, sLength = s.length(), jLength = t.length();

/*
1. 获取移动位数表
*/

int[] next = getNextVal(t);

/*
2. 遍历查找
*/

while (i < sLength && j < jLength) {
if (j == -1 || s.charAt(i) == t.charAt(j)) {
i++;
j++;
} else {
j = next[j];//i不回溯,j变化。
}
}

if (j == jLength) {
return i - j;
}

return -1;
}

// 获取模式串的移动位数
public static int[] getNext(String t) {
int jLength = t.length();
int[] next = new int[jLength];

int k = -1;
int j = 0;
next[0] = -1; // next数组中next[0]为-1
while (j < jLength - 1) {
if (k == -1 || t.charAt(j) == t.charAt(k)) {
k++;
j++;
next[j] = k;
} else {
k = next[k];
}
}

return next;
}

// 优化当p[j] == p[k]时候的next数组,会造成多次的重复判断,所以在构建next数组时就把它优化掉
public static int[] getNextVal(String t) {
int jLength = t.length();
int[] next = new int[jLength];

int k = -1;
int j = 0;
next[0] = -1; // next数组中next[0]为-1
while (j < jLength - 1) {
if (k == -1 || t.charAt(j) == t.charAt(k)) {
k++;
j++;

if (t.charAt(j) != t.charAt(k)) {
next[j] = k; //之前只有这一行
} else {
//因为不能出现p[j] = p[ next[j ]],所以当出现时需要继续递归,k = next[k] = next[next[k]]
next[j] = next[k];
}

} else {
k = next[k];
}
}

return next;
}
}

参考:
阮一峰:字符串匹配的KMP算法
从头到尾彻底理解KMP
克努斯-莫里斯-普拉特算法