李林超博客
首页
归档
留言
友链
动态
关于
归档
留言
友链
动态
关于
首页
GO
正文
golang算法篇之kmp算法介绍
Leefs
2024-04-06 PM
2315℃
0条
[TOC] ### 一、题目描述 **LeetCode 28. 找出字符串中第一个匹配项的下标** 给你两个字符串 `haystack` 和 `needle` ,请你在 `haystack` 字符串中找出 `needle` 字符串的第一个匹配项的下标(下标从 0 开始)。如果 `needle` 不是 `haystack` 的一部分,则返回 `-1` 。 **示例 1:** ``` 输入:haystack = "sadbutsad", needle = "sad" 输出:0 解释:"sad" 在下标 0 和 6 处匹配。 第一个匹配项的下标是 0 ,所以返回 0 。 ``` **示例 2:** ``` 输入:haystack = "leetcode", needle = "leeto" 输出:-1 解释:"leeto" 没有在 "leetcode" 中出现,所以返回 -1 。 ``` ### 二、字符串匹配暴力法 #### 2.1 示例 通过下方示例数据对字符串匹配做讲解 ``` 输入:text = "abaacababcac", pattern = "ababc" 输出:5 ``` #### 2.2 解析 一般的字符串匹配解法是将 2 个串的字符进行挨个比较,相当于是把每个字符都比较了一遍,这样是一定能得到结果的,不过显然这样操作导致的时间复杂度是`O(m*N)`,也就是 2 个字符串的长度之积,俗称暴力解法。 **用图像表示暴力解法的过程如下**: + 一开始,i 和 j 分别指向主字符串和子字符串 ![01.golang算法篇之kmp算法介绍01.jpg](https://lilinchao.com/usr/uploads/2024/04/927425408.jpg) + 这个时候开始比较 `t[i]` 和 `p[j]`,如果匹配则 i 和 j 同时向右移动一格,这样 i 和 j 同时移动到了位置 3 ![01.golang算法篇之kmp算法介绍02.jpg](https://lilinchao.com/usr/uploads/2024/04/2477403970.jpg) + 此时 i 和 j 不匹配了,需要对 i 和 j 进行回退,i 回退到了它最开始的下一位置,即位置 1,而 j 回退到位置 0 ![01.golang算法篇之kmp算法介绍03.jpg](https://lilinchao.com/usr/uploads/2024/04/1067775329.jpg) + 此时 i 和 j 仍然不匹配,i 继续向右移动一格,即位置 2,j 依然保持位置 0 ![01.golang算法篇之kmp算法介绍04.jpg](https://lilinchao.com/usr/uploads/2024/04/722917875.jpg) + 如此反复,每当不匹配时,i 都会回到开始匹配的位置同时移动一格,而 j 回到位置 0,当匹配时,i 和 j 同时向右移动,直到 j 到达子字符串的末尾。 ![01.golang算法篇之kmp算法介绍05.jpg](https://lilinchao.com/usr/uploads/2024/04/3163744695.jpg) ![01.golang算法篇之kmp算法介绍06.jpg](https://lilinchao.com/usr/uploads/2024/04/2340373216.jpg) #### 2.3 实现代码 ```go /** 步骤: 1. 获取字符串长度 2. 将字符串转换成字符数组 3. 遍历字符数组,进行比较 */ func strStr(haystack string, needle string) int { L := len(needle) // 第二个字符串的长度 Cap := len(haystack) // 带比较字符串的长度 H := []byte(haystack) // 将字符串转换成 byte 数组 N := []byte(needle) for i, val := range H { if val == N[0] { if i+L <= Cap && haystack[i:i+L] == needle { return i } } } return -1 } ``` ### 三、KMP算法原理 #### 3.1 概述 KMP 算法则用了一种聪明的办法,当发现字符串不匹配的时候,并不会从头开始比较,因为之前已经匹配成功的字符可以给我们提供一些有用的信息,利用这个信息可以将子串移动到某个位置,并从这个位置直接开始比较,它的时间复杂度降到了`O(m+n)`,也就是 2 个字符串的长度之和。 #### 3.2 字符串的前缀和后缀 首先需要知道字符串的前缀和后缀: 对于字符串 `ababc` 来说,它的前缀有 `[a,ab,aba,abab]`,也就是以字符串第一个字符作为开头,同时不包括最后一个字符的所有子串。 同理它的后缀有 `[c,bc,abc,babc]`,也就是以字符串最后一个字符作为结尾,同时不包括第一个字符的所有字串。 #### 3.3 字符串的最长公共前后缀 了解了这个,我们再来说什么是字符串的**最长公共前后缀**,说白了,也就是前缀和后缀这 2 个集合中的相同部分,同时取最长的那个,就是这个字符串的**最长公共前后缀**。 显然,在这个例子中,`ababc` 是没有公共前后缀的。但是对于 `abab`,它的前缀和后缀分别是 `[a,ab,aba]` 和 `[b,ab,bab]`,那么它的**最长公共前后缀**就是 `ab`。 现在,我们的目标就是取得 `ababc` 所有子串 `[a,ab,aba,abab,ababc]` 的**最长公共前后缀**的长度,分别保存在 `next` 数组中,我们只要知道最大长度即可,并不用关心串具体是什么,而我们目前通过观察即可得出 next 数组这里是 `[0,0,1,2,0]`,我们先知道这个结果即可,此计算方法后续会说明。 #### 3.4 KMP的思路分析 + 得到这个有什么用,回到刚刚的例子,当我们第一次碰到不匹配时(i 和 j 都等于 3 的时候): ![01.golang算法篇之kmp算法介绍07.jpg](https://lilinchao.com/usr/uploads/2024/04/908585634.jpg) 此时 i 和 j 之前的 3 个字符 `aba` 必然完全匹配的,因为只有前面匹配成功了 j 才能走到 3,而我们又知道 `aba` 的**最长公共前后缀**是 `a`。 这可以给我们一个很好的提示,**主串中的 `aba` 的后缀和字串中的 `aba` 前缀有最长的公共部分 `a`**,这样一来,我们就没必要重新比较了,直接将相同部分对齐就好了 + 也就是说让 j 退回到位置 1 就可以了,而 i 保持不变。 ![01.golang算法篇之kmp算法介绍08.jpg](https://lilinchao.com/usr/uploads/2024/04/1815594506.jpg) **分析一下,为什么 j 知道应该退回到位置 1**: 1. 我们知道 j 是在位置 3 不匹配的,那么 j 前面的字符串我们可以知道是 `aba` 2. 前面我们已经得到了 next 数组,知道 `aba` 的最长公共前后缀长度是 **1** 3. 也就是说,我们可以知道 i 之前 **1** 个位置(主串的后缀)和子串开头之后 **1** 个位置(子串的前缀)是相同的 4. 进而我们可以让相同部分对齐,也就是让 `j=next[j-1]` + 接下来,我们发现 i 和 j 仍然不匹配,而 j 之前的字符 `a` 最长公共前后缀是 0,此时 j 退到位置 0,i 仍然保持不变。 ![01.golang算法篇之kmp算法介绍09.jpg](https://lilinchao.com/usr/uploads/2024/04/723866639.jpg) + 接下来 i 和 j 匹配,同时右移一格 ![01.golang算法篇之kmp算法介绍10.jpg](https://lilinchao.com/usr/uploads/2024/04/4192324411.jpg) + 此时 i 和 j 不匹配,`j=next[j-1]` 回退到 0,然后我们发现 i 和 j 仍然不匹配,同时 j 已经是 0 了,那么我们就让 i 往右移动一格。 ![01.golang算法篇之kmp算法介绍11.jpg](https://lilinchao.com/usr/uploads/2024/04/1733381184.jpg) ![01.golang算法篇之kmp算法介绍12.jpg](https://lilinchao.com/usr/uploads/2024/04/1344232605.jpg) 可以看到,相比于暴力解法,i 始终在前进,并没有后退(顶多保持不变),然后我们通过 next 数组来改变 j 的值。 #### 3.5 求解字符串最长公共前后缀 最后,我们还剩下一个问题,如何求 next 数组,也就是求模式字符串各个子串的最长公共前后缀长度。 我们先初始化一个和模式字符串长度相等的 next 数组,在 next 数组中,第 1 位默认为 0,因为我们规定只有一个字符的字符串没有前缀和后缀,自然公共前后缀长度只能是 0。 我们依然设定 2 个指针 i 和 j,j 指向位置 0,i 从位置 1 开始依次为 next 数组进行赋值 ![01.golang算法篇之kmp算法介绍13.jpg](https://lilinchao.com/usr/uploads/2024/04/858115632.jpg) 我们可以把这个过程依然看作是 2 个字符串的比较,j 指向的是模式字符串的前缀,而 i 指向的是模式字符串的后缀 ![01.golang算法篇之kmp算法介绍14.jpg](https://lilinchao.com/usr/uploads/2024/04/4017240683.jpg) 和上面的字符串匹配一样,我们执行同样的处理: 1. 当 i 和 j 匹配时,i 和 j 同时右移一格 2. 当 i 和 j 不匹配时,如果 j 不在字符串开头(位置 0),就回退到上一个能匹配到的位置 3. 当 i 和 j 不匹配时,如果 j 在字符串开头(位置 0),那么 i 就右移一格 **对 next [1] 赋值**:i 和 j 不匹配,同时 j 已经是字符串开头,赋值 0 ![01.golang算法篇之kmp算法介绍15.jpg](https://lilinchao.com/usr/uploads/2024/04/300407991.jpg) 对 next [2] 赋值,i 和 j 匹配,此时 j 为 0,代表只有 1 个字符匹配 (j+1),赋值 1 ![01.golang算法篇之kmp算法介绍16.jpg](https://lilinchao.com/usr/uploads/2024/04/801515162.jpg) 对 next [3] 赋值,i 和 j 匹配,此时 j 为 1,代表有 2 个字符匹配 (j+1),赋值 2 ![01.golang算法篇之kmp算法介绍17.jpg](https://lilinchao.com/usr/uploads/2024/04/2680420409.jpg) 对 next [4] 赋值,i 和 j 不匹配,此时 j 为 2,可以得知 j 前面的字符是 `ab`,而 `ab` 的最长公共前后缀长度就是 `next[1]`,这个值我们前面已经求得了结果是 0,所以 j 回退到位置 0,用代码表示就是 `j=next[j-1]` ![01.golang算法篇之kmp算法介绍18.jpg](https://lilinchao.com/usr/uploads/2024/04/926286784.jpg) 此时 i 和 j 仍然不匹配,但是 j 已为 0,无法继续回退,所以直接对 next [4] 赋值为 0 ![01.golang算法篇之kmp算法介绍19.jpg](https://lilinchao.com/usr/uploads/2024/04/33397438.jpg) 实际上,我们在求解模式字符串 `ababc` 的最长公共前后缀长度的时候,不可避免的会对它的子串进行求解,这样可以方便在不匹配时进行回退,这也是动态规划的思想,要求的结果可以用先前已经求得的结果得出。而我们本身就是要对模式字符串的各个子串进行求解,所以可谓一举两得。 #### 3.6 代码实现 ```go // 方法二 KMP算法 // 此函数用来初始化next数组 func initNext(needle string) []int { //记录后缀中末尾 abc中c i := 1 //前缀中末尾;同时在这里也有着记录最长公共串长度的作用,二者本质是一样的 j := 0 L := len(needle) //初始化next数组;next[0]默认为0,因为对于一个字母我们不认为其具有前后缀,后续也不会再对next[0]进行赋值 next := make([]int, L) //求next数组过程中,我们的i不回退,采用类似于动态规划的思想,也是我们这里的循环条件 for i < L { //如果前后缀匹配 if needle[i] == needle[j] { // 如果字符串对应的编码值相同 //前缀末尾向后移一位,同时代表长度+1 j++ //当前后缀末尾所在位置的最长子串即为j //最长子串是有基础的,如果next[2]=2,那么next[3]的可能性为3或者0,这里是为3的情况 next[i] = j //后缀末尾向后移一位 i++ } else { //如果前后缀不匹配 //当j>0,说明仍旧处于回退的过程 if j > 0 { j = next[j-1] } else { //如果j=0,并且前后缀依旧不匹配,则长度计数应该重新从0开始 //这里是为0的情况 next[i] = j //后缀末尾向后移 i++ } } } //返回next数组 return next } // kmp算法,用空间换时间 func strStr(haystack string, needle string) int { //获取next数组 next := initNext(needle) //主串长度 L := len(haystack) //目标匹配长度,即needle的长度 target := len(needle) //匹配字符串中指针位置 j := 0 //i为主串中指针的位置 for i := 0; i < L; { // 如果匹配上了 if haystack[i] == needle[j] { if j == target-1 { return i - target + 1 } j++ i++ } else { //如果没匹配上 //跟计算next数组有异曲同工之妙 if j > 0 { j = next[j-1] } else { i++ } } } return -1 } func main() { str1and := "aaaabbb" str2and := "abb" num := strStr(str1and, str2and) fmt.Println(num) } ``` ### 四、题目二 #### 4.1 题目描述 **LeetCode 459. 重复的子字符串** 给定一个非空的字符串 `s` ,检查是否可以通过由它的一个子串重复多次构成。 **示例 1:** ``` 输入: s = "abab" 输出: true 解释: 可由子串 "ab" 重复两次构成。 ``` **示例 2:** ``` 输入: s = "aba" 输出: false ``` **示例 3:** ``` 输入: s = "abcabcabcabc" 输出: true 解释: 可由子串 "abc" 重复四次构成。 (或子串 "abcabc" 重复两次构成。) ``` #### 4.2 代码实现 ```go /** 方法一:字符串拼接法 思路 新建一个新字符串str=s+s,把str的首元素和尾元素去掉,剩下的部分如果还含有s,则返回true。 */ func repeatedSubstringPattern2and(s string) bool { var str1 string = s + s var str2 string = str1[1 : len(str1)-1] // 去掉首/尾字母 if strings.Contains(str2, s) { return true } else { return false } } func repeatedSubstringPattern3and(s string) bool { var str1 string = s + s var str2 string = str1[1 : len(str1)-1] // 去掉首/尾字母 return strings.Contains(str2, s) } func repeatedSubstringPattern4and(s string) bool { var str1 string = s + s return strings.Contains(str1[1:len(str1)-1], s) } /** 方法二 暴力解法 */ func repeatedSubstringPattern5and(s string) bool { L := len(s) record := false for i := 1; i < L/2+1; i++ { if L%i != 0 { continue } num := L / i for ; num > 0; num-- { if s[:i] != s[(num-1)*i:num*i] { record = false break } record = true } if record == true { return record } } return record } /** KMP算法 正是因为 最长相等前后缀的规则,当一个字符串由重复子串组成的,最长相等前后缀不包含的子串就是最小重复子串。 */ // 前缀表(不减一)的代码实现 func repeatedSubstringPattern6and(s string) bool { n := len(s) if n == 0 { return false } j := 0 // 初始化 next 数组 // next 数组中存储最长公共后缀部分,跳过第一个公共前缀项 next := make([]int, n) next[0] = j // for i := 1; i < n; i++ { // 匹配不成功,j回到前一位置 next数组所对应的值 for j > 0 && s[i] != s[j] { j = next[j-1] } // 匹配成功,j忘后移 if s[i] == s[j] { j++ } // 更新 next 数组的值 next[i] = j } // 最后判断是否是重复的子字符串 // next[n-1] 最长相同前后缀的长度 // n-next[n-1] === n - 最长相同前后缀长度 == 最小相同前后缀长度 // next[n-1] != 0 字符串中存在相同前后缀 if next[n-1] != 0 && n%(n-next[n-1]) == 0 { return true } return false } // 这里使用前缀表统一减一的实现方式 func repeatedSubstringPattern7and(s string) bool { n := len(s) if n == 0 { return false } next := make([]int, n) j := -1 next[0] = j for i := 1; i < n; i++ { for j >= 0 && s[i] != s[j+1] { j = next[j] } if s[i] == s[j+1] { j++ } next[i] = j } // next[n-1]+1 最长相同前后缀的长度 if next[n-1] != -1 && n%(n-(next[n-1]+1)) == 0 { return true } return false } func main() { str := "abcabcabc" //str2 := "abcabcabca" bl := repeatedSubstringPattern6and(str) //blr := repeatedSubstringPattern4and(str2) fmt.Println(bl) //fmt.Println(blr) } ``` *附参考原文链接地址* *https://www.xdull.cn/kmp.html*
标签:
Golang
非特殊说明,本博所有文章均为博主原创。
如若转载,请注明出处:
https://www.lilinchao.com/archives/2898.html
上一篇
golang之gorm操作示例并兼容达梦数据库
下一篇
golang算法篇之0-1背包算法介绍
评论已关闭
栏目分类
随笔
2
Java
326
大数据
229
工具
31
其它
25
GO
47
NLP
2
标签云
Hadoop
查找
Azkaban
Typora
数据结构和算法
线程池
Scala
字符串
MyBatis
Elastisearch
HDFS
Redis
Beego
Flume
nginx
FastDFS
Jenkins
NIO
Eclipse
SpringBoot
VUE
Spark RDD
Kibana
BurpSuite
SpringCloud
ajax
Python
CentOS
Java工具类
随笔
友情链接
申请
范明明
庄严博客
Mx
陶小桃Blog
虫洞
评论已关闭