串的模式匹配算法-KMP算法
简介
KMP算法是一种改进的字符串匹配算法,由D.E.Knuth,J.H.Morris和V.R.Pratt提出的,因此人们称它为克努特—莫里斯—普拉特操作(简称KMP算法)。KMP算法的核心是利用匹配失败后的信息,尽量减少模式串与主串的匹配次数以达到快速匹配的目的。具体实现就是通过一个next()函数实现,函数本身包含了模式串的局部匹配信息。
简单的模式匹配算法(暴力匹配算法)
假设现在我们面临这样一个问题:有一个文本串S,和一个模式串P,现在要查找P在S中的位置,如何查找呢?
思路
如果用暴力匹配的思路,并假设现在文本串S匹配到 i 位置,模式串P匹配到 j 位置,则有:
- 如果当前字符匹配成功(即S[i] == P[j]),则i++,j++,继续匹配下一个字符;
- 如果失配(即S[i]! = P[j]),令i = i - (j - 1),j = 0。相当于每次匹配失败时,i 回溯,j 被置为0。
时间复杂度
算法时间复杂度分析:
1.最好的情况,即不需要回溯,一次匹配成功,复杂度为O(m)
2.最坏的情况,需要多次回溯时,复杂度为O(n×m)
3.算法平均时间复杂度为O(n×m)
代码实现
下面给出暴力匹配算法的代码:
/**
* 暴力匹配算法(又称简单的模式匹配算法)
* Brute Force: O(mn)
* @param s 目标串
* @param p 模式串
* @return 如果匹配成功,返回下标,否则返回-1
*/
public static int stringMatch(String s, String p) {
if (s.equals(p)) {
return 0;
}
if (s.length() < p.length()) {
return -1;
}
int i = 0, j = 0;
while (i < s.length() && j < p.length()) {
// 如果当前字符匹配成功(即S[i] == P[j]),则i++,j++,继续匹配下一个字符
if (s.charAt(i) == p.charAt(j)) {
i++;
j++;
} else {
// 如果失配(即S[i]! = P[j]),令i = i - (j - 1),j = 0。相当于每次匹配失败时,i 回溯,j 被置为0
i = i - j + 1;
j = 0;
}
}
if (j == p.length()) {
return i - j;
} else {
return -1;
}
}
每次失败都要回溯匹配,导致局部匹配重复。
改进的模式匹配算法——KMP算法
概念
- 前缀:除最后一个字符以外,字符串所有的头部子串
- 后缀:出第一个字符外,字符串的所有尾部子串
- 最长相等前后缀(又称部分匹配值或最长公共前后缀):字符串前缀和后缀的最长相等前后缀
如下所示,找出最长相同前后缀:
next数组:
next 数组相当于“最大长度值” 整体向右移动一位,然后初始值赋为-1,即
思路图解:
1.
首先,字符串"BBC ABCDAB ABCDABCDABDE"的第一个字符与搜索词"ABCDABD"的第一个字符,进行比较。因为B与A不匹配,所以搜索词后移一位。
2.
因为B与A不匹配,搜索词再往后移。
3.
就这样,直到字符串有一个字符,与搜索词的第一个字符相同为止。
4.
接着比较字符串和搜索词的下一个字符,还是相同。
5.
直到字符串有一个字符,与搜索词对应的字符不相同为止。
6.
这时,最自然的反应是,将搜索词整个后移一位,再从头逐个比较。这样做虽然可行,但是效率很差,因为你要把"搜索位置"移到已经比较过的位置,重比一遍。
7.
一个基本事实是,当空格与D不匹配时,你其实知道前面六个字符是"ABCDAB"。KMP算法的想法是,设法利用这个已知信息,不要把"搜索位置"移回已经比较过的位置,继续把它向后移,这样就提高了效率。
8.
怎么做到这一点呢?可以针对搜索词,算出一张《部分匹配表》(Partial Match Table)。这张表是如何产生的,后面再介绍,这里只要会用就可以了。
9.
已知空格与D不匹配时,前面六个字符"ABCDAB"是匹配的。查表可知,最后一个匹配字符B对应的"部分匹配值"为2,因此按照下面的公式算出向后移动的位数:
移动位数 = 已匹配的字符数 - 对应的部分匹配值
因为 6 - 2 等于4,所以将搜索词向后移动4位。
10.
因为空格与C不匹配,搜索词还要继续往后移。这时,已匹配的字符数为2(“AB”),对应的"部分匹配值"为0。所以,移动位数 = 2 - 0,结果为 2,于是将搜索词向后移2位。
11.
因为空格与A不匹配,继续后移一位。
12.
逐位比较,直到发现C与D不匹配。于是,移动位数 = 6 - 2,继续将搜索词向后移动4位。
13.
逐位比较,直到搜索词的最后一位,发现完全匹配,于是搜索完成。如果还要继续搜索(即找出全部匹配),移动位数 = 7 - 0,再将搜索词向后移动7位,这里就不再重复了。
14.
下面介绍《部分匹配表》是如何产生的。
首先,要了解两个概念:“前缀"和"后缀”。 "前缀"指除了最后一个字符以外,一个字符串的全部头部组合;"后缀"指除了第一个字符以外,一个字符串的全部尾部组合。
15.
"部分匹配值"就是"前缀"和"后缀"的最长的共有元素的长度。以"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。
16.
"部分匹配"的实质是,有时候,字符串头部和尾部会有重复。比如,“ABCDAB"之中有两个"AB”,那么它的"部分匹配值"就是2("AB"的长度)。搜索词移动的时候,第一个"AB"向后移动4位(字符串长度-部分匹配值),就可以来到第二个"AB"的位置。
时间复杂度
KMP算法时间复杂度分析:
设主串s长度为n,模式串t长度为m,在KMP算法中求next数组的时间复杂度为O(m),在后面的匹配中因主串s的下标不回溯,比较次数可记为n,所以KMP算法平均时间复杂度为 O(n+m)。
代码实现:
/**
* @ClassName KMP
* @Author Fenglin Cai
* @Date 2021 09 22 20
* @Description 串的KMP模式匹配算法
**/
public class KMP {
/**
* aabaaf 前缀表:01012 , 前缀表的数字表示的是字符从0到该下标位置的最大公共后缀数(也称最长相等前后缀长度)。
* next表 ——》前缀表右移一位,且首位添加-1即:-101012
* @param p
* @return
*/
public static int[] getNext(String p) {
int len = p.length();
int[] next = new int[len];
next[0] = -1;
int i = 0, k = -1;
while (i < len - 1) {
// p[k]表示前缀,p[i]表示后缀
if (k == -1 || p.charAt(i) == p.charAt(k)) {
++k;
++i;
next[i] = k; //前缀的下标加一是最大相同前后缀数(最长公共前后缀数)
} else {
k = next[k];
}
}
return next;
}
/**
* 时间复杂度:O(m+n)
* @param s 目标串
* @param p 模式串
* @return 如果匹配成功,返回下标,否则返回-1
*/
private static int kmpSearch(String s, String p) {
int sLen = s.length();
int pLen = p.length();
if (sLen < pLen) {
return -1;
}
int[] next = getNext(p);
// matching: O(n)
int i = 0, j = 0;
while (i < sLen && j < pLen) {
//①如果j = -1,或者当前字符匹配成功(即S[i] == P[j]),都令i++,j++
if (j == -1 || s.charAt(i) == p.charAt(j)) {
i++;
j++;
} else {
//②如果j != -1,且当前字符匹配失败(即S[i] != P[j]),则令 i 不变,j = next[j]
//next[j]即为j所对应的next值
j = next[j];
}
}
if (j == pLen) {
return i - j;
} else {
return -1;
}
}
// 测试运行入口
public static void main(String[] args) {
String s = "aabaabaaf";
String model = "aabaaf";
int i = KMP.kmpSearch(s, model);
System.out.println(i);
}
}
Reference
https://blog.youkuaiyun.com/qq_45406002/article/details/115277669
https://blog.youkuaiyun.com/u010232171/article/details/41945605
https://blog.youkuaiyun.com/tracydragonlxy/article/details/106658736