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)