This commit is contained in:
youngyangyang04
2020-09-09 10:26:04 +08:00
parent d61b7d8811
commit 88a3910533
9 changed files with 412 additions and 29 deletions

View File

@ -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」「简历模板」「数据结构与算法」等等就可以获得我多年整理的学习资料。

View File

@ -30,36 +30,74 @@ candidates 中的数字可以无限制重复被选取。
# 思路
题目中的**无限制重复被选取,吓得我赶紧想想 出现0 可咋办**然后看到下面提示1 <= candidates[i] <= 200我就放心了。
这道题上来可以这么想看看一个数能不能构成target一个for循环遍历一遍再看看两个数能不能构成target两个for循环遍历在看看三个数能不能构成target三个for循环遍历直到candidates.size()个for循环遍历一遍。
遇到这种问题就要想到递归的层级嵌套关系就可以解决这种多层for循环的问题而回溯则帮我们选择每一个合适的集合
那么使用回溯的时候,要知道求的是排列,还是组合,排列和组合是不一样的。
一些同学可能海分不清,我大概说一下:
**组合是不强调元素顺序的,排列是强调元素顺序的。**
例如 集合 12 和 集合 21 在组合上就是一个集合因为不强调顺序而要是排列的话12 和 21 就是两个集合了。
**求组合,和求排列的回溯写法是不一样的,代码上有小小细节上的改变。**
本题选组过程如下:
<img src='../pics/39.组合总和.png' width=600> </img></div>
分析完过程,回溯算法的模板框架如下:
```
backtracking() {
if (终止条件) {
存放结果;
}
for (选择:选择列表(可以想成树中节点孩子的数量)) {
递归,处理节点;
backtracking();
回溯,撤销处理结果
}
}
```
按照模板不难写出如下代码,但很一些细节,我在注释中标记出来了。
# C++代码
```
// 无限制重复被选取。 吓得我赶紧想想 0 可咋办
class Solution {
private:
vector<vector<int>> result;
void backtracking(vector<int>& candidates, int target, vector<int>& vec, int sum, int startIndex) {
vector<int> path;
void backtracking(vector<int>& 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<vector<int>> combinationSum(vector<int>& candidates, int target) {
vector<int> vec;
backtracking(candidates, target, vec, 0, 0);
backtracking(candidates, target, 0, 0);
return result;
}
};

View File

@ -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为 100k为50呢那就50层for循环是不是开始窒息。**
那么回溯法就能解决这个问题了。
回溯是用来做选择的,递归用来节点层叠嵌套,**每一次的递归是层叠嵌套的关系,可以用于解决多层嵌套循环的问题。**
其实子集和组合问题都可以抽象为一个树形结构,如下:
<img src='../pics/77.组合.png' width=600> </img></div>
可以看一下这个棵树,一开始集合是 1234 从左向右去数,取过的数,不在重复取。
第一取1集合变为234 因为k为2我们只需要去一个数就可以了分别取234 得到集合[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<vector<int>> result; // 存放符合条件结果的集合
vector<int> vec; // 用来存放符合条件结果
vector<int> 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<vector<int>> result; // 存放符合条件结果的集合
vector<int> 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<vector<int>> combine(int n, int k) {
backtracking(n, k, 1);
return result;
}
};
```
# 观后感
我来写一下观后感: 很厉害,转化成从集合中选取子集的问题,可选择的范围随着选择的进行而限缩,于是做剪枝,调整可选择的范围。 每一次的递归是层叠嵌套的关系,可以用于解决多层嵌套循环的问题。 每一层递归中,尽量节省循环次数,这样在后续的递归调用中,节省下来的循环会被以至少指数等级放大。

View File

@ -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/
<img src='../pics/459.重复的子字符串_1.png' width=600> </img></div>
此时next[len - 1] = 7next[len - 1] + 1 = 88就是此时 字符串asdfasdfasdf的最长公共前后缀的长度。
此时next[len - 1] = 7next[len - 1] + 1 = 88就是此时 字符串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;
}
};
```
> 更过算法干货文章持续更新可以微信搜索「代码随想录」第一时间围观关注后回复「Java」「C++」 「python」「简历模板」「数据结构与算法」等等就可以获得我多年整理的学习资料。
# 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;
}
};
```
# 拓展
此时我们已经分享了三篇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」「简历模板」「数据结构与算法」等等就可以获得我多年整理的学习资料。

View File

@ -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)中强调了**打基础的时候,不要太迷恋于库函数。**
甚至一些同学习惯于调用substrsplitreverse之类的库函数却不知道其实现原理也不知道其时间复杂度这样实现出来的代码如果在面试面试现场面试官问“分析其时间复杂度”的话一定会一脸懵逼
所以我们建议**如果题目关键的部分直接用库函数就可以解决,建议不要使用库函数。**
**如果库函数仅仅是 解题过程中的一小部分,并且你已经很清楚这个库函数的内部实现原理的话,可以考虑使用库函数。**
# 双指针法
在[字符串:这道题目,使用库函数一行代码搞定](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)