一、前言
我们在算法练习时,会碰到一些类似于“匹配一个较短的字符串在一个较长的字符串中的首次出现的位置”的问题。
对于此类的问题,你还在进行暴力破解吗?
今天给大家介绍一种比较高效的算法KMP算法。
二、开始算法前的必备知识
1. 一个字符串的前缀的概念:
对于字符串“abcd”而言,字符串的前缀有“a”,“ab”,“abc”。
说白了,前缀就是一个字符串的子集【可不是真子集哈】,并且该子集开始字符是整个字符串的 开始字符
2. 一个字符串的后缀的概念:
对于字符串“abcd”而言,字符串的后缀有“d”,“cd”,“bcd”。
说白了,后缀就是一个字符串的子集【可不是真子集哈】,并且该子集结束字符是整个字符串的结束字符
3. 部分匹配值的概念:
部分匹配值要结合上面的前缀和后缀,我在这里废话不多说【直接举例子】
对于“abcab”而言,我们可以发现其前缀和后缀中相等的是“a”,“ab”这两个。
而一个字符串的部分匹配值就是 前后缀中相等的且长度最大的字符的长度。
对于“abcab”而言,就是“ab”,既部分匹配值为2
三、算法的描述
我们事先约定,较长的字符串为“被匹配字符串”,较短的字符串为“匹配字符串”。
在进行匹配的时候,我们将会只进行移动被匹配字符串【不要觉得疑惑,会面会有解释,自己得先有个印象】来实现快速匹配。
匹配字符串后移对少个单位的公式:
移动位数 = 已匹配的字符数 - 对应的部分匹配值
【移动位数】:被匹配字符串后移(相对后移)的位数
【对应的部分匹配值】:这里指的是“已匹配的字符数”对应的“部分匹配值”
下面进行举例说明:
对于被匹配字符串“ababcabcacbab”而言,求匹配字符串“abcac”首次出现的位置
第一步: 从头开始匹配
说明:
① 我们可以发现,当匹配到第三个字符(红框内的字符)的时候,我们发现匹配失败
② 已匹配的字符数 :2
③ 已匹配的字符“ab”,其前缀为“a”,后缀为“b”,很显然对应的部分匹配值为0
④ 移动位数 = 2 - 0 =2
第二步:被匹配字符串指针(图中没有画出,忘了)后移两位,并进行匹配
说明:
① 我们可以发现,当匹配到第五个字符(红框内的字符)的时候,我们发现匹配失败
② 已匹配的字符数 :4
③ 已匹配的字符“abca”,其前缀为“a、ab、abc”,后缀为“bca、ca、a”,很显然对应的部分匹配值为1【既 a】
④ 移动位数 = 4 - 1 =3
第三步:被匹配字符串指针后移三位,并进行匹配
① 注意:当“部分匹配值”不为0时,移动后的串可不是从头开始比较,看下图
最后,字符串匹配成功
所以最后得到的结果是6,可以发现我们用三步就完成了匹配,所以说KMP是算法还是比较高效的
四、分解代码【java】
- 计算已匹配的字符串的代码块:
- 计算"部分匹配值"的代码块:
- 已匹配的字符串的长度小于2时,相关代码:
- 已匹配的字符串的长度大于等于2时,相关代码:
- 当“被匹配字符串”和“匹配字符串”不符合要求时:
五、源代码【java】
源码:
/**
* @author : HuXuehao
* @date : 2021年5月25日上午10:30:48
* @version :
*/
public class KmpAlagor{
// 返回-1 则表示匹配失败
public int getFirAppearLocate(String str, String subStr) {
if(str == null || subStr ==null) return -1;
if(str.length() < subStr.length()) return -1;
if(str.length() == subStr.length()) {
//当长度相等且数值相等时,
if(str.equals(subStr)) return 0;
else return -1;
}
return getLocate(str, subStr);
}
/**
* @Description 返回subStr在str中出现的首个位置
* @param str
* @param subStr
* @return
*/
private int getLocate(String str, String subStr) {
//记录“被匹配字符串”前移的位置数
int locate = 0;
//记录“被匹配字符串”前移后的字符串
String afterMoveStr = "";
// 获取 str 和 subStr匹配成功的字符串
String partStr = getSameStr(str, subStr, 0);
//如果partStr(已匹配字符串) == subStr(匹配字符串),那么匹配成功
while(!partStr.equals(subStr)) {
//如果已匹配的字符串的长度小于2,那么不可能产生“部分匹配值”,既需要“被匹配字符串”前移1位即可
if(partStr.length() <= 1) {
locate++;
afterMoveStr = str.substring(locate); //“被匹配字符串”前移1位即可
//如果前移后的字符串长度小于“匹配字符串”,既为匹配失败,否自继续匹配
if(afterMoveStr.length()>=subStr.length())
partStr = getSameStr(afterMoveStr, subStr, 0);
else
return -1;
}else { // 如果已匹配的字符串的长度大于2,那么可能产生“部分匹配值”
//获取"部分匹配值"
int partialMatchVal = getPartialMatchVal(partStr);
//计算“被匹配字符串”一共需要前移的位置
locate += (partStr.length()-partialMatchVal);
afterMoveStr = str.substring(locate); //前移
// 如果前移后的字符串长度小于“匹配字符串”,既为匹配失败,否自继续匹配
if(afterMoveStr.length()>=subStr.length())
//获取afterMoveStr与subStr字符串相同字符串
partStr = getSameStr(afterMoveStr, subStr,partialMatchVal);
else
return -1;
}
}
return locate;
}
/**
* @Description 计算"部分匹配值"
* @param partStr : 已匹配的字符串
* @return
*/
private int getPartialMatchVal(String partStr) {
int nums = 0;//存放"部分匹配值"
ArrayList<String> list1 = new ArrayList<>(); //存放所有的前缀
ArrayList<String> list2 = new ArrayList<>(); //存放所有的后缀
// 从头到尾和从尾到头进行扫描(排除首尾的扫描)
for (int i=0,j=partStr.length()-1; i<partStr.length()-1 && j>0; i++,j--) {
list1.add(partStr.substring(0, i+1)); //获取前缀字符串
list2.add(partStr.substring(j, partStr.length())); //获取后缀字符串
}
//比较最大相同前缀(部分匹配值)
for (int i = 0; i < list1.size(); i++) {
if(list1.get(i).equals(list2.get(i)) && list1.get(i).length()>nums) {
nums = list1.get(i).length();
}
}
return nums;
}
/**
* @Description 计算str 和 subStr 中已匹配的字符串
* @param str : 被匹配字符串
* @param subStr 匹配字符串
* @param partialMatchVal 开始时匹配的位置
*/
private String getSameStr(String str, String subStr, int partialMatchVal) {
//根据KMP算法的特点,索引0~(partialMatchVal-1)的字符串肯定是相等的,先将其加入sameStr
String sameStr = subStr.substring(0,partialMatchVal);
//从索引partialMatchVal开始比较两个字符串,如果有相等的字符,则追加到sameStr中。
for (int i = partialMatchVal; i < subStr.length(); i++) {
if(str.charAt(i) == subStr.charAt(i)) {
sameStr += subStr.charAt(i);
}else {
break;
}
}
return sameStr;
}
}
测试代码:
public class Main {
public static void main(String[] args) {
System.out.println("【索引:从0开始,若返回-1,则表示为找到】");
System.out.println("【位置:从1开始,若返回0,则表示为找到】\n");
KmpAlagor kmp = new KmpAlagor();
int firstIndex = kmp.getFirAppearLocate("ababcabcacbab", "abcac");
System.out.println("abcac 在 ababcabcacbab 中首次出现的索引为:"+firstIndex);
System.out.println("abcac 在 ababcabcacbab 中首次出现的位置为:"+(firstIndex+1));
System.out.println();
firstIndex = kmp.getFirAppearLocate("ababcabcacbab", "a");
System.out.println("a 在 ababcabcacbab 中首次出现的索引为:"+firstIndex);
System.out.println("a 在 ababcabcacbab 中首次出现的位置为:"+(firstIndex+1));
System.out.println();
firstIndex = kmp.getFirAppearLocate("ababcabcacbabf", "f");
System.out.println("f 在 ababcabcacbabf 中首次出现的索引为:"+firstIndex);
System.out.println("f 在 ababcabcacbabf 中首次出现的位置为:"+(firstIndex+1));
System.out.println();
firstIndex = kmp.getFirAppearLocate("ababcabhcacbab", "h");
System.out.println("h 在 ababcabhcacbab 中首次出现的索引为:"+firstIndex);
System.out.println("h 在 ababcabhcacbab 中首次出现的位置为:"+(firstIndex+1));
System.out.println();
firstIndex = kmp.getFirAppearLocate("ababcabcacbab", "ydfghj");
System.out.println("ydfghj 在 ababcabcacbab 中首次出现的索引为:"+firstIndex);
System.out.println("ydfghj 在 ababcabcacbab 中首次出现的位置为:"+(firstIndex+1));
}
}
测试结果:
六、总结
- 处理好边界问题