最近在学习KMP用法时观看视频中的讲解,理解更加深刻一些,做一个笔记!
视频地址:1.KMP字符串匹配算法实现原理 2.KMP字符串匹配算法实现代码
假设现在我们面临这样一个问题:有一个文本串T,和一个模式串P,现在要查找P在T中的位置,怎么查找呢?
1.暴力匹配
当a与b不能匹配上的时候,那么将P这个字符串整体往右移动一格
移动之后继续从这个位置开始匹配,如果不能成功,那么继续右移一格:
如果用暴力匹配的思路,并假设现在文本串T匹配到 i 位置,模式串P匹配到 j 位置,则有:
如果当前字符匹配成功(即S[i] == P[j]),则i++,j++,继续匹配下一个字符;
如果失配(即S[i]! = P[j]),令i = i - j+1,j = 0。相当于每次匹配失败时,i 回溯(并右移一格),j 被置为0。
暴力匹配的代码,如下:
public static int getResult(String T,String P){
int tLen = T.length();
int pLen = P.length();
int i = 0;
int j = 0;
while (i < tLen && j < pLen)
{
// 如果当前字符匹配成功(即T[i] == P[j]),则i++,j++
if (T.charAt(i) == P.charAt(j))
{
i++;
j++;
}// 如果失配(即T[i]! = P[j]),令i = i - j + 1,j = 0
else
{
i = i - j + 1;
j = 0;
}
}
// 匹配成功,i - j即i下标减去j的长度,即开始
if (j == pLen)
return i - j;
else
return -1;
}
2.KMP字符串匹配算法
1.先计算前缀表(prefix table)即先对短的字符串(模式串P)求前缀表(假如P=ababa)
- 第一步:写出该模式串P的所有前缀
- 第二步:将每个前缀作为一个独立的字符串,然后找出它的最长公共前后缀(注意:比原始的字串要短)
一般来说不需要最后一段,然后在最前面补一个-1,然后写出前缀表:
寻找最长前缀后缀,模式串为"ababc",从左至右遍历整个模式串,其各个子串前后缀如表:
模式串的各个子串 | 前缀 | 后缀 | 最大公共元素长度 |
---|---|---|---|
a | 空 | 空 | 0 |
ab | a | b | 0 |
aba | a,ab | a,ba | 1 |
abab | a,ab,aba | b,ab,bab | 2 |
ababc | a,ab,aba,abab | c,bc,abc,babc | 0 |
可以写出原模式串子串对应的各个前缀后缀的公共元素的最大长度及next数组,next 数组跟之前求得的最大长度表对比后,不难发现,next 数组相当于“最大长度值” 整体向右移动一位,然后初始值赋为-1。
字符 | a | b | a | b | c |
---|---|---|---|---|---|
下标值 | 0 | 1 | 2 | 3 | 4 |
最大长度值 | 0 | 0 | 1 | 2 | 0 |
next数组 | -1 | 0 | 0 | 1 | 2 |
从左开始匹配,匹配失败之后将P串失配位置对应的next数组中值对应的位置拉到T串失配位置对齐,如图:
对齐之后,继续从这个位置开始匹配,当前位置还是错误的,那么继续同样步骤,对齐下标0(next对应位置值为0)的位置:
对齐0位置,继续匹配,但是当前位置任然不符合,但是前缀表(next数组)中对应值为-1,那么代表把"-1"这个位置对齐当前T中失配位置(将整个P右移一位):
对齐"-1"之后,直接从a开始匹配:
匹配成功:
如果在T中寻找所有P出现的次数,那么需要继续往后移动,按照前缀表,找2的位置,将c对齐2的位置:
从2位置继续往后匹配:
KMP字符串匹配算法代码:
public static void main(String[] args) {
// kmp测试
kmpSearch("ABABABCABAABABCABAAB","ABABCABAA");
}
public static void kmpSearch(String text,String pattern){
int []prefix=new int[pattern.length()];
// 创建prefix前缀表
createPreFixTable(pattern,prefix);
// 将prefix后移一个位置
movePrefixTable(prefix);
int m=text.length();
int n=pattern.length();
int i=0;
int j=0;
while (i<m){
if (j==n-1&&text.charAt(i)==pattern.charAt(j)){
// 匹配成功
System.out.println("已找到,下标开始:"+(i-j));
// 右移继续匹配 找到prefix[j]的值对齐
j=prefix[j];
}
if (text.charAt(i)==pattern.charAt(j)){
i++;
j++;
}
// 当不相等的时候,去找prefix数组里面的值
else {
j=prefix[j];
// 如果移动至某一个位置 遇到-1时 统一右移,使对齐
if (j==-1){
i++;
j++;
}
}
}
}
// 求patter字符串ABABCABAA的prefix(前缀表)
public static void createPreFixTable(String pattern,int []prefix){
prefix[0]=0;
int len=0;
// 从第二个数开始比较(因为第一个数的对应的prefix为0)
int i=1;
while (i<pattern.length()){
// 如果第i个位置的字符等于前缀长度相同位置
if (pattern.charAt(i)==pattern.charAt(len)){
len++;
prefix[i]=len;
i++;
}else { // 不相等的时候
// 限制len必须大于0
if (len>0){
len=prefix[len-1];
}// len已经无法向后面退了
else { // 关键!!! len==0
prefix[i]=len;
i++;
}
}
}
}
// 将前缀表依次后移最左位置补-1
public static void movePrefixTable(int []prefix){
for (int i=prefix.length-1;i>=1;i--){
prefix[i]=prefix[i-1];
}
prefix[0]=-1;
}
效果: