模式匹配(Java)
模式匹配
模式匹配是数据结构中字符串的一种基本运算。
由于字符串我们学习过了,大部分操作都比较清楚,但是模式匹配相对来说操作稍微有些难度,所以我们在这里简单的进行讲述。
模式匹配的具体操作如下:给定一个子串(也称为模式串),要求在某个字符串中找出与该子串相同的所有子串。
我们在此讲述2种常见实现:
- 暴力匹配
- KMP算法
暴力匹配(BF算法)
主要思想:从主串的第一个元素开始,与模式串第一个元素相比较,相等则逐一比较,若有不同元素,主串回溯至下一个元素,与模式串的一个元素相比较,依次循环。
为了方便讲解,我们针对下面的案例来进行讲解:
要求在寻找模式串第一次在主串出现的位置,未找到则返回-1。
我们把这个过程分为5步:
- 首先,我们需要将主串进行遍历。
- 主串的每一次遍历中,与模式串进行比较,若相同比较下一个元素。
- 如果模式串比较结束,说明模式串成功匹配,返回主串当前下标。
- 如果两元素不同,说明此处匹配失败,主串继续遍历下一个元素。
- 若主串遍历结束,仍未成功匹配,则说明主串中无该模式串,返回-1。
我们举个例子演示一下,假设主串abdabcda,模式串abcd,模式匹配后应当得到3。
主串当前遍历到的元素/下标 | 主串的元素 | 模式串元素 | 比较 |
---|---|---|---|
a/0 | a | a | 相等,比较下一个元素 |
a/0 | b | b | 相等,比较下一个元素 |
a/0 | d | c | 不相等,回溯,继续遍历主串 |
b/1 | b | a | 不相等,回溯,继续遍历主串 |
d/2 | d | a | 不相等,回溯,继续遍历主串 |
a/3 | a | a | 相等,比较下一个元素 |
a/3 | b | b | 相等,比较下一个元素 |
a/3 | c | c | 相等,比较下一个元素 |
a/3 | d | d | 相等,比较下一个元素 |
a/3 | a | \0 | 模式串比较结束,匹配成功 |
完整代码如下
public static int bruteForceStringMatch(String str, String pattern) {
//如果主串长度不小于模式串,则进入模式匹配
if (str.length() >= pattern.length()) {
//获取两串的字符数组,以便遍历
char strOfChars[] = str.toCharArray();
char patternOfChars[] = pattern.toCharArray();
//两个循环控制变量
int loopOfStr, loopOfPattern;
//遍历主串,任意一串遍历结束,则匹配结束
for (loopOfStr = 0, loopOfPattern = 0 ; loopOfStr < str.length() && loopOfPattern < pattern.length() ;) {
//如果两元素相同,比较下一个元素
if (strOfChars[loopOfStr] == patternOfChars[loopOfPattern]) {
loopOfStr++;
loopOfPattern++;
} else {
loopOfStr -= loopOfPattern - 1;//主串下标回溯
loopOfPattern = 0;//模式串下标重置
}
}
//模式串匹配结束,表示匹配成功
if (loopOfPattern == pattern.length()) {
return loopOfStr - loopOfPattern;//主串中模式串第一次出现的位置
}
}
//模式匹配失败
return -1;
}
时间复杂度设主串和模式串的长度分别为m,n,则它在最坏情况下的时间复杂度是O(m*n)。
KMP算法
KMP算法
主要解决了BF算法的回溯问题,从而降低了时间复杂度。他的时间复杂度为O(m+n)。
主要思想:KMP算法的关键是利用匹配失败后的信息, 尽量减小两串的匹配次数,以达到快速匹配的目的。通过一个next[]数组寻找最长且相同的前缀和后缀,以减少匹配次数。
我们举个例子,来看看KMP算法是怎么工作的
主串:AAAAAB
模式串:AAAB
BF算法求解时:
我们在匹配时会发现,第一次中模式串与主串只有第四个元素不相同,其他元素相同。同时我们发现,模式串中前3个元素是相同的,我们不妨想想,第二次匹配时,模式串的前两个字母还有必要去比较吗?
显然这两次的比较是没有必要的,那么我们就要借助next[]数组来帮忙了。
模式串AAAB的next[]数组值为{-1,0,1,2},我们在后面会讲解next[]数组如何求取。我们在第一次匹配时,第四个元素不同,模式串下标移至next[3]的位置,即2,也就是下一次从第三个A的位置开始匹配,直接跳过了前两个A,减少了匹配时比较次数。之后是类似的操作。
我们将KMP算法匹配的过程也分为5步:
- 首先,我们需要将主串进行遍历。
- 主串的每一次遍历中,与模式串进行比较,若相同比较下一个元素。
- 如果模式串比较结束,说明模式串成功匹配,返回主串当前下标。
- 如果两元素不同,说明此处匹配失败,模式串下标更新至next[]值的位置,主串继续遍历下一个元素。
- 若主串遍历结束,仍未成功匹配,则说明主串中无该模式串,返回-1。:
KMP算法求解时:
KMP算法代码如下
public static int KMP(String str, String pattern) {
//如果主串长度不小于模式串,则进入模式匹配
if (str.length() >= pattern.length()) {
//获取next数组
int next[] = getNext(pattern);
//获取两串的字符数组,以便遍历
char strOfChars[] = str.toCharArray();
char patternOfChars[] = pattern.toCharArray();
//两个循环控制变量
int loopOfStr, loopOfPattern;
//遍历主串,任意一串遍历结束,则匹配结束
for (loopOfStr = 0, loopOfPattern = 0 ; loopOfStr < str.length() && loopOfPattern < pattern.length() ;) {
//如果两元素相同,或模式串全部匹配失败,比较下一个元素
if (loopOfPattern == -1 || strOfChars[loopOfStr] == patternOfChars[loopOfPattern]) {
loopOfStr++;
loopOfPattern++;
} else {
loopOfPattern = next[loopOfPattern];//模式串下标置为next值
}
}
//模式串匹配结束,表示匹配成功
if (loopOfPattern == pattern.length()) {
return loopOfStr - loopOfPattern;//主串中模式串第一次出现的位置
}
}
//模式匹配失败
return -1;
}
next[ ]数组
经过上面的例子我们发现,next[]数组的求取,是KMP算法的最重要的一环,那么next[]数组究竟应该怎么求呢?
next[]数组实际上存储了模式串每一个元素的前缀与后缀相同的最大长度(不包括自身),因此在匹配时造成了一种跳跃式匹配。我们还是用上面的模式串来解释:AAAB
注意:我们默认把第一个元素的next值设为-1
前缀 | 后缀 | 最大长度 | |
---|---|---|---|
0 | -1 | ||
1 | ∅ | ∅ | 0 |
2 | A | A | 1 |
3 | A,AA | AA,A | 2 |
next[loopOfPattern] = nextValue, 我们这里利用递归的思想求出next[loopOfPattern+1]的值:
- 如果p[loopOfPattern] = p[nextValue],则next[nexValue+1] = next[nextValue] + 1;
- 如果p[loopOfPattern] != p[nextValue],则令nextValue = next[nextValue],如果此时p[loopOfPattern] == p[nextValue],则next[loopOfPattern+1] = nextValue+1;
- 如果不相等,则继续递归前缀索引,令nextValue=next[nextValue],继续判断,直至nextValue=-1(即nextValue=next[0])或者p[loopOfPattern]=p[nextValue]为止
国际惯例,上代码
getNext方法的实现
private static int[] getNext(String pattern)
{
//获取两串的字符数组,以便遍历
char patternOfChars[] = pattern.toCharArray();
//创建next数组
int[] next = new int[pattern.length()];
int nextValue = -1, loopOfPattern = 0;//初始化next值及模式串下标
next[0] = -1;//这里采用-1做标识
while(loopOfPattern < pattern.length() -1)
{
//获取next数组
if(nextValue == -1 || patternOfChars[loopOfPattern] == patternOfChars[nextValue])
{
nextValue++;
loopOfPattern++;
next[loopOfPattern] = nextValue;
} else {
nextValue = next[nextValue];
}
}
return next;
}