1.KMP是什么?
相关代码地址:https://gitee.com/gudongkun/datestruct
kmp算法是高效的字符串匹配算法; 知识点来源于串,这一章。
2.why 串?
c语言本身的字符串,太简陋,甚至获取字符串长度都是O(n)的;很多c程序都有对字符串的封装,如redis的 sds,串,就是对这种封装思想的一个介绍,和实现标准。
除字符串匹配外,串的其他知识相对比较简单,放到本节最后介绍。
3.why KMP?
(1)简单匹配方式
字符串的简单匹配方式又称幼稚匹配方式,它的思路很直接:
- 一个指针指向原字符串开始称为比较指针,另外一个指针指向匹配串开始称为匹配指针。
- 比较指针指向的字符,和匹配指针指向的字符比较,如果相同,比指针和匹配指针都加1,继续比较。
- 如果不同,匹配指针,回溯到,开始比较的位置,匹配指针,重新指向比较指针的开头。继续比较。
- 如果匹配指针,已经指向匹配串的最后一个位置,比较结果仍然相同则,则说明字符匹配。
(2)存在的问题
这个算法的时间复杂度是很低的 O(m*n) ,m是匹配串长度,n是原字符长度。
在一次比较中,我们已经比较了很多位,才发现不同,匹配串真的只能移动1位吗,如果不是,移动多少位合适呢?kmp算法将给出答案。
4.kmp算法
(1)算法伪代码:
- 一个指针指向原字符串开始称为比较指针,另外一个指针指向匹配串开始称为匹配指针。
- 比较指针指向的字符,和匹配指针指向的字符比较,如果相同,比指针和匹配指针都加1,继续比较。
- 如果不同,在匹配指针往前,到匹配串开头截取一段子串(已经比较过的相同部分),找到最长公共前后缀,匹配指针移动到,前缀前一个位置,再比较。注意,比较指针没有回溯。
- 如果匹配指针,已经指向匹配串的最后一个位置,比较结果仍然相同则,则说明字符匹配。
重点:kmp 算法,只在第3步不同:
- 比较指针没有回溯
- 匹配指针只是从最长公共前后缀,的后缀前移动到前缀前。
由于指针没有回溯,算法的时间复杂度变成的O(n)
(2)next数组是什么:
注意匹配指针的移动关键是,是在匹配指针的不同位置x,移动到,前面的子串的前缀位置y。
- x确定后,子串就确定了。
- 子串确定了,子串的最长公共前后缀就确定了。
- 最长公共前后缀确定了,前缀的长度,也就是y就确定了。匹配指针移动到y前面就ok了。
这个对应关系,我们用匹配串就能提前算出来,这些信息可以存入一个,x为下标,y为值,并且和匹配串等长的数组。这个数组就叫next数组。
(3)next数组的求法
//GetNext 获取next数组的函数
//T 是匹配串,返回next数组
// next数组长度和匹配串长度相等
func GetNext(T string) []int {
length := len(T)
nextVal := make([]int, length)
nextVal[0] = -1
k := -1
j := 0
for j < length-1 {
if k == -1 || T[j] == T[k] {
j++
k++
nextVal[j] = k
} else {
k = nextVal[k]
}
}
return nextVal
}
next数组的求法,是大神们总结的很精炼的算法,也比较难以理解。
有一种递归理解的方式,请参考:
https://www.cnblogs.com/tangzhengyue/p/4315393.html
(4)KMP算法代码
//KMP kmp算法匹配字符串,
//S为原始字符,T为需要匹配的字符串,简称匹配串。
func KMP(S, T string) int{
i := 0 //比较指针,原始字符串已经比较的字符的下标。
j := 0 //匹配串的位置。
next := GetNext(T)
for i<len(S) && j <len(T) {
if j == -1 || S[i] == T[j] {
i ++
j ++
} else {
j = next[j]
}
}
if j == len(T) {
return i - j //匹配,返回匹配位置开始下标
} else {
return -1 //不匹配
}
}
完整代码:
package main
import "fmt"
func main() {
//str := "abcdabdab"
//nextVal := GetNext(str)
aa := KMP("hello world!","world")
fmt.Println(aa)
}
//GetNext 获取next数组的函数
//T 是匹配串,返回next数组
// next数组长度和匹配串长度相等
func GetNext(T string) []int {
length := len(T)
nextVal := make([]int, length)
nextVal[0] = -1
k := -1
j := 0
for j < length-1 {
if k == -1 || T[j] == T[k] {
j++
k++
nextVal[j] = k
} else {
k = nextVal[k]
}
}
return nextVal
}
//KMP kmp算法匹配字符串,
//S为原始字符,T为需要匹配的字符串,简称匹配串。
func KMP(S, T string) int{
i := 0 //比较指针,原始字符串已经比较的字符的下标。
j := 0 //匹配串的位置。
next := GetNext(T)
for i<len(S) && j <len(T) {
if j == -1 || S[i] == T[j] {
i ++
j ++
} else {
j = next[j]
}
}
if j == len(T) {
return i - j //匹配,返回匹配位置开始下标
} else {
return -1 //不匹配
}
}
(5)kmp算法,示例说明
假如,我们有两个字符串,(原字符串:ABBABBABABAAABABAAAA,模式串:ABBABAABABAA)我们我们是比较了多个才发现不同,比如6个,如下。
第一次比较,我们再横线处发现不同。
原字符串:ABBAB|BABABAAABABAAAA
模式串 :ABBAB|AABABAA
如果是幼稚模式我们会这样,模式串左移一位,比较指针,回溯到模式串开头。
A|BBABBABABAAABABAAAA
|ABBABAABABAA
可是其实我们已经比较过5个字符串,这些比较过的字符,比较指针根本不用用回溯。模式串则可以移动3位
原字符串:ABBAB|BABABAAABABAAAA
模式串 : AB|BABAABABAA
为什么是三位不是5位,我们看一下比较串中已经比较的5位(AB)B(AB),主意括号括起来的部分是一样的,其实我们只敢确定(AB)B是不同的,从原字符串中 ABB(AB)|BABABAAABABAAAA 和(AB)B(AB)中(AB)B后面部分又有可能是相同的了。这个匹配串中相同的部分,我们就叫公共前后缀。
所以,KMP算法的核心就是:
1.比较指针不回溯(比较变成O(n))
2.模式串,从前缀移动到后缀位置。
5.串
(1)定义
串就是字符串,是0个或者多个字符组成的有序序列。
(2)逻辑结构
一般两部分组成:存字符串的一个字符数组;保存长度的整形变量。
串的操作:
- 赋值操作
- 获取长度操作
- 串比较操作(kmp)
- 串连接操作
- 求子串操作:求给定字符串,从某一位置开始,到某一位置结束的串的操作
- 串清空操作
(3)实现
type SS struct {
Date []byte
Length int
}
//NewSS 赋值操作
func NewSS(s string) SS {
var ss SS
ss.Date = []byte(s)
ss.Length = len(ss.Date)
return ss
}
func (ss SS)Len() int {
return ss.Length
}
func CatSS(s1,s2 SS) SS {
var s3 SS
s3.Date = append(s1.Date,s2.Date...)
s3.Length = s1.Length +s2.Length
return s3
}
func (ss SS)StrPos(begin , end int) SS {
var s1 SS
s1.Date = ss.Date[begin:end]
s1.Length = end + 1 - begin
return s1
}
func (ss *SS)Clean() {
ss.Date = []byte{}
ss.Length = 0
}