程序员常用十大算法(四):KMP算法 与 暴力匹配算法 解决字符串匹配问题

给出如下两字符串:
String str1=“lovilovilovloveiloveyou”;
String str2=“ilove”;
要求从 str1 中找出是否存在目标字符串 str2,如果有,返回第一次出现的首字符下标,没有返回-1.

暴力匹配算法:

思路分析:
假设现在str1匹配到了i位置,子串str2匹配到了j位置,则:
1)如果当前字符匹配成功(即str1[i]==str2[j]),则i++,j++,继续匹配下一个字符
2)如果失败(即str1[i]!=str2[j]),令i=i-(j-1),j=0.相当于每次匹配失败是,i回溯,j被置为0.
代码实现:

public class BruteForceDemo {
    
    
	public static void main(String[] args) {
    
    
		String arr="lovilovilovloveiloveyou";
		String target="ilove";
		
		int index = violenceMatch(arr,target);
		System.out.println(index);
	}
	/**
	 * @param str1 被匹配字符串
	 * @param str2 待匹配字符串
	 * @return 返回出现的首字母索引
	 */
	public static int violenceMatch(String str1,String str2) {
    
    
		char[] s1=str1.toCharArray();
		char[] s2=str2.toCharArray();
		int i=0;//i索引指向s1
		int j=0;//j索引指向s2
		while(i<s1.length&&j<s2.length) {
    
    //保证匹配时下标不越界
			if(s1[i]==s2[j]) {
    
    //匹配正确时,i,j指针均往后移动
				i++;
				j++;
			}else {
    
    //没有匹配成功时
				//匹配失败,i回溯,j置0
				i=i-j+1;
				j=0;
			}
		}
		//判断是否匹配成功
		if(j==s2.length) {
    
    
			return i-j;
		}else {
    
    
		return -1;
		}
	}
}

弊端:

用暴力方法解决的话就会有大量的回溯,每次只移动一位,若是不匹配,移动到下一位接着判断,浪费了大量的时间。

KMP算法:

1)KMP是一个解决模式串在文本串是否出现过,如果出现过,最早出现的位置的经典算法。
2)KMP算法利用之前判断过信息,通过一个next数组,保存模式串中前后最长公共子序列的长度,每次回溯时,通过next数组找到,前面匹配过的位置,省去了大量的计算时间。

图解:
1.首先,用str1的第一个字符和str2的第一个字符去比较,不符合,关键字向后移动一位
在这里插入图片描述
2.重复第一步,还是不符合,继续后移
在这里插入图片描述
3.一直重复,直到str1有一个字符与str2的第一个字符符合为止
在这里插入图片描述
4.接着比较字符串和搜索词的下一个字符,还是符合
在这里插入图片描述
5.直到遇到str1有一个字符与str2的一个字符不匹配
在这里插入图片描述
6.这个时候,想到的是继续遍历str1的下一个字符,重复第一步。(这样是很不明智的,因为此时ilov已经比较过了,没有必要再做重复的工作,一个基本的事实是,当i与e不匹配时,你其实知道前面四个字符是ilov。KMP算法的想法是,设法利用这个已知信息,不要把“搜素位置”移动回已经比较过的位置,继续把它向后移,这样就提高了效率。)
暴力匹配的思路便是:i=i-j+1 ,j=0:
在这里插入图片描述
如果用KMP思想便是:(直接把i的索引返回到之前匹配过的位置上)
在这里插入图片描述
但是这样是在“lov”中没有与“i”重复的情况下。
如果有重复,也会有新的问题:
如下:如果是这样两个字符串
在这里插入图片描述
当e与v不匹配时,按照上面思路,ovoe应该直接开始与oe进行匹配,即:
在这里插入图片描述
但是显然,我们此时错过了第二个o而导致的匹配结果出错。即当匹配子串的首字符与后面字符有相同时,不应直接放到已匹配字符的后面。
即应该从第二个o重新匹配:
在这里插入图片描述
如此我们得到的结果才算正确。
7.那么如何判断子串在匹配失败时应该回溯的位置?
我们可以给出一个公式:
子串向后移动的位数=已匹配的字符数-对应的部分匹配值
那么
什么是部分匹配值
字符串:“bread”
前缀:b,br,bre,brea
后缀:read,ead,ad,d
而部分匹配值就是“前缀”和“后缀”的最长的共有元素的长度。
以ovoe为例
“o"的前缀和后缀都为空,所以最长的共有元素的长度为0
"ov"的前缀为“o“,后缀为”v“,最长的共有元素的长度为0
“ovo"的前缀为”o“,”ov“,后缀为”vo”,“o”,最长的共有元素的长度为1
“ovoe"的前缀为”o“,”ov“,”ovo“,后缀为”voe“,”oe“,”e”,最长的共有元素的长度为0

所以ovoe已匹配的字符为”ovo“,其对应的部分匹配值为1,
已匹配的字符数为3,
子串向后移动的位数便是3-1=2;
所以ovoe才从
在这里插入图片描述

在这里插入图片描述

代码实现:

public class KMP {
    
    
	public static void main(String[] args) {
    
    
		String arr="lovovoe";
		String target="ovoe";
		int[] next=next(target);
		
		int index = kmpMatch(arr,target,next);
		System.out.println(index);
	}
	//先得到子串的部分匹配表
	//获取到一个字符串(子串)的部分匹配值
	public static int[] next(String target) {
    
    
		//创建一个next数组,保存部分匹配值
		int[] next=new int[target.length()];
		next[0]=0;//如果字符串长度为1,部分匹配值一定为0
		for(int i=1,j=0;i<target.length();i++) {
    
    
			while(j>0&&target.charAt(i)!=target.charAt(j)) {
    
    
				j=next[j-1];
			}
			//部分匹配值加1
			if(target.charAt(i)==target.charAt(j)) {
    
    
				j++;
			}
			next[i]=j;
		}
		return next; 
	}
	//kmp搜素
	/**
	 * @param str1 源字符串
	 * @param str2 子串
	 * @param next 子串对应的部分匹配表
	 * @return 返回第一个匹配的位置,没有返回-1
	 */
	public static int kmpMatch(String str1,String str2,int[] next) {
    
    
		 for(int i=0,j=0;i<str1.length();i++) {
    
    
			 //考虑str1.charAt(i)!=str2.charAt(j)的情况
			 while(j>0&&str1.charAt(i)!=str2.charAt(j)) {
    
    
				 j=next[j-1];
			 }
			 if(str1.charAt(i)==str2.charAt(j)) {
    
    
				 j++;
			 }
			 if(j==str2.length()) {
    
    
				 return i-j+1;
			 }
		 }
		 return -1;
	}
}

猜你喜欢

转载自blog.csdn.net/qq_45273552/article/details/109332870
今日推荐