一、暴力匹配算法
1.1 算法介绍
字符串暴力匹配算法(Brute Force Algorithm),又称为朴素的字符串匹配算法(Naive String Matching),在计算机科学中指的是一种在较长的文本串中查找子串的方法。
-
工作原理:它顺序地将子串与文本中的每一个字符进行匹配,直到找到完全匹配的子串或遍历完整个文本。
-
优点:易于实现和理解。
-
缺点:匹配效率不高,特别是在长文本和子串的情况下(会有大量的回溯,每次只移动一位,若是不匹配,移动到下一位接着判断,浪费了大量的时间)。
-
时间复杂度:此算法的时间复杂度为
O(nm)
,但是在一些实际应用场景中,可以通过优化匹配规则,降低运行时间。 -
补充说明:现代计算机科学中已有更高效的字符串匹配算法,如 Knuth-Morris-Pratt 算法( KMP算法 )和 Boyer-Moore 算法等。
1.2 算法步骤
- 首先,设定两个字符串:一个较长的文本串S(长度为n),一个要匹配的子串P(长度为m);
- 用指针 i 遍历文本串S,从0开始,设定指针 j 指向子串P的第一个字符;
- 比较S的第 i 个字符和T的第 j 个字符,如果它们相等,则将 i 和 j 同时加1,重复此步骤。如果不相等,将 i 回溯到上次匹配的下一位置,并将 j 重置为0;
- 当 j 等于子串P的长度m时,找到了完全匹配的子串,即第 i-m 个字符开始;
- 如果 i 遍历到文本串末尾,则匹配结束。
1.3 应用案例
假设有一个文本字符串"BBC ABCDAB ABCDABCDABDE",和一个模式子串"ABCDABD",现在需要查找模式子串在文本字符串中的位置(若存在该模式子串,就返回第一次出现的位置;若没有,则返回-1)。
- 匹配过程-示意图:
- 代码示例:
public class ViolenceMatchDemo {
public static void main(String[] args) {
// 测试一:存在的模式子串。
System.out.println(violenceMatch("BBC ABCDAB ABCDABCDABDE", "ABCDABD"));
// 15
// 测试二:不存在的模式子串。
System.out.println(violenceMatch("BBC ABCDAB ABCDABCDABDE", "X"));
// -1
}
/**
* 字符串暴力匹配。
*
* @param text 文本串
* @param pattern 模式子串
* @return int 找到则返回第一次出现的索引,否则返回 -1
*/
public static int violenceMatch(String text, String pattern) {
int tLen = text.length();
int pLen = pattern.length();
// 辅助指针:i指向文本串;j指向模式子串。
int i = 0;
int j = 0;
// 避免扫描越界。
while (i < tLen && j < pLen) {
// 匹配成功,指针同时顺序后移。
if (text.charAt(i) == pattern.charAt(j)) {
i++;
j++;
// 匹配失败,i回溯到上次匹配的下一位置,j重置为0。
} else {
i = i - (j - 1);
j = 0;
}
}
// 判断是否找到了完全匹配的模式子串。
if (j == pLen) {
return i - j;
} else {
return -1;
}
}
}
二、KMP匹配算法
那有没有一种算法,让指针i不往回退,只需要移动指针j即可呢?
2.1 算法介绍
Knuth-Morris-Pratt 字符串查找算法,简称为 “KMP算法”,常用于在一个文本串S内查找一个模式串P 的出现位置,这个算法由Donald Knuth、James H. Morris、Vaughan Pratt 三人于1977年联合发表,故取这3人的姓氏命名此算法。
- KMP算法的核心是利用匹配失败后的信息,尽量减少模式串与主串的匹配次数以达到快速匹配的目的(具体实现就是通过一个
next()
函数实现,函数本身包含了模式串的局部匹配信息)。 - 时间复杂度:如果文本串的长度为n,模式串的长度为m,那么匹配过程的时间复杂度为
O(n)
,算上计算next的O(m)时间,KMP的整体时间复杂度为O(m + n)
。
2.2 算法步骤
-
假设现在文本串S匹配到 i 位置,模式串P匹配到 j 位置:
- 如果j = -1,或者当前字符匹配成功(即S[i] == P[j]),则令指针同时加1,继续匹配下一个字符;
- 如果j != -1,且当前字符匹配失败(即S[i] != P[j]),则令 i 不变,j = next[j]。此举意味着失配时,模式串P相对于文本串S向右移动了j - next [j] 位。(换言之,当匹配失败时,模式串向右移动的位数为:失配字符所在位置 - 失配字符对应的next 值,即移动的实际位数为:j - next[j],且此值大于等于1)
-
示意图:
2.3 部分匹配表
-
“部分匹配值”就是”前缀”和”后缀”的最长的共有元素的长度。以”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。
-
”部分匹配”的实质是有时候字符串头部和尾部会有重复。比如,”ABCDAB”之中有两个”AB”,那么它的”部分匹配值”就是 2(”AB”的长度)。搜索词移动的时候,第一个”AB”向后移动 4 位(字符串长度- 部分匹配值),就可以来到第二个”AB”的位置。
2.4 应用案例
需求:通过KMP算法完成文本字符串"BBC ABCDAB ABCDABCDABDE",和一个模式子串"ABCDABD"的匹配。
- 代码示例:
public class KMPAlgorithmDemo {
public static void main(String[] args) {
// 文本串。
String text = "BBC ABCDAB ABCDABCDABDE";
// 模式子串。
String pattern = "ABCDABD";
// 模式子串的部分匹配值表。
int[] next = kmpNext(pattern);
System.out.println("next=" + Arrays.toString(next));
// next=[0, 0, 0, 0, 1, 2, 0]
int index = kmpSearch(text, pattern, next);
System.out.println("index=" + index);
// index=15
}
/**
* kmp(改良暴力匹配算法)。
*
* @param text 文本串
* @param pattern 模式子串
* @param next 模式子串对应的部分匹配表
* @return int 找到则返回第一次出现的索引,否则返回 -1
*/
public static int kmpSearch(String text, String pattern, int[] next) {
int tLen = text.length();
int pLen = pattern.length();
for (int i = 0, j = 0; i < tLen; i++) {
// 字符匹配失败时,调整指针j的位置。
while (j > 0 && text.charAt(i) != pattern.charAt(j)) {
j = next[j - 1];
}
// 字符匹配成功,指针j后移一位。
if (text.charAt(i) == pattern.charAt(j)) {
j++;
}
// 找到了。
if (j == pLen) {
return i - j + 1;
}
}
return -1;
}
/**
* 获取到一个模式子串的部分匹配值表。
*
* @param pattern 模式子串
* @return {@link int[]} 对应的部分匹配值
*/
public static int[] kmpNext(String pattern) {
// 部分匹配表。
int[] next = new int[pattern.length()];
// 如果字符串是长度为1,部分匹配值就是0。
next[0] = 0;
for (int i = 1, j = 0; i < pattern.length(); i++) {
// 从next[j-1]获取新的j。
while (j > 0 && pattern.charAt(i) != pattern.charAt(j)) {
j = next[j - 1];
}
// 满足时,部分匹配值就是+1。
if (pattern.charAt(i) == pattern.charAt(j)) {
j++;
}
next[i] = j;
}
return next;
}
}
三、结束语
“-------怕什么真理无穷,进一寸有一寸的欢喜。”
微信公众号搜索:饺子泡牛奶。