KMP算法
网上大多数讲解kmp算法讲解的都是解法,而并没有清楚告诉为什么要这样做,就算能照着解法做并解决但不理解原由也很容易忘记,如果带着问题并解决一步步问题才能真正理解KMP算法。
为什么需要KMP算法?
kmp算法是一种改进的字符串匹配算法(相对于暴力法),提高匹配效率,查找字符串中是否包含某个子串,被匹配的串一般称为主串,匹配的串称为模式串。
KMP算法的步骤以及为什么?
无论是暴力法,还是KMP都是通过相对于主串移动模式串来进行匹配的主串和子每一位的字符串是否匹配来进行判断的,主串和子串某一位不匹配的时候暴力法只会将模式串相对于主串向前移动一位,如果每次都是模式串最后一位不匹配,那么比较的次数过于多。
而KMP则是尽可能多的向前移动以减少比较次数,提高匹配效率,但是不能跳过可能存在的匹配,由此KMP算法的解法就是:尽可能将模式串相对于主串的向前移动以减少比较次数,但又不能移动过多而跳过可能符合的匹配。
如何知道该向前移动多少位?
公共前后缀就能解决这个问题。
什么是公共前后缀?
前缀和后缀
可以用正则表达式来解释:
对于字符串ABABA,满足正则表达式^(.+).*A$
小括号对应的分组的内容就是前缀,而^A.*(.+)$
小括号的分组内容对应的就是后缀
则ABABA的前缀有ABAB ABA AB A,后缀有BABA ABA BA A
公共前后缀
公共前后缀有两个ABA与A
为什么公共前后缀可以解决移动多少位的问题?
假设主串为ABABABABCD,模式串为ABABAC,模式串匹配到C的时候,此时模式串和主串对应位匹配失败,此时为ABABA为成功匹配的字符串。为了尽可能多的向前移动而跳过无效的匹配位,如果ABABA存在包括第一位但不包括最后一位的连续子字符串(前缀)与包括最后一位但不包括第一位的连续子字符串(后缀)相等,就代表前缀可以相对于主串移动到后缀对应主串位置且恰好匹配成功,因为前缀和后缀是相等的,这就使下次模式串相对于主串向前移动的位置不仅跳过了无效的位置,而且匹配的位置是从公共前后缀后一位开始的。
存在多个公共前后缀以谁作为参照移动?
此就对应提及到KMP算法中,尽可能向前移动而不能跳过可能符合的匹配。存在多个公共前后缀,以谁为参照移动都是可以的,但是要满足不能跳过可能的匹配。对于模式字符串ABABAC而言,当匹配到C不成功时,此时匹配成功的串为ABABA,公共前后缀为ABA与A,如果以ABA为参照则模式串需要相对于主串ABABABABCD移动两位,模式串此时第一位与主串第三位相对应,相当于前缀移动到相同后缀对应的位置,如果以A为参照前缀A移动到相同后缀位置则需要移动4位,由此可见公共前缀越短向前移动越远,越长则移动越短,为了满足不能跳过可能的匹配,应该选择以最长公共前缀作为参照。
如何计算向前移动的位数?
对于主串ABABABABACD,模式串ABABAC而言,当匹配到C不匹配,匹配串ABABA的最长公共前缀为ABA,需要模式串前缀移动到其对应的公共后缀对应主串的位置,也就是模式串相对应主串移动两位,而2=5-3,2对应移动的位数,5对应模式串不匹配位C的下标(从0开始),3位最长公共前后缀的长度,所以公式为:
- 移动的位数=为不匹配位对应模式串的下标(从0开始)-匹配串(不包括匹配为)最长公共前后缀
特殊情况,如第一位就不匹配,此时匹配串为空,则规定此时最长公共前后缀为-1,如果不做此规定,如果最长公共前后缀为0的话,移动为为0-0=0模式串永远也不会向前移动,就像斐波拉契一样,第一位为1,第二位也为1,第三位才是前两位的和,第一位不匹配为KMP算法的特殊情况,此时也能满足以上公式
模式串ABABC每一位不匹配位对应的下标、匹配串最后一位、此时匹配串最长公共前缀:
不匹配位下标 | 0 | 1 | 2 | 3 | 4 | 5 |
---|---|---|---|---|---|---|
不匹配位字符 | A | B | A | B | A | C |
匹配串对应字符 | A | B | A | B | A | |
匹配串最长公共前后缀 | -1 | 0 | 0 | 1 | 2 | 3 |
当不匹配位为第一位,下标为0时,匹配串为空,对应最长公共前后缀为-1
匹配结束
- 如果模式串最后一位匹配成功,则匹配成功,下标为模式串第一位相对于主串的位置
- 如果模式串当前匹配位等于主串匹配位,则比较下一位,如果模式串当前匹配位无对应主串匹配为,则匹配结束无对应匹配项
代码实现
type Substr struct {
Input string
Search string
index int
}
//KMP算法
func (it *Substr) Kmp() {
//不匹配位对应的最长公共前后缀长度
table := make(map[int]int)
table[0] = -1 //下标为0则最长公共前后缀长度为-1
for i := 1; i <= len(it.Search)-1; i++ {
table[i] = it.maxCommonPrefixSuffix(it.Search[0:i])
}
i := 0 //主串下标位置
j := 0 //模式串下标位置
inputLen := len(it.Input)
searchLen := len(it.Search)
init := 0
for j < searchLen && (inputLen-i >= searchLen-j) {
if it.Input[i] == it.Search[j] {
i++
j++
} else {
i = j - table[j] + init
init = i
if table[j] >= 0 {//初始化
j = table[j]
}else{
j = 0
}
}
}
if j == searchLen { //匹配成功
it.index = i - j
} else {
it.index = -1
}
}
//求字符串的最长公共前后缀
func (it *Substr) maxCommonPrefixSuffix(str string) int {
strLen := len(str)
//计算前缀
prefix := make(map[string]int)
for i := 0; i < strLen-1; i++ {
prefix[str[0:i+1]] = i + 1
}
//计算后缀
suffix := make(map[string]int)
for i := 1; i <= strLen-1; i++ {
suffix[str[i:]] = strLen - 1
}
//找出最长公共前后缀
longest := 0
for k, v := range prefix {
if _, ok := suffix[k]; ok {
if v > longest {
longest = v
}
}
}
return longest
}
func (it *Substr) String() string {
return fmt.Sprintf("hackstack为:%s,needle为:%s,index为:%d", it.Input, it.Search, it.index)
}