朴素匹配算法
算法的基本思想是:从主串中的指定位置的字符开始和模式串中的第一个字符比较,若相等,则继续逐个比较后续字符;否则从主串的下一个字符起再重新和模式串的字符依次比较。
public int getIndex(String mainStr, String patternStr, int index){
int i = index, j = 0; //从指定下标开始匹配模式串
int msLength = mainStr.length(); //主串的长度
int psLength = patternStr.length(); //模式串的长度
while(i < msLength && j < psLength){
if(mainStr.charAt(i) == patternStr.charAt(j)){
i++; //字符相同则i,j都向后移动以为继续比较
j++;
} else {
i = i - j + 1; //字符不同则i,j都进行回溯
j = 0;
}
}
return j >= psLength ? i - j : -1; //这里的i,j代表的是字符在字符串中的下标
}
这种算法是最普通的匹配算法,算法效率低,时间复杂度在处理不同情况下不同,有[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-DewiOysN-1678323993269)(null#card=math&code=O(n+m)]和O(n*m)&id=e8g42)两种情况。
KMP模式匹配算法
在模式串中有重复出现的字串,kmp算法可以减少部分主串和模式串已经重合部分的字符比较,提高效率。相比于上述算法每次匹配失败,i指针回溯,在kmp算法中i指针不用回溯,只有j指针回溯,主串相当于只遍历了一次。
如图,朴素模式匹配和kmp模式比较:
代码如下:和朴素算法唯一不同就是i指针不用回溯,j回溯位置有next数组中指定。
public int getIndex(String mainStr, String patternStr, int index){
int i = index, j = 0; //从指定下标开始匹配模式串
int msLength = mainStr.length(); //主串的长度
int psLength = patternStr.length(); //模式串的长度
int[] next = getNext(patternStr); //获取next数组,数组中记录了回溯位置
while(i < msLength && j < psLength){
if(j == -1 || mainStr.charAt(i) == patternStr.charAt(j)){
i++; //字符相同则i,j都向后移动以为继续比较
j++;
} else {
j = next[j]; //只有j指针回溯,回溯位置有next数组指定
}
}
return j >= psLength ? i - j : -1; //这里的i,j代表的是字符在字符串中的下标
}
Next数组推导及算法实现
next数组中记录了j指针回溯位置,是kmp算法的关键。
下面举例说明推导过程:注意这里的num是字符是第几个而不是下标位置。
num=index+1 | 123456789 |
---|---|
模式串T | ababaaaba |
next[index] | 011234223 |
- 当num=1时,next[index]=0。第一个字符前面没有任何字符串。
- 当num=2时,此时字符前面的字符串为"a",属于其他情况,需要模式串从头开始比较,next[index]=1。
- 当num=3时,此时字符前面的字符串为"ab",属于其他情况,next[index]=1。
- 当num=4时,此时字字符前面的字符串为"aba",前缀字符"a",后缀"a",next[index]=2。
- 当num=5时,此时字字符前面的字符串为"abab",前缀字符"ab",后缀"ab",next[index]=3。
- 当num=6时,此时字字符前面的字符串为"ababa",前缀字符"aba",后缀"aba",next[index]=4。
- 当num=7时,此时字字符前面的字符串为"ababaa",前缀字符"a",后缀"a",next[index]=2。
- 当num=8时,此时字字符前面的字符串为"ababaaa",前缀字符"a",后缀"a",next[index]=2。
- 当num=9时,此时字字符前面的字符串为"ababaaab",前缀字符"ab",后缀"ab",next[index]=3。
解释说明:
- next数组中记录的值就是该字符前面字符串前缀和后缀能够重合字符的个数再加一。但是在代码处理的部分可以整体都减一直接可以作为移动的下标。详细可见示例代码。
public int[] getNext(String patternStr){
int[] next = new int[patternStr.length()]; //建立next数组存储模式串中移动下标
int i = -1, j = 0; //i指向字符前 前缀字符串的最后一位下标 j是遍历模式串
next[0] = -1; //标记为回溯到头没有重合部分,主串和模式串都向后移动一位进行比较
while(j < next.length - 1){
if(i == -1 || patternStr.charAt(i) == patternStr.charAt(j)){
i++;
j++;
next[j] = i; //记录重合前缀字符长度,从重合部分的下一位比较
} else {
i = next[i]; //回溯i指针到有重合部分的前缀字符串最后一位
}
}
return next;
}
KMP模式匹配算法改进
当主串为"aaaabcde",模式串为"aaaaax",它的next数组为[-1, 0, 1, 2, 3, 4],而在i指针回溯的时候 i = 3,2,1,0。直接由i=3回溯到i=-1,从头开始比较。
num | 123456789 |
---|---|
模式串T | ababaaaba |
next[num] | 011234223 |
nextval[num] | 010104210 |
说明这里不是从0开始的下标计数。
- 当num=1,nextval[index]=0。
- 当num=2,因为第二位字符"b"的next值是1,而第一位就是"a",它们不相等,所以nextval[2]=next[2]=1,维持原值。
- 当num=3,因为第三位字符"a"的next值是1,而第一位就是"a",它们相等,所以nextval[3]=next[1]=0。
- 当num=4,因为第四位字符"b"的next值是2,而第二位就是"b",它们相等,所以nextval[4]=next[2]=1。
- 当num=5,因为第五位字符"a"的next值是3,而第三位就是"a",它们相等,所以nextval[5]=next[3]=0。
- 当num=6,因为第六位字符"a"的next值是4,而第四位就是"b",它们不相等,所以nextval[6]=4。
- 当num=7,因为第七位字符"a"的next值是2,而第二位就是"b",它们不相等,所以nextval[7]=2。
- 当num=8,因为第八位字符"b"的next值是1,而第一位就是"a",它们不相等,所以nextval[7]=1。
- 当num=9,因为第九位字符"a"的next值是3,而第三位就是"a",它们相等,所以nextval[7]=next[3]=0。
public int[] getNextVal(String patternStr){
int[] next = new int[patternStr.length()];
int i = -1, j = 0;
next[0] = -1;
while(j < next.length - 1){
if(i == -1 || patternStr.charAt(i) == patternStr.charAt(j)){
i++;
j++;
if(patternStr.charAt(j) != patternStr.charAt(i)){
next[j] = i; //若字符不等 当前i值为该字符回溯下标
} else {
next[j] = next[i]; //若字符相等 当前字符回溯下标和回溯字符的回溯下标相同
}
} else {
i = next[i];
}
}
return next;
}
改进过的KMP算法,它是在计算出next值的同时,如果a位字符于它next值指向的b位字符相等,则该a位的nextval就指向b位的nextval值,如果不相等,则该a位的next值就是它自己a位的next的值。