图解--KMP到底是怎么一回事?

94 阅读2分钟

1.前言

Kmp是一种用于字符串比较的常用算法,那KMP到底是如何实现快速匹配字符串的呢?下面由一个具体的例子引入

Leetcode算法题第28题

这个例子中文本串是haystack = "sadbutsad",目标串是:needle = "sad"

其解法我们首先想到的就是暴力破解

暴力解法的流程图如上图所示,从文本串的第一位开始和目标串一一比较字符,不匹配则从文本串后的下一位开始比较,直到i=len(文本串)-len(目标串)

暴力破解代码如下:

func strStr(haystack string, needle string) int {
	if len(needle)>len(haystack){
		return -1
	}
	for i:=0;i<len(haystack)-len(needle);i++{
		flage:=true
		for j:=0;j<len(needle);j++{
			if haystack[i+j]!=needle[j]{
				flage=false
				break
			}
		}
		if flage{
			return i
		}
	}
	return -1

} 

2.理论

Kmp主要的思想是匹配字符串的时候,出现不匹配的情况,记录目标串已经匹配的一部分内容,利用这些信息避免从头匹配目标串

如上图所示,当匹配到f时,发现与b不匹配,这时候kmp算法回退到b这个位置,从这个位置往后与文本串的冲突位置b往后一一匹配。那么回退的位置b是如何确定的呢?下面就引入这样几个概念---前缀表,前缀,后缀,最长前后缀

2.1前缀表

前缀表是用来回退的,他记录了目标串与文本串不匹配的时候,目标串应该从哪个位置重新开始匹配。

在具体的代码里很多人用next数组来表示。

2.2前缀

前缀:包含字符串第一个字符,不包含字符串最后一个字符的子串

2.3后缀

后缀:包含字符串最后一个字符,不包含字符串第一个字符的子串

2.4最长相等前后缀

最长相等前后缀:相等的前缀后缀且长度最长的子串

如下图所示,对aabaaf的每个子串求取最长相等前后缀,最长相等前后缀的长度就是前缀表的数值,没有最长相等前后缀数值为0

2.5根据前缀表进行目标串回退

上面得出aabaaf的前缀表是010120,发生冲突的位置在f,kmp会找到冲突位置的前一位的前缀表,其数值就是目标串要回退到的位置。

获取前缀表的函数中具体做一下三个操作:

1.初始化

2.处理前后缀不相等

3.处理前后缀相等

初始化:

KMP设定两个指针 i 和 j,那么这两个指针代表着什么含义呢?

j表示前缀的结束位置,也代表着i之前子串最长相等前后缀的长度,开始位置从0开始;

i表示后缀的结束位置,因为后缀不包括首个字母所以i的位置从1开始

又因为第一个单字符子串a是只有一个字符的字符串没有前后缀,最长相等前后缀长度必然为0所以设置next[0]=0

处理前后缀不相等

needle[i]!=needle[j],j要循环回退,j--;并且j>0否则数组会越界,如果j回退0

next[i]=j

处理前后缀相等

needle[i]==needle[j],j++,next[i]=j

具体流程如下图

获取前缀表代码实现

//求取前缀表
func GetPrefix(next []int,needle string){
    j:=0     //j表示前缀的结束位置,开始位置从0开始;j也代表着i之前子串最长相等前后缀的长度
    next[0]=0//前缀表第一个元素为0
    for i:=1;i<len(needle);i++{
        //前后缀不相等
        for needle[i]!=needle[j]&&j>0{
            j=next[j-1]
        }
        if needle[i]==needle[j]{
            j++
        }
        next[i]=j
    }
}

3.KMP解决字符串匹配问题

kmp解决Leetcode算法题第28题

func strStr(haystack string, needle string) int {
    next:=make([]int,len(needle))
    GetPrefix(next,needle)
    j:=0//目标串比较的起始位置
    for i:=0;i<len(haystack);i++{
        //目标串与文本串字符不相等
        for haystack[i]!=needle[j]&&j>0{
            j=next[j-1]
        }
        if haystack[i]==needle[j]{
            j++
        }
        //neddle遍历到最后一位时匹配到字符串
        if j==len(needle){
            return i-len(needle)+1
        }
    }
    return -1
}

//求取前缀表
func GetPrefix(next []int,needle string){
    j:=0     //j表示前缀的结束位置,开始位置从0开始;j也代表着i之前子串最长相等前后缀的长度
    next[0]=0//前缀表第一个元素为0
    for i:=1;i<len(needle);i++{
        //前后缀不相等
        for needle[i]!=needle[j]&&j>0{
            j=next[j-1]
        }
        if needle[i]==needle[j]{
            j++
        }
        next[i]=j
    }
}