KMP算法详解,或字符串匹配算法详解,附带源代码哦~

一、前言

我们在算法练习时,会碰到一些类似于“匹配一个较短的字符串在一个较长的字符串中的首次出现的位置”的问题。

对于此类的问题,你还在进行暴力破解吗?
今天给大家介绍一种比较高效的算法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】

  1. 计算已匹配的字符串的代码块:
    在这里插入图片描述
  2. 计算"部分匹配值"的代码块:在这里插入图片描述
  3. 已匹配的字符串的长度小于2时,相关代码:
    在这里插入图片描述
  4. 已匹配的字符串的长度大于等于2时,相关代码:
    在这里插入图片描述
  5. 当“被匹配字符串”和“匹配字符串”不符合要求时:
    在这里插入图片描述

五、源代码【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));
	}
}

测试结果:
在这里插入图片描述

六、总结

  1. 处理好边界问题

在这里插入图片描述

  • 1
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值