KMP算法

目录

KMP算法的引入

最长前缀和后缀的匹配长度的详解

KMP算法的详解

KMP算法的引入

       KMP算法解决的问题是有字符串str1和str2,str1是否包含str2(str2是否是str1中连续的一段),如果包含返回str2在str1中开始的位置。如何做到时间复杂度O(N)完成?

       对于这个问题如果使用暴力求解,从str1中第一个字符开始,比较是否和str2中的字符相同,相同的话继续往下查找,否则在str1中继续以下一个字符开始依次查找比较,这样操作的话,假设str1的长度为N,str2的长度为M,那么时间复杂度为O\left ( N*M \right ),时间复杂度非常大。使用KMP算法可以很好的解决这个问题。

最长前缀和后缀的匹配长度的详解

       在介绍KMP算法之前,先介绍最长前缀和后缀匹配长度这一概念,指的是求解一个字符前面的前缀和后缀相同的最长的长度。

       比如对于一个字符串“abbabbk”,求k的最长前缀和后缀的匹配长度,首先从最开始的a开始,a作为前缀,最后的b为后缀不匹配,继续往下。前缀为ab,后缀为bb不匹配,继续往下。前缀为abb,后缀为abb,此时匹配,记录长度为3。继续往下,前缀为abba,后缀为babb不匹配,继续往下。前缀为abbab,后缀为bbabb,不匹配,继续的话达到字符串长度结束,所以k前面的最长前缀和后缀的匹配长度为3。

        对于这个长度的求解,采用的思想和KMP算法的思想是相同的,具体思想可以看下面的KMP算法的思想,也就是目前到了str2数组的i位置,根据它前面一个位置的最长前缀和后缀匹配长度的大小,跳转到和str2中和这个长度相同的位置处,将此处的字符和i-1位置的字符进行比较,如果相同,那么i位置的最长前缀和后缀匹配长度就等于i-1长度加上1。而如果二者不相同,那么根据此时这个长度相同的位置处的最长前缀和后缀匹配长度,按照前面同样的方式进行跳转,如果来到了零位置,和i-1位置的字符进行比较,还是不相同,那么此时i位置的最长前缀和后缀匹配长度为0.

public static int[] getNextArray(char[] ms) {//求解str2每个字符前面的最长前缀和后缀匹配长度
		if (ms.length == 1) {//长度为1时只有一个人为规定-1
			return new int[] { -1 };
		}
		int[] next = new int[ms.length];
		next[0] = -1;//0位置人为规定-1
		next[1] = 0;//1位置人为规定0
		int i = 2;//next数组的位置
		int cn = 0;//拿哪一位的字符和i-1位置的字符比较,同时也表示着当前位置的最长前缀和后缀怕匹配长度
		while (i < next.length) {
			if (ms[i - 1] == ms[cn]) {//当cn位置的字符和i-1位置的字符相等
				next[i++] = ++cn;//此时next[i]位置的cn为cn+1,同时也方便于后面cn的使用,然后后面该求i+1位置的信息了,i++
			} else if (cn > 0) {//如果不相等,但是cn大于0
				cn = next[cn];//cn位置根据自己的最长前缀和后缀匹配长度往前跳
			} else {//如果cn没有办法往前跳
				next[i++] = 0;//i位置的cn为0
			}
		}
		return next;
	}

KMP算法的详解

       对str2执行上面的操作,求解str2中每一个字符的最长前缀和后最匹配长度放到一个新建立的数组中,str2的第一个字符的长度设置为-1,第二个字符的长度设置为0,因为它们无法计算,所以人为规定。

        对于KMP算法实质上是对于在str1中查询str2匹配时,两个字符串均从头开始进行比较,传统方法是遇到不匹配的情况是str2直接跳到开头,str1直接跳到开头的下一个进行继续比较匹配,而KMP算法则是取消掉了都调回去的过程,减少了一些不必要的操作。KMP算法中如果遇到了不匹配的情况,那么根据str2中当前不匹配字符的最长前缀和后缀匹配长度的大小,str2的比较位置移动到长度大小的位置,str1的比较位置不移动,两者比较,如果相同继续往下比对。如果不相同,str2的比较位置根据当前位置的最长前缀和后缀匹配长度继续往前调转,直到str2跳到第0位置,如果此时和str1比较还不相同,那么此时str1的位置往前移动,str2的位置此时也在开头,开始新的比较配对操作。

        KMP算法的精妙之处在于可以省去很多无用的比较步骤,加速比较查询的过程。 时间复杂度为O\left ( N \right )。   

    //总体M<=N,总体时间复杂度为O(N)
    public static int getIndexOf(String s, String m) {
		if (s == null || m == null || m.length() < 1 || s.length() < m.length()) {
			return -1;
		}
		char[] str1 = s.toCharArray();
		char[] str2 = m.toCharArray();
		int i1 = 0;
		int i2 = 0;
		int[] next = getNextArray(str2);//str2的最长前缀和后缀匹配长度数组
        //O(N),因为这个循环里面三个条件的最大变化幅度为2N
		while (i1 < str1.length && i2 < str2.length) {//i1越界代表无法匹配出来str2的字符,i2越界代表已经从i1中匹配出来了
			if (str1[i1] == str2[i2]) {//如果两个字符串比对的位置都相等,都往下移
				i1++;
				i2++;
			} else if (next[i2] == -1) {//相当于i2==0,i2在第一个位置已经无法再往前跳了
				i1++;//str1++换一个开头和str2的零位置比对
			} else {
				i2 = next[i2];//str2中i2还可以往前跳,也就是还拥有最长前缀和后缀匹配长度
			}
		}
		return i2 == str2.length ? i1 - i2 : -1;//如果i2越界,那么i1目前的位置减去str2的长度就是匹配到的字符的开头,如果i1越界,那么没有匹配出来返回-1.
	}
    //O(M),采用同样的方式分析,循环里面的每一块可调节的最大幅度为2M
	public static int[] getNextArray(char[] ms) {//求解str2每个字符前面的最长前缀和后缀匹配长度
		if (ms.length == 1) {//长度为1时只有一个人为规定-1
			return new int[] { -1 };
		}
		int[] next = new int[ms.length];
		next[0] = -1;//0位置人为规定-1
		next[1] = 0;//1位置人为规定0
		int i = 2;//next数组的位置
		int cn = 0;//拿哪一位的字符和i-1位置的字符比较,同时也表示着当前位置的最长前缀和后缀怕匹配长度
		while (i < next.length) {
			if (ms[i - 1] == ms[cn]) {//当cn位置的字符和i-1位置的字符相等
				next[i++] = ++cn;//此时next[i]位置的cn为cn+1,同时也方便于后面cn的使用,然后后面该求i+1位置的信息了,i++
			} else if (cn > 0) {//如果不相等,但是cn大于0
				cn = next[cn];//cn位置根据自己的最长前缀和后缀匹配长度往前跳
			} else {//如果cn没有办法往前跳
				next[i++] = 0;//i位置的cn为0
			}
		}
		return next;
	}

你可能感兴趣的:(数据结构与算法,数据结构,算法,java)