KMP算法——字符串匹配及其应用 时间复杂度O(M+N) 空间复杂度O(N)

本文介绍了Knuth-Morris-Pratt(KMP)算法用于字符串匹配的高效实现,通过next数组加速匹配过程,并在示例和测试用例中展示了算法的工作原理。涉及到了字符串旋转查找、二叉树结构匹配和KMP算法在实际场景的应用。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

package kmp

import (
	"strconv"
	"strings"
	"testing"
)

/*


/*
Knuth-Morris-Pratt算法(简称KMP),是由D.E.Knuth、J.H.Morris
和V.R.Pratt共同提出的一个改进算法,消除了朴素的模式匹配算法中回溯问题,
完成串的模式匹配。
算法思想:
设目标串为s,模式串为t, i、j 分别为指示s和t的指针,i、j的初值均为0。
若有 si = tj,则i和j分别增1;
否则,i不变,j退回至j=next[j]的位置 ( 也可理解为串s不动,模式串t向右移动到si与tnext[j]对齐 );
比较si和tj。若相等则指针各增1;
否则 j 再退回到下一个j=next[j]的位置(即模式串继续向右移动 ),再比较 si和tj。
依次类推,直到下列两种情况之一:
1)j退回到某个j=next[j]时有 si = tj,则指针各增1,继续匹配;
2)j退回至 j=-1,此时令指针各增l,即下一次比较 si+1和 t0。
记模式P的长度为m,目标T的长度为n,则KMP匹配算法的时间复杂度的分析如下:
整个匹配算法由Find()和GenKMPNext()两部分算法组成。
在Find()中包含一个循环,J的初值为0,每循环一次j的值严格家1,
指导j等于n时循环结束,故循环执行了n次。在GenKMPNext()中,
表面上有两重循环,时间复杂度似乎为O(),其实不然,
GenKMPNext()的外层循环恰好执行了m-1次;
另外,j的初值为-1,外层循环中,每循环一次,j的值就加1,
同时,在内层循环中j减小,但最少不会小于-1,
因此内层循环中j=next[j]的语句的总的执行次数
应小于或等于j的值在外层循环中被加2 的次数。
即在算法GenKMPNext()结束时,j=next[j]的执行总次数小于等于m-1次。
综上,对于长度为m的模式和长度为n的目标T的模式匹配,
KMP算法的时间复杂度为O(m+n)。

字符串匹配位置
match字符串在str的某个子串(连续的字符)中是否被包含,是则返回下标,不是则返回-1
子序列包含match 是 动态规划问题

暴力解:以string 中每个位置开头,看能否匹配到match  str N   match M   复杂度O(N*M)




kmp复杂度             O(N)
加速:一个字符中每一个位置的一个指标:这个字符串中之前的最长前缀和最长后缀的匹配长度,每个位置的指标和自己没有关系
    a b c a b c k
    0 1 2 3 4 5 6

6位置指标:
待匹配字符串长度	前缀字符串		后缀字符串		前后子串是否相等
	1				a				c				否
	2				ab				bc				否
	3				abc				abc				是
	4				abca			cabc			否
	5				abcab			bcabc			否
一定不要让前后缀取到整体:即长度为6
这时候 说k所在位置的指标是 3 (相等的最大长度)即abc == abc 这一组数据的长度

  a a a a a a b
              位置的指标是:5
和这个位置本身无关,之前的一坨字符串,前缀和后缀相等时的最大长度



str长度N      match长度M
              对match字符串每个位置建立指标,建立好指标数组: next


match:
      a a b a a b c a a b a  a  b  c  s
      0 1 2 3 4 5 6 7 8 9 10 11 12 13 14
0位置前边没有元素,   所以指标人为规定 -1
1位置前边只有一个元素,所以指标人为规定 0

指标 -> next数组:[-1, 0, 1, 0, 1, 2, 3,. . . . .] 所有位置信息都存成了一个数组,该数组可以告诉我他位置前边的字符串前缀和后缀的最大子串长度
                 0  1  2  3  4  5  6
看next 数组如何加速匹配速度
next 表示如果失败,下一个位置去哪




 [暴力方法]
 str    i ............ x    假设 xy位置的元素不等,暴力解,要比对的位置 str 退回到i+1位置   match比对位置退回到0位置
 match  0 ............ y
 ------------------------------------------------------------------------------------------------------------

 [kmp算法]
 用next数组加速,str 字符串如果在某一步,在i位置对match的0位置,一路都相等,然后到x位置,match到y位置比对失败,下边的流程
 match 关于y位置建立过一个信息,前缀与后缀最大的相等长度,假设该长度为5,5代表在y之前的字符串中前缀长度为5的字符串和后缀长度为
 5 的字符串相等

 match [0............len=5]............[z。。。len=5]
 str比对位置的x不动,match比对位置y跳到前置为5的字符串的下一个字符的位置 [0............len=5]y............[z............len=5]

只是y往回跳,x不动,比较该位置是否相等,继续往下匹配
 原理的实质是:x停在这不动,y位置回跳到前缀匹配长度为5的下一个位置的实质是什么?
 如果在x位置之前作出一个长度为5的子串,str 【xxxx  [ j ...5...]】

 因为x,y之前的字符串都相等,所以,D一定等于A     A一定等于B    B一定等于C  所以A一定等于C,那么把开头
 str   [[===D===]......[===A===][x]]   x,y假设不等,则y回退到y',x不动,然后,x与y‘继续比对,在可能不等的位置继续向下比
 match [[===C===]y'....[===B===][y]]
 A == B == C


 原理2;比较抽象
 str      “a a b a a c” [a a b a a] [c?]
           i             j

 match:   [ a a b a a ]	c a a b a a  t
            0 1 2 3 4	5 6 7 8 9 10 11
                        ^
 c 位置和 t位置配不上
 前后缀为5 "aabaa"

 实质是str 从 j位置开始,能否匹配出match字符串
 不确定的是 c位置和回退到5的位置字符是否相等

原理1,即上边的
原理2, “a a b a a c” 这些位置一定无法匹配

加速原理:某个位置失败,来到右边离他最近的靠谱的能配出match的位置
 >>> i,j位置任何位置都配不出match,为什么?

str   [        k      |j          ]
       i       k      |          x
                      |
match [               |           ]
       0....                     y

 k .... x 之间的字符串一定不等于match 0。。。。到某一个字符之间的串
*/


//str中当前比对到的位置
//match中当前比对到的位置
//期望该算法整体收敛于O(N)
func getIndex(str, match string) int {
	if len(str) == 0 && len(match) == 0  || len(match) == 0 {
		return 0
	}
	if str == "" || match == "" || len(match) < 1 || len(str) < len(match) {
		return -1
	}

	// M <= N O(?)
	x, y, next := 0, 0, getNextArray(match)   //next[i] match中i之前的字符串match[0...i-1]最长的前缀和后缀相等的长度
	for x < len(str) && y < len(match) {  	  // O(N)
		if str[x] == match[y] {     // 相等 一起往下走
			x, y = x + 1, y + 1
		} else if next[y] == -1 {   // y == 0 也可以 让x换一个开头,因为y已经无法回退了
			x++
		} else {                    // y 还能往前跳最长前缀的下一个字符
			y = next[y]
		}
	}


	if y == len(match) {   // y越界,匹配到    返回某一个开头
		return x - y
	}

	return -1             // y没有越界 -1
}





/*
[ len 7 ]a ....... [ len 7 ]  a   x
                                  i
                                  ^
看 i - 1位置的数代表的下标 和 i - 1 位置的字符
是否 等于 i-1 位置的数对应下标上的字符

[ len 7 ]a ....... [ len 7 ]  a   x
 0 ~ 6   7                   i-1  i
                              ^
     7   : 'a'
     i-1 : 'a'
     所以x = 7 + 1 = 8
     任意 match[7] != match[i-1]  则 继续往前跳  直到 找到 下标对应的字符 == i位置的字符 或 -1

 */

//M O(M)
func getNextArray(match string) []int { // 返回匹配失败,下一个字符去的位置的数组  和 最大长度的数组。他俩是一个玩意
	if len(match) == 1 {
		return []int{-1}
	}

	next := make([]int, len(match))
	next[0] = -1
	next[1] = 0


	i, cur := 2, 0 //cur代表cur位置的字符,是当前和i-1位置比较的字符
	for i < len(next) {
		if match[i-1] == match[cur] { //跳出来的时候
			cur++
			next[i] = cur
			i++
		} else if cur > 0 {
			cur = next[cur]
		} else {
			next[i] = 0
			i++
		}
	}
	return next
}

func TestGetIndex(t *testing.T)  {
	t.Log(getIndex("hello12","hello1"))
}

func GoStringIndex(s, m string) int {
	for i := 0; i < len(s) - len(m); i++ {
		if s[i:i+len(m)] == m {
			return i
		}
	}
	return -1
}

func BenchmarkKMP(b *testing.B) {
	for i := 0; i < b.N; i++ {
		getIndex("aaaaaaaaaaab","aaaab")
	}
}

func BenchmarkIndex(b *testing.B) {
	for i := 0; i < b.N; i++ {
		GoStringIndex("aaaaaaaaaaab","aaaab")
	}
}






/*
str1  有很多的旋转词, 自己是自己的旋转词,这种旋转方式叫没有旋转
     “123456”
     “234561”
     “345612“
     “456123“
     “561234”
     “612345”
     “123456”

  给两个字符串,是旋转词 ,返回 true, 否则返回false
  最大的过滤条件是长度,长度对不上 返回false

  暴力:循环下标,逐个比对,有点像循环数组
  取巧的算法: 拼接一个自己,用kmp算法看字符串是否是大字符串的子串



 */

func TestContains(t *testing.T)  {  // go的字符串比对方法 如果字符串量非常小,会用暴力的方法,否则,底层函数已经涉及到调用汇编指令
	strings.Index("123","456")
}

/*
    给你两棵树 T1(二叉树) head1    T2(二叉树) head2
    问: head2 延伸出的子树是不是T1某棵子树结构对应关系一样, 子树 头节点下的结构都要
    返回true or false



   暴力:
   把整棵树先序 序列化


 */

type Node struct {
	Value int
	left  *Node
	right *Node
}

// big 做头节点的树,其中是否有某棵子树的结构是和small为头的树,完全一样的
func containsTree1(big, small *Node) bool {  //O(N*M)
	if small == nil {
		return true
	}
	if big == nil {
		return false
	}
	if isSameValueStructure(big,small) {
		return true
	}
	return containsTree1(big.left, small) || containsTree1(big.right, small)
}

// head1 为头的树是否和 head2 为头的整棵树的结构对应上,完全一样
func isSameValueStructure(head1, head2 *Node) bool {
	if head1 == nil && head2 != nil {
		return false
	}
	if head1 != nil && head2 == nil {
		return false
	}
	if head1 == nil && head2 == nil {
		return true
	}
	if head1.Value != head2.Value {
		return false
	}
	return isSameValueStructure(head1.left,head2.left) && isSameValueStructure(head1.right,head1.right)
}



//先序序列化, 没有歧义
func containsTree2(big, small *Node) bool { //O(N)
	if small == nil {
		return true
	}
	if big == nil {
		return false
	}

	strBig   := strings.Builder{}
	pres(big,&strBig)
	strSmall := strings.Builder{}
	pres(small,&strSmall)
	return getIndex(strBig.String(),strSmall.String()) != -1
}

//先序序列化, 没有歧义
func containsTree2Array(big, small *Node) bool { //O(N)
  if small == nil {
	  return true
  }
  if big == nil {
	  return false
  }
  
  strBigArr := []string{}
  presForArray(big,&strBigArr)

  strSmallArr := []string{}
  presForArray(small,&strSmallArr)
  
  return getIndexForArray(strBigArr,strSmallArr) != -1
}

func presForArray(head *Node, ans *[]string)  { // ans *[]string
	if head == nil {
		*ans = append(*ans, "nil")
	}else {
		*ans = append(*ans, strconv.Itoa(head.Value))
		presForArray(head.left,ans)
		presForArray(head.right,ans)
	}
}


func pres(head *Node, ans *strings.Builder)  { // ans *[]string
	if head == nil {
		ans.WriteString("<nil>")
		//ans = append(and, "nil"
	}else {
		ans.WriteString("<"+strconv.Itoa(head.Value)+">")  //防止歧义    <1020><3><nil><nil><nil>    <20><3><nil><nil><nil>
		//ans.WriteString(strconv.Itoa(head.Value))
		pres(head.left,ans)
		pres(head.right,ans)
	}
}

func TestContainsStructure(t *testing.T) {
	n1 := &Node{Value: 100}
	n2 := &Node{Value: 100}
	t.Log(containsTree2(n1,n2))
}


func TestContainsStructureArr(t *testing.T) {
	n1 := &Node{Value: 100}
	n2 := &Node{Value: 100}
	t.Log(containsTree2Array(n1,n2))
}


func getIndexForArray(str, match []string) int {  //改写kmp 形参接收array
	if len(str) == 0 && len(match) == 0  || len(match) == 0 {
		return 0
	}
	if str == nil || match == nil || len(match) < 1 || len(str) < len(match) {
		return -1
	}

	// M <= N O(?)
	x, y, next := 0, 0, getNextArrayForArray(match)   //next[i] match中i之前的字符串match[0...i-1]最长的前缀和后缀相等的长度
	for x < len(str) && y < len(match) {  	  // O(N)
		if str[x] == match[y] {     // 相等 一起往下走
			x, y = x + 1, y + 1
		} else if next[y] == -1 {   // y == 0 也可以 让x换一个开头,因为y已经无法回退了
			x++
		} else {                    // y 还能往前跳最长前缀的下一个字符
			y = next[y]
		}
	}


	if y == len(match) {   // y越界,匹配到    返回某一个开头
		return x - y
	}

	return -1             // y没有越界 -1
}

func getNextArrayForArray(match []string) []int { // 返回匹配失败,下一个字符去的位置的数组  和 最大长度的数组。他俩是一个玩意
	if len(match) == 1 {
		return []int{-1}
	}

	next := make([]int, len(match))
	next[0] = -1
	next[1] = 0


	i, cur := 2, 0 //cur代表cur位置的字符,是当前和i-1位置比较的字符
	for i < len(next) {
		if match[i-1] == match[cur] { //跳出来的时候
			cur++
			next[i] = cur
			i++
		} else if cur > 0 {
			cur = next[cur]
		} else {
			next[i] = 0
			i++
		}
	}
	return next
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

metabit

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值