diff --git a/README.md b/README.md index 2b6412aa24..aba247371f 100644 --- a/README.md +++ b/README.md @@ -50,8 +50,11 @@ * [字符串:简单的反转还不够!](https://mp.weixin.qq.com/s/XGSk1GyPWhfqj2g7Cb1Vgw) * [字符串:替换空格](https://mp.weixin.qq.com/s/t0A9C44zgM-RysAQV3GZpg) * [字符串:花式反转还不够!](https://mp.weixin.qq.com/s/X3qpi2v5RSp08mO-W5Vicw) +* [字符串:反转个字符串还有这个用处?](https://mp.weixin.qq.com/s/PmcdiWSmmccHAONzU0ScgQ) * [字符串:KMP是时候上场了(一文读懂系列)](https://mp.weixin.qq.com/s/70OXnZ4Ez29CKRrUpVJmug) * [字符串:都来看看KMP的看家本领!](https://mp.weixin.qq.com/s/Gk9FKZ9_FSWLEkdGrkecyg) +* [字符串:听说你对KMP有这些疑问?](https://mp.weixin.qq.com/s/mqx6IM2AO4kLZwvXdPtEeQ) +* [字符串:KMP算法还能干这个!](https://mp.weixin.qq.com/s/lR2JPtsQSR2I_9yHbBmBuQ) (持续更新中....) diff --git "a/pics/347.\345\211\215K\344\270\252\351\253\230\351\242\221\345\205\203\347\264\240.png" "b/pics/347.\345\211\215K\344\270\252\351\253\230\351\242\221\345\205\203\347\264\240.png" new file mode 100644 index 0000000000..0d665000fc Binary files /dev/null and "b/pics/347.\345\211\215K\344\270\252\351\253\230\351\242\221\345\205\203\347\264\240.png" differ diff --git "a/pics/39.\347\273\204\345\220\210\346\200\273\345\222\214.png" "b/pics/39.\347\273\204\345\220\210\346\200\273\345\222\214.png" new file mode 100644 index 0000000000..29db134f0a Binary files /dev/null and "b/pics/39.\347\273\204\345\220\210\346\200\273\345\222\214.png" differ diff --git "a/pics/77.\347\273\204\345\220\210.png" "b/pics/77.\347\273\204\345\220\210.png" new file mode 100644 index 0000000000..17cde49318 Binary files /dev/null and "b/pics/77.\347\273\204\345\220\210.png" differ diff --git "a/problems/0028.\345\256\236\347\216\260strStr().md" "b/problems/0028.\345\256\236\347\216\260strStr().md" index efbe050a49..8e26e6efeb 100644 --- "a/problems/0028.\345\256\236\347\216\260strStr().md" +++ "b/problems/0028.\345\256\236\347\216\260strStr().md" @@ -240,6 +240,46 @@ public: } }; +``` + +前缀表不减一版本 +``` +class Solution { +public: + void getNext(int* next, const string& s) { + int j = 0; + next[0] = 0; + for(int i = 1; i < s.size(); i++) { + while (j > 0 && s[i] != s[j]) { + j = next[j - 1]; + } + if (s[i] == s[j]) { + j++; + } + next[i] = j; + } + } + int strStr(string haystack, string needle) { + if (needle.size() == 0) { + return 0; + } + int next[needle.size()]; + getNext(next, needle); + int j = 0; + for (int i = 0; i < haystack.size(); i++) { + while(j > 0 && haystack[i] != needle[j]) { + j = next[j - 1]; + } + if (haystack[i] == needle[j]) { + j++; + } + if (j == needle.size() ) { + return (i - needle.size() + 1); + } + } + return -1; + } +}; ``` > 更多算法干货文章持续更新,可以微信搜索「代码随想录」第一时间围观,关注后,回复「Java」「C++」 「python」「简历模板」「数据结构与算法」等等,就可以获得我多年整理的学习资料。 diff --git "a/problems/0039.\347\273\204\345\220\210\346\200\273\345\222\214.md" "b/problems/0039.\347\273\204\345\220\210\346\200\273\345\222\214.md" index ec89c132d6..db303fc562 100644 --- "a/problems/0039.\347\273\204\345\220\210\346\200\273\345\222\214.md" +++ "b/problems/0039.\347\273\204\345\220\210\346\200\273\345\222\214.md" @@ -30,36 +30,74 @@ candidates 中的数字可以无限制重复被选取。 # 思路 +题目中的**无限制重复被选取,吓得我赶紧想想 出现0 可咋办**,然后看到下面提示:1 <= candidates[i] <= 200,我就放心了。 + + +这道题上来可以这么想,看看一个数能不能构成target,一个for循环遍历一遍,再看看两个数能不能构成target,两个for循环遍历,在看看三个数能不能构成target,三个for循环遍历,直到candidates.size()个for循环遍历一遍。 + +遇到这种问题,就要想到递归的层级嵌套关系就可以解决这种多层for循环的问题,而回溯则帮我们选择每一个合适的集合! + +那么使用回溯的时候,要知道求的是排列,还是组合,排列和组合是不一样的。 + +一些同学可能海分不清,我大概说一下: + +**组合是不强调元素顺序的,排列是强调元素顺序的。** + +例如 集合 1,2 和 集合 2,1 在组合上,就是一个集合,因为不强调顺序,而要是排列的话,1,2 和 2,1 就是两个集合了。 + +**求组合,和求排列的回溯写法是不一样的,代码上有小小细节上的改变。** + +本题选组过程如下: + + + + +分析完过程,回溯算法的模板框架如下: +``` +backtracking() { + if (终止条件) { + 存放结果; + } + + for (选择:选择列表(可以想成树中节点孩子的数量)) { + 递归,处理节点; + backtracking(); + 回溯,撤销处理结果 + } +} +``` + +按照模板不难写出如下代码,但很一些细节,我在注释中标记出来了。 + # C++代码 ``` -// 无限制重复被选取。 吓得我赶紧想想 0 可咋办 class Solution { private: vector> result; - void backtracking(vector& candidates, int target, vector& vec, int sum, int startIndex) { + vector path; + void backtracking(vector& candidates, int target, int sum, int startIndex) { if (sum > target) { return; } if (sum == target) { - result.push_back(vec); + result.push_back(path); return; } - - // 因为可重复,所以我们从0开始, 这道题目感觉像是47.全排列II,其实不是 + + // 这里i 依然从 startIndex开始,因为求的是组合,如果求的是排列,那么i每次都从0开始 for (int i = startIndex; i < candidates.size(); i++) { sum += candidates[i]; - vec.push_back(candidates[i]); - backtracking(candidates, target, vec, sum, i); // 关键点在这里,不用i+1了 + path.push_back(candidates[i]); + backtracking(candidates, target, sum, i); // 关键点在这里,不用i+1了,表示可以重复读取当前的数 sum -= candidates[i]; - vec.pop_back(); + path.pop_back(); } } public: vector> combinationSum(vector& candidates, int target) { - vector vec; - backtracking(candidates, target, vec, 0, 0); + backtracking(candidates, target, 0, 0); return result; } }; diff --git "a/problems/0077.\347\273\204\345\220\210.md" "b/problems/0077.\347\273\204\345\220\210.md" index 2000e9566e..6400a75ce0 100644 --- "a/problems/0077.\347\273\204\345\220\210.md" +++ "b/problems/0077.\347\273\204\345\220\210.md" @@ -15,6 +15,81 @@ # 思路 +这是回溯法的经典题目。 + +直觉上当然是使用for循环,例如示例中k为2,很容易想到 用两个for循环,这样就可以输出 和示例中一样的结果。 + +代码如下: +``` + int n = 4; + for (int i = 1; i <= n; i++) { + for (int j = i + 1; j <= n; j++) { + cout << i << " " << j << endl; + } + } +``` + +输入:n = 100, k = 3 +那么就三层for循环,代码如下: + +``` + for (int i = 1; i <= n; i++) { + for (int j = i + 1; j <= n; j++) { + for (int u = j + 1; u <=n; n++) { + + } + } + } +``` +**如果n为 100,k为50呢,那就50层for循环,是不是开始窒息。** + +那么回溯法就能解决这个问题了。 + +回溯是用来做选择的,递归用来节点层叠嵌套,**每一次的递归是层叠嵌套的关系,可以用于解决多层嵌套循环的问题。** + +其实子集和组合问题都可以抽象为一个树形结构,如下: + + + + +可以看一下这个棵树,一开始集合是 1,2,3,4, 从左向右去数,取过的数,不在重复取。 + +第一取1,集合变为2,3,4 ,因为k为2,我们只需要去一个数就可以了,分别取,2,3,4, 得到集合[1,2] [1,3] [1,4],以此类推。 + +**其实这就转化成从集合中选取子集的问题,可选择的范围随着选择的进行而限缩,于是做剪枝,调整可选择的范围** + +如何在这个树上遍历,然后收集到我们要的结果集呢,用的就是回溯搜索法,**可以发现,每次搜索到了叶子节点,我们就找到了一个结果。** + +分析完过程,我们来看一下 回溯算法的模板框架如下: +``` +backtracking() { + if (终止条件) { + 存放结果; + } + + for (选择:选择列表(可以想成树中节点孩子的数量)) { + 递归,处理节点; + backtracking(); + 回溯,撤销处理结果 + } +} +``` + +分析模板: + +什么是达到了终止条件,树中就可以看出,搜到了叶子节点了,就找到了一个符合题目要求的答案,就把这个答案存放起来。 + +看一下这个for循环,这个for循环是做什么的,for 就是处理树中节点各个孩子的情况, 一个节点有多少个孩子,这个for循环就执行多少次。 + +最后就要看这个递归的过程了,注意这个backtracking就是自己调用自己,实现递归。 + +一些同学对递归操作本来就不熟练,递归上面又加上一个for循环,可能就更迷糊了, 我来给大家捋顺一下。 + +这个backtracking 其实就是向树的叶子节点方向遍历, for循环可以理解是横向遍历,backtracking 就是纵向遍历,这样就把这棵树全遍历完了。 + +那么backtracking就是一直往深处遍历,总会遇到叶子节点,遇到了叶子节点,就要返回,那么backtracking的下面部分就是回溯的操作了,撤销本次处理的结果。 + +分析完模板,本题代码如下: # C++ 代码 @@ -22,17 +97,17 @@ class Solution { private: vector> result; // 存放符合条件结果的集合 - vector vec; // 用来存放符合条件结果 + vector path; // 用来存放符合条件结果 void backtracking(int n, int k, int startIndex) { - if (vec.size() == k) { - result.push_back(vec); + if (path.size() == k) { + result.push_back(path); return; } // 这个for循环有讲究,组合的时候 要用startIndex,排列的时候就要从0开始 for (int i = startIndex; i <= n; i++) { - vec.push_back(i); + path.push_back(i); // 处理节点 backtracking(n, k, i + 1); - vec.pop_back(); + path.pop_back(); // 回溯,撤销处理的节点 } } public: @@ -43,3 +118,62 @@ public: } }; ``` + +## 剪枝优化 + +在遍历的过程中如下代码 : + +``` +for (int i = startIndex; i <= n; i++) +``` + +这个遍历的范围是可以剪枝优化的,怎么优化呢? + +来举一个例子,n = 4, k = 4的话,那么从2开始的遍历都没有意义了。 + +已经选择的元素个数:path.size(); + +要选择的元素个数 : k - path.size(); + +在集合n中开始选择的起始位置 : n - (k - path.size()); + +因为起始位置是从1开始的,而且代码里是n <= 起始位置,所以 集合n中开始选择的起始位置 : n - (k - path.size()) + 1; + +所以优化之后是: + +``` +for (int i = startIndex; i <= n - (k - path.size()) + 1; i++) +``` + +整体代码如下: + +``` +class Solution { +private: + vector> result; // 存放符合条件结果的集合 + vector path; // 用来存放符合条件结果 + void backtracking(int n, int k, int startIndex) { + if (path.size() == k) { + result.push_back(path); + return; + } + // 这个for循环有讲究,组合的时候 要用startIndex,排列的时候就要从0开始 + for (int i = startIndex; i <= n - (k - path.size()) + 1; i++) { + path.push_back(i); // 处理节点 + backtracking(n, k, i + 1); + path.pop_back(); // 回溯,撤销处理的节点 + } + } +public: + + vector> combine(int n, int k) { + backtracking(n, k, 1); + return result; + } +}; +``` + + + +# 观后感 +我来写一下观后感: 很厉害,转化成从集合中选取子集的问题,可选择的范围随着选择的进行而限缩,于是做剪枝,调整可选择的范围。 每一次的递归是层叠嵌套的关系,可以用于解决多层嵌套循环的问题。 每一层递归中,尽量节省循环次数,这样在后续的递归调用中,节省下来的循环会被以至少指数等级放大。 diff --git "a/problems/0459.\351\207\215\345\244\215\347\232\204\345\255\220\345\255\227\347\254\246\344\270\262.md" "b/problems/0459.\351\207\215\345\244\215\347\232\204\345\255\220\345\255\227\347\254\246\344\270\262.md" index 4c9be6e8dc..64e39c0a8a 100644 --- "a/problems/0459.\351\207\215\345\244\215\347\232\204\345\255\220\345\255\227\347\254\246\344\270\262.md" +++ "b/problems/0459.\351\207\215\345\244\215\347\232\204\345\255\220\345\255\227\347\254\246\344\270\262.md" @@ -2,11 +2,41 @@ ## 题目地址 https://leetcode-cn.com/problems/repeated-substring-pattern/ -## 思路 +> KMP算法还能干这个 -这是一道标准的KMP的题目。 +# 题目459.重复的子字符串 -使用KMP算法,next 数组记录的就是最长公共前后缀, 最后如果 next[len - 1] != -1,说明此时有最长公共前后缀(就是字符串里的前缀子串和后缀子串相同的最长长度),同时如果len % (len - (next[len - 1] + 1)) == 0 ,则说明 (数组长度-最长公共前后缀的长度) 正好可以被 数组的长度整除,说明有重复的子字符串。 +给定一个非空的字符串,判断它是否可以由它的一个子串重复多次构成。给定的字符串只含有小写英文字母,并且长度不超过10000。 + +示例 1: +输入: "abab" +输出: True +解释: 可由子字符串 "ab" 重复两次构成。 + +示例 2: +输入: "aba" +输出: False + +示例 3: +输入: "abcabcabcabc" +输出: True +解释: 可由子字符串 "abc" 重复四次构成。 (或者子字符串 "abcabc" 重复两次构成。) + +# 思路 + +这又是一道标准的KMP的题目。 + +我们在[字符串:都来看看KMP的看家本领!](https://mp.weixin.qq.com/s/Gk9FKZ9_FSWLEkdGrkecyg)里提到了,在一个串中查找是否出现过另一个串,这是KMP的看家本领。 + +那么寻找重复子串怎么也涉及到KMP算法了呢? + +这里就要说一说next数组了,next 数组记录的就是最长相同前后缀( [字符串:听说你对KMP有这些疑问?](https://mp.weixin.qq.com/s/mqx6IM2AO4kLZwvXdPtEeQ) 这里介绍了什么是前缀,什么是后缀,什么又是最长相同前后缀), 如果 next[len - 1] != -1,则说明字符串有最长相同的前后缀(就是字符串里的前缀子串和后缀子串相同的最长长度)。 + +最长相等前后缀的长度为:next[len - 1] + 1。 + +数组长度为:len。 + +如果len % (len - (next[len - 1] + 1)) == 0 ,则说明 (数组长度-最长相等前后缀的长度) 正好可以被 数组的长度整除,说明有该字符串有重复的子字符串。 **强烈建议大家把next数组打印出来,看看next数组里的规律,有助于理解KMP算法** @@ -14,44 +44,87 @@ https://leetcode-cn.com/problems/repeated-substring-pattern/ -此时next[len - 1] = 7,next[len - 1] + 1 = 8,8就是此时 字符串asdfasdfasdf的最长公共前后缀的长度。 +此时next[len - 1] = 7,next[len - 1] + 1 = 8,8就是此时 字符串asdfasdfasdf的最长相同前后缀的长度。 -(len - (next[len - 1] + 1)) 也就是: 12(字符串的长度) - 8(最长公共前后缀的长度) = 4, 4正好可以被 12(字符串的长度) 整除,所以说明有重复的子字符串。 +(len - (next[len - 1] + 1)) 也就是: 12(字符串的长度) - 8(最长公共前后缀的长度) = 4, 4正好可以被 12(字符串的长度) 整除,所以说明有重复的子字符串(asdf)。 代码如下: -## C++代码 - +# C++代码 ``` class Solution { public: - void preKmp(int* next, const string& s){ + // KMP里标准构建next数组的过程 + void getNext (int* next, const string& s){ next[0] = -1; int j = -1; for(int i = 1;i < s.size(); i++){ - while(j >= 0 && s[i] !=s [j+1]) + while(j >= 0 && s[i] != s[j+1]) { j = next[j]; - if(s[i] == s[j+1]) + } + if(s[i] == s[j+1]) { j++; + } next[i] = j; } } - bool repeatedSubstringPattern(string s) { + bool repeatedSubstringPattern (string s) { if (s.size() == 0) { return false; } int next[s.size()]; - preKmp(next, s); + getNext(next, s); int len = s.size(); if (next[len - 1] != -1 && len % (len - (next[len - 1] + 1)) == 0) { return true; } return false; + } +}; +``` + +# next减一C++代码 +``` +class Solution { +public: + // KMP里标准构建next数组的过程 + void getNext (int* next, const string& s){ + next[0] = 0; + int j = 0; + for(int i = 1;i < s.size(); i++){ + while(j > 0 && s[i] != s[j]) { + j = next[j - 1]; + } + if(s[i] == s[j]) { + j++; + } + next[i] = j; + } + } + bool repeatedSubstringPattern (string s) { + if (s.size() == 0) { + return false; + } + int next[s.size()]; + getNext(next, s); + int len = s.size(); + if (next[len - 1] != 0 && len % (len - (next[len - 1] )) == 0) { + return true; + } + return false; } }; ``` -> 更过算法干货文章持续更新,可以微信搜索「代码随想录」第一时间围观,关注后,回复「Java」「C++」 「python」「简历模板」「数据结构与算法」等等,就可以获得我多年整理的学习资料。 +# 拓展 + +此时我们已经分享了三篇KMP的文章,首先是[字符串:KMP是时候上场了(一文读懂系列)](https://mp.weixin.qq.com/s/70OXnZ4Ez29CKRrUpVJmug)讲解KMP算法的基础理论,给出next数组究竟是如何来了,前缀表又是怎么回事,为什么要选择前缀表。 + +然后通过[字符串:都来看看KMP的看家本领!](https://mp.weixin.qq.com/s/Gk9FKZ9_FSWLEkdGrkecyg)讲解一道KMP的经典题目,判断文本串里是否出现过模式串,这里涉及到构造next数组的代码实现,以及使用next数组完成模式串与文本串的匹配过程。 + +后来很多同学反馈说:搞不懂前后缀,什么又是最长相同前后缀(最长公共前后缀我认为这个用词不准确),以及为什么前缀表要统一减一(右移)呢,不减一行不行?针对这些问题,我在[字符串:听说你对KMP有这些疑问?](https://mp.weixin.qq.com/s/mqx6IM2AO4kLZwvXdPtEeQ)中又给出了详细的讲解。 + +> 更多算法干货文章持续更新,可以微信搜索「代码随想录」第一时间围观,关注后,回复「Java」「C++」 「python」「简历模板」「数据结构与算法」等等,就可以获得我多年整理的学习资料。 diff --git "a/problems/\345\255\227\347\254\246\344\270\262\346\200\273\347\273\223.md" "b/problems/\345\255\227\347\254\246\344\270\262\346\200\273\347\273\223.md" new file mode 100644 index 0000000000..26387cb72a --- /dev/null +++ "b/problems/\345\255\227\347\254\246\344\270\262\346\200\273\347\273\223.md" @@ -0,0 +1,95 @@ +# 字符串:帮你对字符串不再恐惧(总结篇) + + +# 什么是字符串 + +字符串是若干字符组成的有限序列,也可以理解为是一个字符数组,但是很多语言对字符串做了特殊的规定,接下来我来说一说C/C++中的字符串。 + +在C语言中,把一个字符串存入一个数组时,也把结束符 '\0'存入数组,并以此作为该字符串是否结束的标志。 + +例如这段代码: + +``` +char a[5] = "asd"; +for (int i = 0; a[i] != '\0'; i++) { +} +``` + +在C++中,提供一个string类,string类会提供 size接口,可以用来判断string类字符串是否结束,就不用'\0'来判断是否结束。 + +例如这段代码: + +``` +string a = "asd"; +for (int i = 0; i < a.size(); i++) { +} +``` + +那么vector< char > 和 string 又有什么区别呢? + +其实在基本操作上没有区别,但是 string提供更多的字符串处理的相关接口,例如string 重载了+,而vector却没有。 + +所以想处理字符串,我们还是会定义一个string类型。 + +# 要不要使用库函数 + +在文章[字符串:这道题目,使用库函数一行代码搞定](https://mp.weixin.qq.com/s/X02S61WCYiCEhaik6VUpFA)中强调了**打基础的时候,不要太迷恋于库函数。** + +甚至一些同学习惯于调用substr,split,reverse之类的库函数,却不知道其实现原理,也不知道其时间复杂度,这样实现出来的代码,如果在面试面试现场,面试官问:“分析其时间复杂度”的话,一定会一脸懵逼! + +所以我们建议**如果题目关键的部分直接用库函数就可以解决,建议不要使用库函数。** + +**如果库函数仅仅是 解题过程中的一小部分,并且你已经很清楚这个库函数的内部实现原理的话,可以考虑使用库函数。** + +# 双指针法 + + +在[字符串:这道题目,使用库函数一行代码搞定](https://mp.weixin.qq.com/s/X02S61WCYiCEhaik6VUpFA) ,我们使用双指针法实现了反转字符串的操作,双指针法在数组,链表和字符串中很常用。 + +双指针法在数组,链表,字符串操作中,经常会使用双指针法。 + +接着在[字符串:替换空格](https://mp.weixin.qq.com/s/t0A9C44zgM-RysAQV3GZpg),同样还是使用双指针法在时间复杂度O(n)的情况下完成替换空格。 + +**其实很多数组填充类的问题,都可以先预先给数组扩容带填充后的大小,然后在从后向前进行操作。** + + +# 反转系列 + +在反转上还可以在加一些玩法,其实考察的是对代码的掌控能力。 + +[字符串:简单的反转还不够!](https://mp.weixin.qq.com/s/XGSk1GyPWhfqj2g7Cb1Vgw)中,一些同学可能为了处理逻辑:每隔2k个字符的前k的字符,写了一堆逻辑代码或者再搞一个计数器,来统计2k,再统计前k个字符。 + +其实**当需要固定规律一段一段去处理字符串的时候,要想想在在for循环的表达式上做做文章**。 + +只要让 i += (2 * k),i 每次移动 2 * k 就可以了,然后判断是否需要有反转的区间。 + +因为要找的也就是每2 * k 区间的起点,这样写程序会高效很多。 + +在[字符串:花式反转还不够!](https://mp.weixin.qq.com/s/X3qpi2v5RSp08mO-W5Vicw)中要求翻转字符串里的单词,这道题目可以说是综合考察了字符串的多种操作。是考察字符串的好题。 + +这道题目通过 **先整体反转再局部反转**,实现了反转字符串里的单词。 + +后来发现反转字符串还有一个牛逼的用处,就是达到左旋的效果。 + +在[字符串:反转个字符串还有这个用处?](https://mp.weixin.qq.com/s/PmcdiWSmmccHAONzU0ScgQ)中,我们通过**先局部反转再整体反转**达到了左旋的效果。 + +# KMP + +KMP的主要思想是「当出现字符串不匹配时,可以知道一部分之前已经匹配的文本内容,可以利用这些信息避免从头再去做匹配了。」 + +首先要理解KMP的理论基础,[字符串:KMP是时候上场了(一文读懂系列)](https://mp.weixin.qq.com/s/70OXnZ4Ez29CKRrUpVJmug),这里提到了,什么是KMP,什么是前缀表,以及为什么要用前缀表 + + + +打基础的时候,不要太迷恋于库函数 + +* [字符串:反转个字符串还有这个用处?](https://mp.weixin.qq.com/s/PmcdiWSmmccHAONzU0ScgQ) + +* [字符串:这道题目,使用库函数一行代码搞定](https://mp.weixin.qq.com/s/X02S61WCYiCEhaik6VUpFA) +* [字符串:简单的反转还不够!](https://mp.weixin.qq.com/s/XGSk1GyPWhfqj2g7Cb1Vgw) +* [字符串:替换空格](https://mp.weixin.qq.com/s/t0A9C44zgM-RysAQV3GZpg) +* [字符串:花式反转还不够!](https://mp.weixin.qq.com/s/X3qpi2v5RSp08mO-W5Vicw) +* [字符串:KMP是时候上场了(一文读懂系列)](https://mp.weixin.qq.com/s/70OXnZ4Ez29CKRrUpVJmug) +* [字符串:都来看看KMP的看家本领!](https://mp.weixin.qq.com/s/Gk9FKZ9_FSWLEkdGrkecyg) +* [字符串:听说你对KMP有这些疑问?](https://mp.weixin.qq.com/s/mqx6IM2AO4kLZwvXdPtEeQ) +* [字符串:KMP算法还能干这个!](https://mp.weixin.qq.com/s/lR2JPtsQSR2I_9yHbBmBuQ)