01、kmp算法和串

1.KMP是什么?

相关代码地址:https://gitee.com/gudongkun/datestruct

kmp算法是高效的字符串匹配算法; 知识点来源于串,这一章。

2.why 串?

c语言本身的字符串,太简陋,甚至获取字符串长度都是O(n)的;很多c程序都有对字符串的封装,如redis的 sds,串,就是对这种封装思想的一个介绍,和实现标准。

除字符串匹配外,串的其他知识相对比较简单,放到本节最后介绍。

3.why KMP?

(1)简单匹配方式

字符串的简单匹配方式又称幼稚匹配方式,它的思路很直接:

  1. 一个指针指向原字符串开始称为比较指针,另外一个指针指向匹配串开始称为匹配指针。
  2. 比较指针指向的字符,和匹配指针指向的字符比较,如果相同,比指针和匹配指针都加1,继续比较。
  3. 如果不同,匹配指针,回溯到,开始比较的位置,匹配指针,重新指向比较指针的开头。继续比较。
  4. 如果匹配指针,已经指向匹配串的最后一个位置,比较结果仍然相同则,则说明字符匹配。
(2)存在的问题

这个算法的时间复杂度是很低的 O(m*n) ,m是匹配串长度,n是原字符长度。

在一次比较中,我们已经比较了很多位,才发现不同,匹配串真的只能移动1位吗,如果不是,移动多少位合适呢?kmp算法将给出答案。

4.kmp算法

(1)算法伪代码:
  1. 一个指针指向原字符串开始称为比较指针,另外一个指针指向匹配串开始称为匹配指针。
  2. 比较指针指向的字符,和匹配指针指向的字符比较,如果相同,比指针和匹配指针都加1,继续比较。
  3. 如果不同,在匹配指针往前,到匹配串开头截取一段子串(已经比较过的相同部分),找到最长公共前后缀,匹配指针移动到,前缀前一个位置,再比较。注意,比较指针没有回溯。
  4. 如果匹配指针,已经指向匹配串的最后一个位置,比较结果仍然相同则,则说明字符匹配。

重点:kmp 算法,只在第3步不同:

  1. 比较指针没有回溯
  2. 匹配指针只是从最长公共前后缀,的后缀前移动到前缀前。

由于指针没有回溯,算法的时间复杂度变成的O(n)

(2)next数组是什么:

注意匹配指针的移动关键是,是在匹配指针的不同位置x,移动到,前面的子串的前缀位置y。

  1. x确定后,子串就确定了。
  2. 子串确定了,子串的最长公共前后缀就确定了。
  3. 最长公共前后缀确定了,前缀的长度,也就是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)逻辑结构

一般两部分组成:存字符串的一个字符数组;保存长度的整形变量。

串的操作:

  1. 赋值操作
  2. 获取长度操作
  3. 串比较操作(kmp)
  4. 串连接操作
  5. 求子串操作:求给定字符串,从某一位置开始,到某一位置结束的串的操作
  6. 串清空操作
(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
}
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值