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
}
KMP算法——字符串匹配及其应用 时间复杂度O(M+N) 空间复杂度O(N)
最新推荐文章于 2024-04-23 05:00:00 发布