kmp及next数组

142 阅读2分钟

整体思路:

1.目标是从主串 str 中找到子串 sub 开始出现的位置 pos

    str : a b c a b c a b c a b c d a b c d e
    sub : c a b c d a
    找到 pos = 8

2.我们用 i 表示主串 str 的下标,用 j 表示子串 sub 的下标,同时遍历两个字符串,如果 j >= len(sub),则表示子串遍历完了,pos 位置为 i-j (i-len(sub));否则主串已经遍历完了,但没找到子串,pos 的位置为-1

3.主串 str 的下标 i 不可以回退,子串 sub 的下标 j 可以回退

                  i=5
  str : a b c a b d c c  
  sub : a b c a b c   
                  j=5  
  此时 i 不动,j 需要回到下标为2的位置,然后比较str[5]sub[2]
  我们可以用一个叫 next 的数组存放 j 的位置,j 根据 next 数组的值进行回退

  还是上面那个例子,我们用[]括起来一些信息,如下:
    str:  ab c[ab]dcc  
    sub: [ab]c[ab]c  
    str 中的 [ab]sub 中的第二个 [ab] 一定是匹配的,此时 i=5(指向d),j=5(指向c)
    根据i不后退,j后退的目标,j应该退到2的位置(指向c),因此next[5]=2,继续遍历,如果str[i]!=sub[j]则继续回退

4.next数组是遍历子串得到的,通过getNext()实现

next数组的的前两个元素总是-10  
  sub : a b a b a c  
  next:-1 0 0 1 2 3  
  
  对于sub字符串中的a(下标为2),需找到ab中以a开头,b结尾的两个相等的真子串(不存在)的长度,即0  
  对于sub字符串中的b(下标为3),需找到aba中以a开头,a结尾的两个相等的真子串(下标从0开始的"a"和下标从2开始的"a")的长度,即1  
  对于sub字符串中的a(下标为4),需找到abab中以a开头,b结尾的两个相等的真子串(下标从0开始的"ab"和下标从2开始的"ab")的长度,即2  
  对于sub字符串中的c(下标为5),需找到ababa中以a开头,a结尾的两个相等的真子串(下标从0开始的"aba"和下标从2开始的"aba")的长度,即3  

总结:next[i]=sub[0:i-1]中,if (找到两个 sub[0]...sub[i-1] 的子串,它们的长度赋给next) else (next[i]=0)  

代码实现

//next数组存放j需要回退的位置,数组的前两个元素总是-10
func getNext(str string) []int {
   //1.数组长度为字符串的长度
   next := make([]int,len(str))

   if len(str) >= 1{
      next[0] = -1
   }
   if len(str) >= 2 {
      next[1] = 0
   }
   
   //2.初始化i和k,i为字符串的下标,k=next[i]
   i := 1
   k := 0
   //3.遍历子串
   for i < len(str) - 1 {
      if k == -1 || str[i] == str[k]  {
         next[i+1] = k + 1
         k = next[i+1]
         i++
      } else if str[i] != str[k] {
         k = next[k]
      }
   }

   return next
}
// str是主串
// sub是字串
// return 字串在主串中的下标
// 假设i为主串的下标,j为字串的下标
// 目标:i不回退,j回退但不一定退到下标为0的位置
func KMP(str string, sub string) int{
   strlen, sublen:= len(str),len(sub)

   //1.判断字符串长度是否为0
   if strlen == 0 || sublen == 0 {
      return -1
   }

   //2.获取next数组
   next := getNext(sub)

   //3.遍历主串和子串
   i, j := 0, 0
   for i < strlen && j < sublen {
      if j == -1 || str[i] == sub[j] {
         i++
         j++
      } else {
         j = next[j] //j可能回退到-1,因此j重置为0(可以用j++),此时i需要往后移动(即i++),因此可以放到上面的if条件里
      }
   }

   //4.子串已经遍历完了,且j==sublen,因此i-j==i-sublen,也就是字串在主串中开始的下标
   if j >= sublen {
      return i-j
   }

   return -1
}
func main() {
   //1.
   //str := "ab"
   //str := "ababcabcdabcde"
   //str := "abcabcabcabcdabcde"
   //getNext(str)

   //2.
   s := "abcabcabcabcdabcde"
   t := "cabcda"
   pos := KMP(s, t)
   fmt.Println(pos)
}