Merge branch 'youngyangyang04:master' into leetcode-modify-the-code-of-the-BinaryTree

This commit is contained in:
Yuhao Ju
2022-11-29 15:15:47 +08:00
committed by GitHub
33 changed files with 536 additions and 395 deletions

View File

@ -76,7 +76,7 @@ cd a/b/c/../../
**一些同学,在面试中看到这种题目上来就开始写代码,然后就越写越乱。** **一些同学,在面试中看到这种题目上来就开始写代码,然后就越写越乱。**
建议写代码之前要分析好有哪几种不匹配的情况,如果不动手之前分析好,写出的代码也会有很多问题。 建议写代码之前要分析好有哪几种不匹配的情况,如果不动手之前分析好,写出的代码也会有很多问题。
先来分析一下 这里有三种不匹配的情况, 先来分析一下 这里有三种不匹配的情况,

View File

@ -94,7 +94,7 @@ next数组就是一个前缀表prefix table
**前缀表是用来回退的,它记录了模式串与主串(文本串)不匹配的时候,模式串应该从哪里开始重新匹配。** **前缀表是用来回退的,它记录了模式串与主串(文本串)不匹配的时候,模式串应该从哪里开始重新匹配。**
为了清楚了解前缀表的来历,我们来举一个例子: 为了清楚了解前缀表的来历,我们来举一个例子:
要在文本串aabaabaafa 中查找是否出现过一个模式串aabaaf。 要在文本串aabaabaafa 中查找是否出现过一个模式串aabaaf。
@ -110,9 +110,9 @@ next数组就是一个前缀表prefix table
![KMP详解1](https://code-thinking.cdn.bcebos.com/gifs/KMP%E7%B2%BE%E8%AE%B21.gif) ![KMP详解1](https://code-thinking.cdn.bcebos.com/gifs/KMP%E7%B2%BE%E8%AE%B21.gif)
动画里,我特意把 子串`aa` 标记上了,这是有原因的,大家先注意一下,后面还会说 动画里,我特意把 子串`aa` 标记上了,这是有原因的,大家先注意一下,后面还会说
可以看出文本串中第六个字符b 和 模式串的第六个字符f不匹配了。如果暴力匹配发现不匹配,此时就要从头匹配了。 可以看出文本串中第六个字符b 和 模式串的第六个字符f不匹配了。如果暴力匹配发现不匹配此时就要从头匹配了。
但如果使用前缀表就不会从头匹配而是从上次已经匹配的内容开始匹配找到了模式串中第三个字符b继续开始匹配。 但如果使用前缀表就不会从头匹配而是从上次已经匹配的内容开始匹配找到了模式串中第三个字符b继续开始匹配。
@ -157,7 +157,7 @@ next数组就是一个前缀表prefix table
以下这句话,对于理解为什么使用前缀表可以告诉我们匹配失败之后跳到哪里重新匹配 非常重要! 以下这句话,对于理解为什么使用前缀表可以告诉我们匹配失败之后跳到哪里重新匹配 非常重要!
**下标5之前这部分的字符串也就是字符串aabaa的最长相等的前缀 和 后缀字符串是 子字符串aa ,因为找到了最长相等的前缀和后缀,匹配失败的位置是后缀子串的后面,那么我们找到与其相同的前缀的后面新匹配就可以了。** **下标5之前这部分的字符串也就是字符串aabaa的最长相等的前缀 和 后缀字符串是 子字符串aa ,因为找到了最长相等的前缀和后缀,匹配失败的位置是后缀子串的后面,那么我们找到与其相同的前缀的后面新匹配就可以了。**
所以前缀表具有告诉我们当前位置匹配失败,跳到之前已经匹配过的地方的能力。 所以前缀表具有告诉我们当前位置匹配失败,跳到之前已经匹配过的地方的能力。
@ -199,7 +199,7 @@ next数组就是一个前缀表prefix table
所以要看前一位的 前缀表的数值。 所以要看前一位的 前缀表的数值。
前一个字符的前缀表的数值是2把下标移动到下标2的位置继续比配。 可以再反复看一下上面的动画。 前一个字符的前缀表的数值是2把下标移动到下标2的位置继续比配。 可以再反复看一下上面的动画。
最后就在文本串中找到了和模式串匹配的子串了。 最后就在文本串中找到了和模式串匹配的子串了。
@ -211,7 +211,7 @@ next数组就可以是前缀表但是很多实现都是把前缀表统一减
为什么这么做呢,其实也是很多文章视频没有解释清楚的地方。 为什么这么做呢,其实也是很多文章视频没有解释清楚的地方。
其实**这并不涉及到KMP的原理而是具体实现next数组可以就是前缀表,也可以是前缀表统一减一(右移一位,初始位置为-1。** 其实**这并不涉及到KMP的原理而是具体实现next数组可以就是前缀表,也可以是前缀表统一减一(右移一位,初始位置为-1。**
后面我会提供两种不同的实现代码,大家就明白了。 后面我会提供两种不同的实现代码,大家就明白了。
@ -231,7 +231,7 @@ next数组就可以是前缀表但是很多实现都是把前缀表统一减
其中n为文本串长度m为模式串长度因为在匹配的过程中根据前缀表不断调整匹配的位置可以看出匹配的过程是O(n)之前还要单独生成next数组时间复杂度是O(m)。所以整个KMP算法的时间复杂度是O(n+m)的。 其中n为文本串长度m为模式串长度因为在匹配的过程中根据前缀表不断调整匹配的位置可以看出匹配的过程是O(n)之前还要单独生成next数组时间复杂度是O(m)。所以整个KMP算法的时间复杂度是O(n+m)的。
暴力的解法显而易见是O(n × m),所以**KMP在字符串匹配中极大提高搜索的效率。** 暴力的解法显而易见是O(n × m),所以**KMP在字符串匹配中极大提高搜索的效率。**
为了和力扣题目28.实现strStr保持一致方便大家理解以下文章统称haystack为文本串, needle为模式串。 为了和力扣题目28.实现strStr保持一致方便大家理解以下文章统称haystack为文本串, needle为模式串。
@ -251,7 +251,7 @@ void getNext(int* next, const string& s)
2. 处理前后缀不相同的情况 2. 处理前后缀不相同的情况
3. 处理前后缀相同的情况 3. 处理前后缀相同的情况
接下来我们详解详解一下。 接下来我们详解一下。
1. 初始化: 1. 初始化:

View File

@ -829,6 +829,67 @@ object Solution {
} }
``` ```
## Rust
递归:
```rust
impl Solution {
pub fn is_symmetric(root: Option<Rc<RefCell<TreeNode>>>) -> bool {
Self::recur(
&root.as_ref().unwrap().borrow().left,
&root.as_ref().unwrap().borrow().right,
)
}
pub fn recur(
left: &Option<Rc<RefCell<TreeNode>>>,
right: &Option<Rc<RefCell<TreeNode>>>,
) -> bool {
match (left, right) {
(None, None) => true,
(Some(n1), Some(n2)) => {
return n1.borrow().val == n2.borrow().val
&& Self::recur(&n1.borrow().left, &n2.borrow().right)
&& Self::recur(&n1.borrow().right, &n2.borrow().left)
}
_ => false,
}
}
}
```
迭代:
```rust
use std::cell::RefCell;
use std::collections::VecDeque;
use std::rc::Rc;
impl Solution {
pub fn is_symmetric(root: Option<Rc<RefCell<TreeNode>>>) -> bool {
let mut queue = VecDeque::new();
if let Some(node) = root {
queue.push_back(node.borrow().left.clone());
queue.push_back(node.borrow().right.clone());
}
while !queue.is_empty() {
let (n1, n2) = (queue.pop_front().unwrap(), queue.pop_front().unwrap());
match (n1.clone(), n2.clone()) {
(None, None) => continue,
(Some(n1), Some(n2)) => {
if n1.borrow().val != n2.borrow().val {
return false;
}
}
_ => return false,
};
queue.push_back(n1.as_ref().unwrap().borrow().left.clone());
queue.push_back(n2.as_ref().unwrap().borrow().right.clone());
queue.push_back(n1.unwrap().borrow().right.clone());
queue.push_back(n2.unwrap().borrow().left.clone());
}
true
}
}
```
<p align="center"> <p align="center">
<a href="https://programmercarl.com/other/kstar.html" target="_blank"> <a href="https://programmercarl.com/other/kstar.html" target="_blank">
<img src="../pics/网站星球宣传海报.jpg" width="1000"/> <img src="../pics/网站星球宣传海报.jpg" width="1000"/>

View File

@ -200,32 +200,6 @@ public:
}; };
``` ```
rust:
```rust
impl Solution {
pub fn max_depth(root: Option<Rc<RefCell<TreeNode>>>) -> i32 {
if root.is_none(){
return 0;
}
let mut max_depth: i32 = 0;
let mut stack = vec![root.unwrap()];
while !stack.is_empty() {
let num = stack.len();
for _i in 0..num{
let top = stack.remove(0);
if top.borrow_mut().left.is_some(){
stack.push(top.borrow_mut().left.take().unwrap());
}
if top.borrow_mut().right.is_some(){
stack.push(top.borrow_mut().right.take().unwrap());
}
}
max_depth+=1;
}
max_depth
}
```
那么我们可以顺便解决一下n叉树的最大深度问题 那么我们可以顺便解决一下n叉树的最大深度问题
@ -975,6 +949,50 @@ object Solution {
} }
``` ```
## rust
### 0104.二叉树的最大深度
递归:
```rust
impl Solution {
pub fn max_depth(root: Option<Rc<RefCell<TreeNode>>>) -> i32 {
if root.is_none() {
return 0;
}
std::cmp::max(
Self::max_depth(root.clone().unwrap().borrow().left.clone()),
Self::max_depth(root.unwrap().borrow().right.clone()),
) + 1
}
}
```
迭代:
```rust
impl Solution {
pub fn max_depth(root: Option<Rc<RefCell<TreeNode>>>) -> i32 {
if root.is_none(){
return 0;
}
let mut max_depth: i32 = 0;
let mut stack = vec![root.unwrap()];
while !stack.is_empty() {
let num = stack.len();
for _i in 0..num{
let top = stack.remove(0);
if top.borrow_mut().left.is_some(){
stack.push(top.borrow_mut().left.take().unwrap());
}
if top.borrow_mut().right.is_some(){
stack.push(top.borrow_mut().right.take().unwrap());
}
}
max_depth+=1;
}
max_depth
}
```
<p align="center"> <p align="center">
<a href="https://programmercarl.com/other/kstar.html" target="_blank"> <a href="https://programmercarl.com/other/kstar.html" target="_blank">
<img src="../pics/网站星球宣传海报.jpg" width="1000"/> <img src="../pics/网站星球宣传海报.jpg" width="1000"/>

View File

@ -248,7 +248,7 @@ class Solution {
return root; return root;
} }
// 左闭右闭区间[left, right) // 左闭右闭区间[left, right]
private TreeNode traversal(int[] nums, int left, int right) { private TreeNode traversal(int[] nums, int left, int right) {
if (left > right) return null; if (left > right) return null;

View File

@ -574,66 +574,48 @@ object Solution {
rust: rust:
```rust ```rust
impl Solution { impl Solution {
pub fn min_depth(root: Option<Rc<RefCell<TreeNode>>>) -> i32 {
return Solution::bfs(root)
}
// 递归 // 递归
pub fn dfs(node: Option<Rc<RefCell<TreeNode>>>) -> i32{ pub fn min_depth(root: Option<Rc<RefCell<TreeNode>>>) -> i32 {
if node.is_none(){ if let Some(node) = root {
return 0; match (node.borrow().left.clone(), node.borrow().right.clone()) {
(Some(n1), None) => 1 + Self::min_depth(Some(n1)),
(None, Some(n2)) => 1 + Self::min_depth(Some(n2)),
(Some(n1), Some(n2)) => {
1 + std::cmp::min(Self::min_depth(Some(n1)), Self::min_depth(Some(n2)))
} }
let parent = node.unwrap(); _ => 1,
let left_child = parent.borrow_mut().left.take();
let right_child = parent.borrow_mut().right.take();
if left_child.is_none() && right_child.is_none(){
return 1;
} }
let mut min_depth = i32::MAX; } else {
if left_child.is_some(){ 0
let left_depth = Solution::dfs(left_child);
if left_depth <= min_depth{
min_depth = left_depth
} }
} }
if right_child.is_some(){
let right_depth = Solution::dfs(right_child);
if right_depth <= min_depth{
min_depth = right_depth
}
}
min_depth + 1
}
// 迭代 // 迭代
pub fn bfs(node: Option<Rc<RefCell<TreeNode>>>) -> i32{ // 需要 use std::collections::VecDeque;
let mut min_depth = 0; pub fn min_depth(root: Option<Rc<RefCell<TreeNode>>>) -> i32 {
if node.is_none(){ let mut res = 0;
return min_depth let mut queue = VecDeque::new();
if root.is_some() {
queue.push_back(root);
} }
let mut stack = vec![node.unwrap()]; while !queue.is_empty() {
while !stack.is_empty(){ res += 1;
min_depth += 1; for _ in 0..queue.len() {
let num = stack.len(); let node = queue.pop_front().unwrap().unwrap();
for _i in 0..num{ if node.borrow().left.is_none() && node.borrow().right.is_none() {
let top = stack.remove(0); return res;
let left_child = top.borrow_mut().left.take();
let right_child = top.borrow_mut().right.take();
if left_child.is_none() && right_child.is_none(){
return min_depth;
} }
if left_child.is_some(){ if node.borrow().left.is_some() {
stack.push(left_child.unwrap()); queue.push_back(node.borrow().left.clone());
} }
if right_child.is_some(){ if node.borrow().right.is_some() {
stack.push(right_child.unwrap()); queue.push_back(node.borrow().right.clone());
} }
} }
} }
min_depth res
}
} }
``` ```
<p align="center"> <p align="center">

View File

@ -6,7 +6,7 @@
## 139.单词拆分 # 139.单词拆分
[力扣题目链接](https://leetcode.cn/problems/word-break/) [力扣题目链接](https://leetcode.cn/problems/word-break/)
@ -19,19 +19,19 @@
你可以假设字典中没有重复的单词。 你可以假设字典中没有重复的单词。
示例 1 示例 1
输入: s = "leetcode", wordDict = ["leet", "code"] * 输入: s = "leetcode", wordDict = ["leet", "code"]
输出: true * 输出: true
解释: 返回 true 因为 "leetcode" 可以被拆分成 "leet code"。 * 解释: 返回 true 因为 "leetcode" 可以被拆分成 "leet code"。
示例 2 示例 2
输入: s = "applepenapple", wordDict = ["apple", "pen"] * 输入: s = "applepenapple", wordDict = ["apple", "pen"]
输出: true * 输出: true
解释: 返回 true 因为 "applepenapple" 可以被拆分成 "apple pen apple"。 * 解释: 返回 true 因为 "applepenapple" 可以被拆分成 "apple pen apple"。
  注意你可以重复使用字典中的单词。 * 注意你可以重复使用字典中的单词。
示例 3 示例 3
输入: s = "catsandog", wordDict = ["cats", "dog", "sand", "and", "cat"] * 输入: s = "catsandog", wordDict = ["cats", "dog", "sand", "and", "cat"]
输出: false * 输出: false
## 思路 ## 思路
@ -158,24 +158,19 @@ dp[0]表示如果字符串为空的话,说明出现在字典里。
**如果求排列数就是外层for遍历背包内层for循环遍历物品** **如果求排列数就是外层for遍历背包内层for循环遍历物品**
对这个结论还有疑问的同学可以看这篇[本周小结!(动态规划系列五)](https://programmercarl.com/%E5%91%A8%E6%80%BB%E7%BB%93/20210204动规周末总结.html)这篇本周小节中我做了如下总结 我在这里做一个一个总结
求组合数[动态规划518.零钱兑换II](https://programmercarl.com/0518.零钱兑换II.html) 求组合数[动态规划518.零钱兑换II](https://programmercarl.com/0518.零钱兑换II.html)
求排列数[动态规划377. 组合总和 Ⅳ](https://programmercarl.com/0377.组合总和.html)[动态规划70. 爬楼梯进阶版(完全背包)](https://programmercarl.com/0070.爬楼梯完全背包版本.html) 求排列数[动态规划377. 组合总和 Ⅳ](https://programmercarl.com/0377.组合总和.html)[动态规划70. 爬楼梯进阶版(完全背包)](https://programmercarl.com/0070.爬楼梯完全背包版本.html)
求最小数[动态规划322. 零钱兑换](https://programmercarl.com/0322.零钱兑换.html)[动态规划279.完全平方数](https://programmercarl.com/0279.完全平方数.html) 求最小数[动态规划322. 零钱兑换](https://programmercarl.com/0322.零钱兑换.html)[动态规划279.完全平方数](https://programmercarl.com/0279.完全平方数.html)
本题最终要求的是是否都出现过所以对出现单词集合里的元素是组合还是排列并不在意 本题其实我们求的是排列数为什么呢 s = "applepenapple", wordDict = ["apple", "pen"] 举例
**那么本题使用求排列的方式,还是求组合的方式都可以** "apple", "pen" 是物品那么我们要求 物品的组合一定是 "apple" + "pen" + "apple" 才能组成 "applepenapple"
外层for循环遍历物品内层for遍历背包 或者 外层for遍历背包内层for循环遍历物品 都是可以的 "apple" + "apple" + "pen" 或者 "pen" + "apple" + "apple" 是不可以的那么我们就是强调物品之间顺序
但本题还有特殊性因为是要求子串最好是遍历背包放在外循环将遍历物品放在内循环
如果要是外层for循环遍历物品内层for遍历背包就需要把所有的子串都预先放在一个容器里。(如果不理解的话可以自己尝试这么写一写就理解了
**所以最终我选择的遍历顺序为:遍历背包放在外循环,将遍历物品放在内循环。内循环从前到后**
所以说本题一定是 先遍历 背包在遍历物品
5. 举例推导dp[i] 5. 举例推导dp[i]
@ -210,22 +205,51 @@ public:
* 时间复杂度O(n^3)因为substr返回子串的副本是O(n)的复杂度这里的n是substring的长度 * 时间复杂度O(n^3)因为substr返回子串的副本是O(n)的复杂度这里的n是substring的长度
* 空间复杂度O(n) * 空间复杂度O(n)
## 拓展
关于遍历顺序再给大家讲一下为什么 先遍历物品再遍历背包不行
这里可以给出先遍历物品在遍历背包的代码
```CPP
class Solution {
public:
bool wordBreak(string s, vector<string>& wordDict) {
unordered_set<string> wordSet(wordDict.begin(), wordDict.end());
vector<bool> dp(s.size() + 1, false);
dp[0] = true;
for (int j = 0; j < wordDict.size(); j++) { // 物品
for (int i = wordDict[j].size(); i <= s.size(); i++) { // 背包
string word = s.substr(i - wordDict[j].size(), wordDict[j].size());
// cout << word << endl;
if ( word == wordDict[j] && dp[i - wordDict[j].size()]) {
dp[i] = true;
}
// for (int k = 0; k <= s.size(); k++) cout << dp[k] << " "; //这里打印 dp数组的情况
// cout << endl;
}
}
return dp[s.size()];
}
};
```
使用用例:s = "applepenapple", wordDict = ["apple", "pen"],对应的dp数组状态如下
![](https://code-thinking-1253855093.file.myqcloud.com/pics/20221123205105.png)
最后dp[s.size()] = 0 dp[13] = 0 ,而不是1,因为先用 "apple" 去遍历的时候,dp[8]并没有被赋值为1 (还没用"pen"),所以 dp[13]也不能变成1
除非是先用 "apple" 遍历一遍,在用 "pen" 遍历,此时 dp[8]已经是1,最后再用 "apple" 去遍历,dp[13]才能是1
如果大家对这里不理解,建议可以把我上面给的代码,拿去力扣上跑一跑,把dp数组打印出来,对着递推公式一步一步去看,思路就清晰了。
## 总结 ## 总结
本题和我们之前讲解回溯专题的[回溯算法:分割回文串](https://programmercarl.com/0131.分割回文串.html)非常像,所以我也给出了对应的回溯解法。 本题和我们之前讲解回溯专题的[回溯算法:分割回文串](https://programmercarl.com/0131.分割回文串.html)非常像,所以我也给出了对应的回溯解法。
稍加分析便可知道本题是完全背包而且是求能否组成背包所以遍历顺序理论上来讲 两层for循环谁先谁后都可以 稍加分析便可知道本题是完全背包是求能否组成背包,而且这里要求物品是要有顺序的。
但因为分割子串的特殊性遍历背包放在外循环将遍历物品放在内循环更方便一些
本题其实递推公式都不是重点遍历顺序才是重点如果我直接把代码贴出来估计同学们也会想两个for循环的顺序理所当然就是这样甚至都不会想为什么遍历背包的for循环在外层
不分析透彻不是Carl的风格啊哈哈
## 其他语言版本 ## 其他语言版本

View File

@ -51,7 +51,7 @@
``` ```
逆波兰表达式:是一种后缀表达式,所谓后缀就是指算符写在后面。 逆波兰表达式:是一种后缀表达式,所谓后缀就是指算符写在后面。
平常使用的算式则是一种中缀表达式,如 ( 1 + 2 ) * ( 3 + 4 ) 。 平常使用的算式则是一种中缀表达式,如 ( 1 + 2 ) * ( 3 + 4 ) 。
@ -61,11 +61,11 @@
* 去掉括号后表达式无歧义,上式即便写成 1 2 + 3 4 + * 也可以依据次序计算出正确结果。 * 去掉括号后表达式无歧义,上式即便写成 1 2 + 3 4 + * 也可以依据次序计算出正确结果。
* 适合用栈操作运算:遇到数字则入栈;遇到算符则取出栈顶两个数字进行计算,并将结果压入栈中。 * 适合用栈操作运算:遇到数字则入栈;遇到算符则取出栈顶两个数字进行计算,并将结果压入栈中。
# 思路 # 思路
《代码随想录》算法视频公开课:[栈的最后表演! | LeetCode150. 逆波兰表达式求值](https://www.bilibili.com/video/BV1kd4y1o7on),相信结合视频看本篇题解,更有助于大家对本题的理解。 《代码随想录》算法视频公开课:[栈的最后表演! | LeetCode150. 逆波兰表达式求值](https://www.bilibili.com/video/BV1kd4y1o7on),相信结合视频看本篇题解,更有助于大家对本题的理解。
在上一篇文章中[1047.删除字符串中的所有相邻重复项](https://programmercarl.com/1047.删除字符串中的所有相邻重复项.html)提到了 递归就是用栈来实现的。 在上一篇文章中[1047.删除字符串中的所有相邻重复项](https://programmercarl.com/1047.删除字符串中的所有相邻重复项.html)提到了 递归就是用栈来实现的。
@ -73,7 +73,7 @@
那么来看一下本题,**其实逆波兰表达式相当于是二叉树中的后序遍历**。 大家可以把运算符作为中间节点,按照后序遍历的规则画出一个二叉树。 那么来看一下本题,**其实逆波兰表达式相当于是二叉树中的后序遍历**。 大家可以把运算符作为中间节点,按照后序遍历的规则画出一个二叉树。
但我们没有必要从二叉树的角度去解决这个问题,只要知道逆波兰表达式是用后遍历的方式把二叉树序列化了,就可以了。 但我们没有必要从二叉树的角度去解决这个问题,只要知道逆波兰表达式是用后遍历的方式把二叉树序列化了,就可以了。
在进一步看,本题中每一个子表达式要得出一个结果,然后拿这个结果再进行运算,那么**这岂不就是一个相邻字符串消除的过程,和[1047.删除字符串中的所有相邻重复项](https://programmercarl.com/1047.删除字符串中的所有相邻重复项.html)中的对对碰游戏是不是就非常像了。** 在进一步看,本题中每一个子表达式要得出一个结果,然后拿这个结果再进行运算,那么**这岂不就是一个相邻字符串消除的过程,和[1047.删除字符串中的所有相邻重复项](https://programmercarl.com/1047.删除字符串中的所有相邻重复项.html)中的对对碰游戏是不是就非常像了。**
@ -118,9 +118,9 @@ public:
我们习惯看到的表达式都是中缀表达式,因为符合我们的习惯,但是中缀表达式对于计算机来说就不是很友好了。 我们习惯看到的表达式都是中缀表达式,因为符合我们的习惯,但是中缀表达式对于计算机来说就不是很友好了。
例如4 + 13 / 5这就是中缀表达式计算机从左到右去扫描的话扫到13还要判断13后面是什么运算还要比较一下优先级然后13还和后面的5做运算做完运算之后还要向前回退到 4 的位置,继续做加法,你说麻不麻烦! 例如4 + 13 / 5这就是中缀表达式计算机从左到右去扫描的话扫到13还要判断13后面是什么运算还要比较一下优先级然后13还和后面的5做运算做完运算之后还要向前回退到 4 的位置,继续做加法,你说麻不麻烦!
那么将中缀表达式,转化为后缀表达式之后:["4", "13", "5", "/", "+"] ,就不一样了,计算机可以利用栈顺序处理,不需要考虑优先级了。也不用回退了, **所以后缀表达式对计算机来说是非常友好的。** 那么将中缀表达式,转化为后缀表达式之后:["4", "13", "5", "/", "+"] ,就不一样了,计算机可以利用栈顺序处理,不需要考虑优先级了。也不用回退了, **所以后缀表达式对计算机来说是非常友好的。**
可以说本题不仅仅是一道好题,也展现出计算机的思考方式。 可以说本题不仅仅是一道好题,也展现出计算机的思考方式。
@ -161,6 +161,24 @@ class Solution {
} }
``` ```
python3
```python
class Solution:
def evalRPN(self, tokens: List[str]) -> int:
stack = []
for item in tokens:
if item not in {"+", "-", "*", "/"}:
stack.append(item)
else:
first_num, second_num = stack.pop(), stack.pop()
stack.append(
int(eval(f'{second_num} {item} {first_num}')) # 第一个出来的在运算符后面
)
return int(stack.pop()) # 如果一开始只有一个数,那么会是字符串形式的
```
Go: Go:
```Go ```Go
func evalRPN(tokens []string) int { func evalRPN(tokens []string) int {
@ -169,7 +187,7 @@ func evalRPN(tokens []string) int {
val, err := strconv.Atoi(token) val, err := strconv.Atoi(token)
if err == nil { if err == nil {
stack = append(stack, val) stack = append(stack, val)
} else { } else { // 如果err不为nil说明不是数字
num1, num2 := stack[len(stack)-2], stack[(len(stack))-1] num1, num2 := stack[len(stack)-2], stack[(len(stack))-1]
stack = stack[:len(stack)-2] stack = stack[:len(stack)-2]
switch token { switch token {
@ -191,27 +209,31 @@ func evalRPN(tokens []string) int {
javaScript: javaScript:
```js ```js
/**
* @param {string[]} tokens
* @return {number}
*/
var evalRPN = function (tokens) { var evalRPN = function (tokens) {
const s = new Map([
["+", (a, b) => a * 1 + b * 1],
["-", (a, b) => b - a],
["*", (a, b) => b * a],
["/", (a, b) => (b / a) | 0]
]);
const stack = []; const stack = [];
for (const i of tokens) { for (const token of tokens) {
if(!s.has(i)) { if (isNaN(Number(token))) { // 非数字
stack.push(i); const n2 = stack.pop(); // 出栈两个数字
continue; const n1 = stack.pop();
switch (token) { // 判断运算符类型,算出新数入栈
case "+":
stack.push(n1 + n2);
break;
case "-":
stack.push(n1 - n2);
break;
case "*":
stack.push(n1 * n2);
break;
case "/":
stack.push(n1 / n2 | 0);
break;
} }
stack.push(s.get(i)(stack.pop(),stack.pop())) } else { // 数字
stack.push(Number(token));
} }
return stack.pop(); }
return stack[0]; // 因没有遇到运算符而待在栈中的结果
}; };
``` ```
@ -280,24 +302,6 @@ function evalRPN(tokens: string[]): number {
}; };
``` ```
python3
```python
class Solution:
def evalRPN(self, tokens: List[str]) -> int:
stack = []
for item in tokens:
if item not in {"+", "-", "*", "/"}:
stack.append(item)
else:
first_num, second_num = stack.pop(), stack.pop()
stack.append(
int(eval(f'{second_num} {item} {first_num}')) # 第一个出来的在运算符后面
)
return int(stack.pop()) # 如果一开始只有一个数,那么会是字符串形式的
```
Swift Swift
```Swift ```Swift
func evalRPN(_ tokens: [String]) -> Int { func evalRPN(_ tokens: [String]) -> Int {

View File

@ -119,7 +119,7 @@ void removeExtraSpaces(string& s) {
1. leetcode上的测试集里字符串的长度不够长如果足够长性能差距会非常明显。 1. leetcode上的测试集里字符串的长度不够长如果足够长性能差距会非常明显。
2. leetcode的测程序耗时不是很准确的。 2. leetcode的测程序耗时不是很准确的。
版本一的代码是比较如何一般思考过程,就是 先移除字符串的空格,移除中间的,移除后面部分。 版本一的代码是一般思考过程,就是 先移除字符串的空格,移除中间的,移除后面部分。
不过其实还可以优化,这部分和[27.移除元素](https://programmercarl.com/0027.移除元素.html)的逻辑是一样一样的,本题是移除空格,而 27.移除元素 就是移除元素。 不过其实还可以优化,这部分和[27.移除元素](https://programmercarl.com/0027.移除元素.html)的逻辑是一样一样的,本题是移除空格,而 27.移除元素 就是移除元素。
@ -145,7 +145,7 @@ void removeExtraSpaces(string& s) {//去除所有空格并在相邻单词之间
此时我们已经实现了removeExtraSpaces函数来移除冗余空格。 此时我们已经实现了removeExtraSpaces函数来移除冗余空格。
实现反转字符串的功能,支持反转字符串子区间,这个实现我们分别在[344.反转字符串](https://programmercarl.com/0344.反转字符串.html)和[541.反转字符串II](https://programmercarl.com/0541.反转字符串II.html)里已经讲过了。 实现反转字符串的功能,支持反转字符串子区间,这个实现我们分别在[344.反转字符串](https://programmercarl.com/0344.反转字符串.html)和[541.反转字符串II](https://programmercarl.com/0541.反转字符串II.html)里已经讲过了。
代码如下: 代码如下:
@ -451,6 +451,7 @@ class Solution:
tmp.append(s[left]) tmp.append(s[left])
left += 1 left += 1
return tmp return tmp
#2.翻转字符数组 #2.翻转字符数组
def reverse_string(self, nums, left, right): def reverse_string(self, nums, left, right):
while left < right: while left < right:
@ -458,6 +459,7 @@ class Solution:
left += 1 left += 1
right -= 1 right -= 1
return None return None
#3.翻转每个单词 #3.翻转每个单词
def reverse_each_word(self, nums): def reverse_each_word(self, nums):
start = 0 start = 0

View File

@ -12,15 +12,16 @@
给定一个代表每个房屋存放金额的非负整数数组,计算你 不触动警报装置的情况下 ,一夜之内能够偷窃到的最高金额。 给定一个代表每个房屋存放金额的非负整数数组,计算你 不触动警报装置的情况下 ,一夜之内能够偷窃到的最高金额。
示例 1 * 示例 1
输入:[1,2,3,1] * 输入:[1,2,3,1]
输出4 * 输出4
解释:偷窃 1 号房屋 (金额 = 1) ,然后偷窃 3 号房屋 (金额 = 3)。 解释:偷窃 1 号房屋 (金额 = 1) ,然后偷窃 3 号房屋 (金额 = 3)。
  偷窃到的最高金额 = 1 + 3 = 4 。   偷窃到的最高金额 = 1 + 3 = 4 。
示例 2 * 示例 2
输入:[2,7,9,3,1] * 输入:[2,7,9,3,1]
输出12 * 输出12
解释:偷窃 1 号房屋 (金额 = 2), 偷窃 3 号房屋 (金额 = 9),接着偷窃 5 号房屋 (金额 = 1)。 解释:偷窃 1 号房屋 (金额 = 2), 偷窃 3 号房屋 (金额 = 9),接着偷窃 5 号房屋 (金额 = 1)。
  偷窃到的最高金额 = 2 + 9 + 1 = 12 。   偷窃到的最高金额 = 2 + 9 + 1 = 12 。
@ -33,7 +34,13 @@
## 思路 ## 思路
打家劫舍是dp解决的经典问题动规五部曲分析如下 大家如果刚接触这样的题目,会有点困惑,当前的状态我是偷还是不偷呢?
仔细一想,当前房屋偷与不偷取决于 前一个房屋和前两个房屋是否被偷了。
所以这里就更感觉到,当前状态和前面状态会有一种依赖关系,那么这种依赖关系都是动规的递推公式。
当然以上是大概思路打家劫舍是dp解决的经典问题接下来我们来动规五部曲分析如下
1. 确定dp数组dp table以及下标的含义 1. 确定dp数组dp table以及下标的含义

View File

@ -29,7 +29,7 @@
# 思路 # 思路
《代码随想录》算法公开课:[队列的基本操作! | LeetCode225. 用队列实现栈](https://www.bilibili.com/video/BV1Fd4y1K7sm),相信结合视频看本篇题解,更有助于大家对链表的理解。 《代码随想录》算法公开课:[队列的基本操作! | LeetCode225. 用队列实现栈](https://www.bilibili.com/video/BV1Fd4y1K7sm),相信结合视频看本篇题解,更有助于大家对链表的理解。
(这里要强调是单向队列) (这里要强调是单向队列)
@ -44,7 +44,7 @@
所以用栈实现队列, 和用队列实现栈的思路还是不一样的,这取决于这两个数据结构的性质。 所以用栈实现队列, 和用队列实现栈的思路还是不一样的,这取决于这两个数据结构的性质。
但是依然还是要用两个队列来模拟栈,只不过没有输入和输出的关系,而是另一个队列完全用来备份的! 但是依然还是要用两个队列来模拟栈,只不过没有输入和输出的关系,而是另一个队列完全用来备份的!
如下面动画所示,**用两个队列que1和que2实现队列的功能que2其实完全就是一个备份的作用**把que1最后面的元素以外的元素都备份到que2然后弹出最后面的元素再把其他元素从que2导回que1。 如下面动画所示,**用两个队列que1和que2实现队列的功能que2其实完全就是一个备份的作用**把que1最后面的元素以外的元素都备份到que2然后弹出最后面的元素再把其他元素从que2导回que1。
@ -116,7 +116,7 @@ public:
其实这道题目就是用一个队列就够了。 其实这道题目就是用一个队列就够了。
**一个队列在模拟栈弹出元素的时候只要将队列头部的元素(除了最后一个元素外) 重新添加到队列尾部,此时去弹出元素就是栈的顺序了。** **一个队列在模拟栈弹出元素的时候只要将队列头部的元素(除了最后一个元素外) 重新添加到队列尾部,此时去弹出元素就是栈的顺序了。**
C++优化代码 C++优化代码

View File

@ -38,14 +38,14 @@ queue.empty(); // 返回 false
## 思路 ## 思路
《代码随想录》算法公开课:[栈的基本操作! | LeetCode232.用栈实现队列](https://www.bilibili.com/video/BV1nY4y1w7VC),相信结合视频看本篇题解,更有助于大家对链表的理解。 《代码随想录》算法公开课:[栈的基本操作! | LeetCode232.用栈实现队列](https://www.bilibili.com/video/BV1nY4y1w7VC),相信结合视频看本篇题解,更有助于大家对链表的理解。
这是一道模拟题,不涉及到具体算法,考察的就是对栈和队列的掌握程度。 这是一道模拟题,不涉及到具体算法,考察的就是对栈和队列的掌握程度。
使用栈来模式队列的行为,如果仅仅用一个栈,是一定不行的,所以需要两个栈**一个输入栈,一个输出栈**,这里要注意输入栈和输出栈的关系。 使用栈来模式队列的行为,如果仅仅用一个栈,是一定不行的,所以需要两个栈**一个输入栈,一个输出栈**,这里要注意输入栈和输出栈的关系。
下面动画模拟以下队列的执行过程如下 下面动画模拟以下队列的执行过程:
执行语句: 执行语句:
queue.push(1); queue.push(1);
@ -120,7 +120,7 @@ public:
这样的项目代码会越来越乱,**一定要懂得复用,功能相近的函数要抽象出来,不要大量的复制粘贴,很容易出问题!(踩过坑的人自然懂)** 这样的项目代码会越来越乱,**一定要懂得复用,功能相近的函数要抽象出来,不要大量的复制粘贴,很容易出问题!(踩过坑的人自然懂)**
工作中如果发现某一个功能自己要经常用,同事们可能也会用到,自己就花点时间把这个功能抽象成一个好用的函数或者工具类,不仅自己方便,也方了同事们。 工作中如果发现某一个功能自己要经常用,同事们可能也会用到,自己就花点时间把这个功能抽象成一个好用的函数或者工具类,不仅自己方便,也方便了同事们。
同事们就会逐渐认可你的工作态度和工作能力,自己的口碑都是这么一点一点积累起来的!在同事圈里口碑起来了之后,你就发现自己走上了一个正循环,以后的升职加薪才少不了你!哈哈哈 同事们就会逐渐认可你的工作态度和工作能力,自己的口碑都是这么一点一点积累起来的!在同事圈里口碑起来了之后,你就发现自己走上了一个正循环,以后的升职加薪才少不了你!哈哈哈
@ -231,56 +231,51 @@ class MyQueue:
Go Go
```Go ```Go
type MyQueue struct { type MyQueue struct {
stack []int stackIn []int //输入栈
back []int stackOut []int //输出栈
} }
/** Initialize your data structure here. */
func Constructor() MyQueue { func Constructor() MyQueue {
return MyQueue{ return MyQueue{
stack: make([]int, 0), stackIn: make([]int, 0),
back: make([]int, 0), stackOut: make([]int, 0),
} }
} }
/** Push element x to the back of queue. */ // 往输入栈做push
func (this *MyQueue) Push(x int) { func (this *MyQueue) Push(x int) {
for len(this.back) != 0 { this.stackIn = append(this.stackIn, x)
val := this.back[len(this.back)-1]
this.back = this.back[:len(this.back)-1]
this.stack = append(this.stack, val)
}
this.stack = append(this.stack, x)
} }
/** Removes the element from in front of queue and returns that element. */ // 在输出栈做poppop时如果输出栈数据为空需要将输入栈全部数据导入如果非空则可直接使用
func (this *MyQueue) Pop() int { func (this *MyQueue) Pop() int {
for len(this.stack) != 0 { inLen, outLen := len(this.stackIn), len(this.stackOut)
val := this.stack[len(this.stack)-1] if outLen == 0 {
this.stack = this.stack[:len(this.stack)-1] if inLen == 0 {
this.back = append(this.back, val) return -1
} }
if len(this.back) == 0 { for i := inLen - 1; i >= 0; i-- {
return 0 this.stackOut = append(this.stackOut, this.stackIn[i])
} }
val := this.back[len(this.back)-1] this.stackIn = []int{} //导出后清空
this.back = this.back[:len(this.back)-1] outLen = len(this.stackOut) //更新长度值
}
val := this.stackOut[outLen-1]
this.stackOut = this.stackOut[:outLen-1]
return val return val
} }
/** Get the front element. */
func (this *MyQueue) Peek() int { func (this *MyQueue) Peek() int {
val := this.Pop() val := this.Pop()
if val == 0 { if val == -1 {
return 0 return -1
} }
this.back = append(this.back, val) this.stackOut = append(this.stackOut, val)
return val return val
} }
/** Returns whether the queue is empty. */
func (this *MyQueue) Empty() bool { func (this *MyQueue) Empty() bool {
return len(this.stack) == 0 && len(this.back) == 0 return len(this.stackIn) == 0 && len(this.stackOut) == 0
} }
``` ```

View File

@ -38,7 +38,7 @@
难点是如何求一个区间里的最大值呢? (这好像是废话),暴力一下不就得了。 难点是如何求一个区间里的最大值呢? (这好像是废话),暴力一下不就得了。
暴力方法,遍历一遍的过程中每次从窗口中找到最大的数值这样很明显是O(n × k)的算法。 暴力方法,遍历一遍的过程中每次从窗口中找到最大的数值这样很明显是O(n × k)的算法。
有的同学可能会想用一个大顶堆优先级队列来存放这个窗口里的k个数字这样就可以知道最大的最大值是多少了 **但是问题是这个窗口是移动的,而大顶堆每次只能弹出最大值,我们无法移除其他数值,这样就造成大顶堆维护的不是滑动窗口里面的数值了。所以不能用大顶堆。** 有的同学可能会想用一个大顶堆优先级队列来存放这个窗口里的k个数字这样就可以知道最大的最大值是多少了 **但是问题是这个窗口是移动的,而大顶堆每次只能弹出最大值,我们无法移除其他数值,这样就造成大顶堆维护的不是滑动窗口里面的数值了。所以不能用大顶堆。**
@ -66,7 +66,7 @@ public:
**可惜了,没有! 我们需要自己实现这么个队列。** **可惜了,没有! 我们需要自己实现这么个队列。**
然后分析一下,队列里的元素一定是要排序的,而且要最大值放在出队口,要不然怎么知道最大值呢。 然后分析一下,队列里的元素一定是要排序的,而且要最大值放在出队口,要不然怎么知道最大值呢。
但如果把窗口里的元素都放进队列里,窗口移动的时候,队列需要弹出元素。 但如果把窗口里的元素都放进队列里,窗口移动的时候,队列需要弹出元素。
@ -74,9 +74,9 @@ public:
大家此时应该陷入深思..... 大家此时应该陷入深思.....
**其实队列没有必要维护窗口里的所有元素,只需要维护有可能成为窗口里最大值的元素就可以了,同时保证队里的元素数值是由大到小的。** **其实队列没有必要维护窗口里的所有元素,只需要维护有可能成为窗口里最大值的元素就可以了,同时保证队里的元素数值是由大到小的。**
那么这个维护元素单调递减的队列就叫做**单调队列即单调递减或单调递增的队列。C++中没有直接支持单调队列,需要我们自己来一个单调队列** 那么这个维护元素单调递减的队列就叫做**单调队列即单调递减或单调递增的队列。C++中没有直接支持单调队列,需要我们自己来实现一个单调队列**
**不要以为实现的单调队列就是 对窗口里面的数进行排序,如果排序的话,那和优先级队列又有什么区别了呢。** **不要以为实现的单调队列就是 对窗口里面的数进行排序,如果排序的话,那和优先级队列又有什么区别了呢。**
@ -185,7 +185,7 @@ public:
}; };
``` ```
来看一下时间复杂度,使用单调队列的时间复杂度是 O(n)。 来看一下时间复杂度,使用单调队列的时间复杂度是 O(n)。
有的同学可能想了,在队列中 push元素的过程中还有pop操作呢感觉不是纯粹的O(n)。 有的同学可能想了,在队列中 push元素的过程中还有pop操作呢感觉不是纯粹的O(n)。

View File

@ -131,12 +131,10 @@ public:
vector<int> dp(n + 1, INT_MAX); vector<int> dp(n + 1, INT_MAX);
dp[0] = 0; dp[0] = 0;
for (int i = 1; i * i <= n; i++) { // 遍历物品 for (int i = 1; i * i <= n; i++) { // 遍历物品
for (int j = 1; j <= n; j++) { // 遍历背包 for (int j = i * i; j <= n; j++) { // 遍历背包
if (j - i * i >= 0) {
dp[j] = min(dp[j - i * i] + 1, dp[j]); dp[j] = min(dp[j - i * i] + 1, dp[j]);
} }
} }
}
return dp[n]; return dp[n];
} }
}; };

View File

@ -53,8 +53,6 @@
2. 确定递推公式 2. 确定递推公式
得到dp[j]考虑coins[i]只有一个来源dp[j - coins[i]]没有考虑coins[i])。
凑足总额为j - coins[i]的最少个数为dp[j - coins[i]]那么只需要加上一个钱币coins[i]即dp[j - coins[i]] + 1就是dp[j]考虑coins[i] 凑足总额为j - coins[i]的最少个数为dp[j - coins[i]]那么只需要加上一个钱币coins[i]即dp[j - coins[i]] + 1就是dp[j]考虑coins[i]
所以dp[j] 要取所有 dp[j - coins[i]] + 1 中最小的。 所以dp[j] 要取所有 dp[j - coins[i]] + 1 中最小的。

View File

@ -50,8 +50,8 @@ public:
}; };
``` ```
* 时间复杂度:$O(n^2)$,这个时间复杂度不太标准,也不容易准确化,例如越往下的节点重复计算次数就越多 * 时间复杂度O(n^2),这个时间复杂度不太标准,也不容易准确化,例如越往下的节点重复计算次数就越多
* 空间复杂度:$O(\log n)$,算上递推系统栈的空间 * 空间复杂度O(log n),算上递推系统栈的空间
当然以上代码超时了,这个递归的过程中其实是有重复计算了。 当然以上代码超时了,这个递归的过程中其实是有重复计算了。
@ -84,8 +84,8 @@ public:
``` ```
* 时间复杂度:$O(n)$ * 时间复杂度O(n)
* 空间复杂度:$O(\log n)$,算上递推系统栈的空间 * 空间复杂度O(log n),算上递推系统栈的空间
### 动态规划 ### 动态规划

View File

@ -271,7 +271,7 @@ func (h *IHeap) Pop() interface{}{
} }
//方法二:利用O(logn)排序 //方法二:利用O(nlogn)排序
func topKFrequent(nums []int, k int) []int { func topKFrequent(nums []int, k int) []int {
ans:=[]int{} ans:=[]int{}
map_num:=map[int]int{} map_num:=map[int]int{}

View File

@ -4,9 +4,8 @@
</a> </a>
<p align="center"><strong><a href="https://mp.weixin.qq.com/s/tqCxrMEU-ajQumL1i8im9A">参与本项目</a>,贡献其他语言版本的代码,拥抱开源,让更多学习算法的小伙伴们收益!</strong></p> <p align="center"><strong><a href="https://mp.weixin.qq.com/s/tqCxrMEU-ajQumL1i8im9A">参与本项目</a>,贡献其他语言版本的代码,拥抱开源,让更多学习算法的小伙伴们收益!</strong></p>
# 动态规划Carl称它为排列总和
## 377. 组合总和 Ⅳ # 377. 组合总和 Ⅳ
[力扣题目链接](https://leetcode.cn/problems/combination-sum-iv/) [力扣题目链接](https://leetcode.cn/problems/combination-sum-iv/)

View File

@ -79,7 +79,17 @@
01背包中dp[j] 表示: 容量为j的背包所背的物品价值可以最大为dp[j]。 01背包中dp[j] 表示: 容量为j的背包所背的物品价值可以最大为dp[j]。
**套到本题dp[j]表示 背包总容量是j最大可以凑成j的子集总和为dp[j]** 本题中每一个元素的数值即是重量,也是价值
**套到本题dp[j]表示 背包总容量所能装的总重量是j放进物品后背的最大重量为dp[j]**
那么如果背包容量为target dp[target]就是装满 背包之后的重量,所以 当 dp[target] == target 的时候,背包就装满了。
有录友可能想,那还有装不满的时候?
拿输入数组 [1, 5, 11, 5],距离, dp[7] 只能等于 6因为 只能放进 1 和 5。
而dp[6] 就可以等于6了放进1 和 5那么dp[6] == 6说明背包装满了。
2. 确定递推公式 2. 确定递推公式

View File

@ -46,17 +46,17 @@
## 移动匹配 ## 移动匹配
当一个字符串sabcabc内部重复的子串组成,那么这个字符串的结构一定是这样的: 当一个字符串sabcabc内部重复的子串组成,那么这个字符串的结构一定是这样的:
![图一](https://code-thinking-1253855093.file.myqcloud.com/pics/20220728104518.png) ![图一](https://code-thinking-1253855093.file.myqcloud.com/pics/20220728104518.png)
也就是前后相同的子串组成。 也就是前后相同的子串组成。
那么既然前面有相同的子串,后面有相同的子串,用 s + s这样组成的字符串中后面的子串做前串前后的子串做后串就一定还能组成一个s如图 那么既然前面有相同的子串,后面有相同的子串,用 s + s这样组成的字符串中后面的子串做前串前后的子串做后串就一定还能组成一个s如图
![图二](https://code-thinking-1253855093.file.myqcloud.com/pics/20220728104931.png) ![图二](https://code-thinking-1253855093.file.myqcloud.com/pics/20220728104931.png)
所以判断字符串s是否重复子串组成只要两个s拼接在一起里面还出现一个s的话就说明是重复子串组成。 所以判断字符串s是否重复子串组成只要两个s拼接在一起里面还出现一个s的话就说明是重复子串组成。
当然,我们在判断 s + s 拼接的字符串里是否出现一个s的的时候**要刨除 s + s 的首字符和尾字符**这样避免在s+s中搜索出原来的s我们要搜索的是中间拼接出来的s。 当然,我们在判断 s + s 拼接的字符串里是否出现一个s的的时候**要刨除 s + s 的首字符和尾字符**这样避免在s+s中搜索出原来的s我们要搜索的是中间拼接出来的s。
@ -81,7 +81,7 @@ public:
## KMP ## KMP
### 为什么会使用KMP ### 为什么会使用KMP
以下使用KMP方式讲解强烈建议大家先把下两个视频看了理解KMP算法来看下面讲解,否则会很懵。 以下使用KMP方式讲解强烈建议大家先把下两个视频看了理解KMP算法来看下面讲解,否则会很懵。
* [视频讲解版帮你把KMP算法学个通透理论篇](https://www.bilibili.com/video/BV1PD4y1o7nd/) * [视频讲解版帮你把KMP算法学个通透理论篇](https://www.bilibili.com/video/BV1PD4y1o7nd/)
* [视频讲解版帮你把KMP算法学个通透求next数组代码篇](https://www.bilibili.com/video/BV1M5411j7Xx) * [视频讲解版帮你把KMP算法学个通透求next数组代码篇](https://www.bilibili.com/video/BV1M5411j7Xx)
@ -93,12 +93,12 @@ KMP算法中next数组为什么遇到字符不匹配的时候可以找到上一
那么 最长相同前后缀和重复子串的关系又有什么关系呢。 那么 最长相同前后缀和重复子串的关系又有什么关系呢。
可能很多录友又忘了 前缀和后缀的定义,回顾一下: 可能很多录友又忘了 前缀和后缀的定义,回顾一下:
* 前缀是指不包含最后一个字符的所有以第一个字符开头的连续子串; * 前缀是指不包含最后一个字符的所有以第一个字符开头的连续子串;
* 后缀是指不包含第一个字符的所有以最后一个字符结尾的连续子串 * 后缀是指不包含第一个字符的所有以最后一个字符结尾的连续子串
在由重复子串组成的字符串中,最长相等前后缀不包含的子串就是最小重复子串,这里字符串sabababab 来举例ab就是最小重复单位如图所示 在由重复子串组成的字符串中,最长相等前后缀不包含的子串就是最小重复子串,这里字符串sabababab 来举例ab就是最小重复单位如图所示
![图三](https://code-thinking-1253855093.file.myqcloud.com/pics/20220728205249.png) ![图三](https://code-thinking-1253855093.file.myqcloud.com/pics/20220728205249.png)
@ -123,11 +123,11 @@ KMP算法中next数组为什么遇到字符不匹配的时候可以找到上一
### 简单推理 ### 简单推理
这里给出一个数推导,就容易理解很多。 这里给出一个数推导,就容易理解很多。
假设字符串s使用多个重复子串构成这个子串是最小重复单位重复出现的子字符串长度是x所以s是由n * x组成。 假设字符串s使用多个重复子串构成这个子串是最小重复单位重复出现的子字符串长度是x所以s是由n * x组成。
因为字符串s的最长相同前后缀的长度一定是不包含s本身所以 最长相同前后缀长度必然是m * x而且 n - m = 1这里如果不懂看上面的推理 因为字符串s的最长相同前后缀的长度一定是不包含s本身所以 最长相同前后缀长度必然是m * x而且 n - m = 1这里如果不懂看上面的推理
所以如果 nx % (n - m)x = 0就可以判定有重复出现的子字符串。 所以如果 nx % (n - m)x = 0就可以判定有重复出现的子字符串。

View File

@ -3,9 +3,8 @@
<img src="../pics/训练营.png" width="1000"/> <img src="../pics/训练营.png" width="1000"/>
</a> </a>
<p align="center"><strong><a href="https://mp.weixin.qq.com/s/tqCxrMEU-ajQumL1i8im9A">参与本项目</a>,贡献其他语言版本的代码,拥抱开源,让更多学习算法的小伙伴们收益!</strong></p> <p align="center"><strong><a href="https://mp.weixin.qq.com/s/tqCxrMEU-ajQumL1i8im9A">参与本项目</a>,贡献其他语言版本的代码,拥抱开源,让更多学习算法的小伙伴们收益!</strong></p>
# 动态规划:一和零!
## 474.一和零 # 474.一和零
[力扣题目链接](https://leetcode.cn/problems/ones-and-zeroes/) [力扣题目链接](https://leetcode.cn/problems/ones-and-zeroes/)
@ -42,7 +41,7 @@
* [动态规划关于01背包问题你该了解这些](https://programmercarl.com/背包理论基础01背包-1.html) * [动态规划关于01背包问题你该了解这些](https://programmercarl.com/背包理论基础01背包-1.html)
* [动态规划关于01背包问题你该了解这些滚动数组](https://programmercarl.com/背包理论基础01背包-2.html) * [动态规划关于01背包问题你该了解这些滚动数组](https://programmercarl.com/背包理论基础01背包-2.html)
这道题目,还是比较难的,也有点像程序员自己给自己出个脑筋急转弯,程序员何苦为难程序员呢哈哈 这道题目,还是比较难的,也有点像程序员自己给自己出个脑筋急转弯,程序员何苦为难程序员呢。
来说题,本题不少同学会认为是多重背包,一些题解也是这么写的。 来说题,本题不少同学会认为是多重背包,一些题解也是这么写的。
@ -155,8 +154,15 @@ public:
不少同学刷过这道提,可能没有总结这究竟是什么背包。 不少同学刷过这道提,可能没有总结这究竟是什么背包。
这道题的本质是有两个维度的01背包如果大家认识到这一点对这道题的理解就比较深入了。 此时我们讲解了0-1背包的多种应用
* [纯 0 - 1 背包](https://programmercarl.com/%E8%83%8C%E5%8C%85%E7%90%86%E8%AE%BA%E5%9F%BA%E7%A1%8001%E8%83%8C%E5%8C%85-2.html) 是求 给定背包容量 装满背包 的最大价值是多少。
* [416. 分割等和子集](https://programmercarl.com/0416.%E5%88%86%E5%89%B2%E7%AD%89%E5%92%8C%E5%AD%90%E9%9B%86.html) 是求 给定背包容量,能不能装满这个背包。
* [1049. 最后一块石头的重量 II](https://programmercarl.com/1049.%E6%9C%80%E5%90%8E%E4%B8%80%E5%9D%97%E7%9F%B3%E5%A4%B4%E7%9A%84%E9%87%8D%E9%87%8FII.html) 是求 给定背包容量,尽可能装,最多能装多少
* [494. 目标和](https://programmercarl.com/0494.%E7%9B%AE%E6%A0%87%E5%92%8C.html) 是求 给定背包容量,装满背包有多少种方法。
* 本题是求 给定背包容量,装满背包最多有多少个物品。
所以在代码随想录中所列举的题目,都是 0-1背包不同维度上的应用大家可以细心体会

View File

@ -3,9 +3,11 @@
<img src="../pics/训练营.png" width="1000"/> <img src="../pics/训练营.png" width="1000"/>
</a> </a>
<p align="center"><strong><a href="https://mp.weixin.qq.com/s/tqCxrMEU-ajQumL1i8im9A">参与本项目</a>,贡献其他语言版本的代码,拥抱开源,让更多学习算法的小伙伴们收益!</strong></p> <p align="center"><strong><a href="https://mp.weixin.qq.com/s/tqCxrMEU-ajQumL1i8im9A">参与本项目</a>,贡献其他语言版本的代码,拥抱开源,让更多学习算法的小伙伴们收益!</strong></p>
# 动态规划:目标和!
## 494. 目标和
# 494. 目标和
[力扣题目链接](https://leetcode.cn/problems/target-sum/) [力扣题目链接](https://leetcode.cn/problems/target-sum/)
@ -52,9 +54,9 @@
既然为target那么就一定有 left组合 - right组合 = target。 既然为target那么就一定有 left组合 - right组合 = target。
left + right等于sum而sum是固定的。 left + right = sum而sum是固定的。right = sum - left
公式来了, left - (sum - left) = target -> left = (target + sum)/2 。 公式来了, left - (sum - left) = target 推导出 left = (target + sum)/2 。
target是固定的sum是固定的left就可以求出来。 target是固定的sum是固定的left就可以求出来。
@ -117,22 +119,26 @@ public:
假设加法的总和为x那么减法对应的总和就是sum - x。 假设加法的总和为x那么减法对应的总和就是sum - x。
所以我们要求的是 x - (sum - x) = S 所以我们要求的是 x - (sum - x) = target
x = (S + sum) / 2 x = (target + sum) / 2
**此时问题就转化为装满容量为x背包有几种方法** **此时问题就转化为装满容量为x背包有几种方法**
大家看到(S + sum) / 2 应该担心计算的过程中向下取整有没有影响 这里的x就是bagSize也就是我们后面要求的背包容量
大家看到(target + sum) / 2 应该担心计算的过程中向下取整有没有影响。
这么担心就对了例如sum 是5S是2的话其实就是无解的所以 这么担心就对了例如sum 是5S是2的话其实就是无解的所以
```CPP ```CPP
C++代码中输入的S 就是题目描述的 target
if ((S + sum) % 2 == 1) return 0; // 此时没有方案 if ((S + sum) % 2 == 1) return 0; // 此时没有方案
``` ```
同时如果 S的绝对值已经大于sum那么也是没有方案的。 同时如果 S的绝对值已经大于sum那么也是没有方案的。
```CPP ```CPP
C++代码中输入的S 就是题目描述的 target
if (abs(S) > sum) return 0; // 此时没有方案 if (abs(S) > sum) return 0; // 此时没有方案
``` ```
@ -156,17 +162,15 @@ dp[j] 表示填满j包括j这么大容积的包有dp[j]种方法
有哪些来源可以推出dp[j]呢? 有哪些来源可以推出dp[j]呢?
不考虑nums[i]的情况下填满容量为j的背包有dp[j]种方法。 只要搞到nums[i]凑成dp[j]就有dp[j - nums[i]] 种方法。
那么考虑nums[i]的话只要搞到nums[i]凑成dp[j]就有dp[j - nums[i]] 种方法。
例如dp[j]j 为5 例如dp[j]j 为5
* 已经有一个1nums[i] 的话,有 dp[4]种方法 凑成 dp[5] * 已经有一个1nums[i] 的话,有 dp[4]种方法 凑成 容量为5的背包
* 已经有一个2nums[i] 的话,有 dp[3]种方法 凑成 dp[5] * 已经有一个2nums[i] 的话,有 dp[3]种方法 凑成 容量为5的背包
* 已经有一个3nums[i] 的话,有 dp[2]中方法 凑成 dp[5] * 已经有一个3nums[i] 的话,有 dp[2]中方法 凑成 容量为5的背包
* 已经有一个4nums[i] 的话,有 dp[1]中方法 凑成 dp[5] * 已经有一个4nums[i] 的话,有 dp[1]中方法 凑成 容量为5的背包
* 已经有一个5 nums[i])的话,有 dp[0]中方法 凑成 dp[5] * 已经有一个5 nums[i])的话,有 dp[0]中方法 凑成 容量为5的背包
那么凑整dp[5]有多少方法呢,也就是把 所有的 dp[j - nums[i]] 累加起来。 那么凑整dp[5]有多少方法呢,也就是把 所有的 dp[j - nums[i]] 累加起来。
@ -182,9 +186,19 @@ dp[j] += dp[j - nums[i]]
从递归公式可以看出在初始化的时候dp[0] 一定要初始化为1因为dp[0]是在公式中一切递推结果的起源如果dp[0]是0的话递归结果将都是0。 从递归公式可以看出在初始化的时候dp[0] 一定要初始化为1因为dp[0]是在公式中一切递推结果的起源如果dp[0]是0的话递归结果将都是0。
dp[0] = 1理论上也很好解释装满容量为0的背包有1种方法就是装0件物品。 这里有录友可能认为从dp数组定义来说 dp[0] 应该是0也有录友认为dp[0]应该是1。
dp[j]其他下标对应的数值应该初始化为0从递归公式也可以看出dp[j]要保证是0的初始值才能正确的由dp[j - nums[i]]推导出来。 其实不要硬去解释它的含义,咱就把 dp[0]的情况带入本题看看就是应该等于多少。
如果数组[0] target = 0那么 bagSize = (target + sum) / 2 = 0。 dp[0]也应该是1 也就是说给数组里的元素 0 前面无论放加法还是减法,都是 1 种方法。
所以本题我们应该初始化 dp[0] 为 1。
可能有同学想了,那 如果是 数组[0,0,0,0,0] target = 0 呢。
其实 此时最终的dp[0] = 32也就是这五个零 子集的所有组合情况但此dp[0]非彼dp[0]dp[0]能算出32其基础是因为dp[0] = 1 累加起来的。
dp[j]其他下标对应的数值也应该初始化为0从递归公式也可以看出dp[j]要保证是0的初始值才能正确的由dp[j - nums[i]]推导出来。
4. 确定遍历顺序 4. 确定遍历顺序
@ -213,7 +227,6 @@ public:
if (abs(S) > sum) return 0; // 此时没有方案 if (abs(S) > sum) return 0; // 此时没有方案
if ((S + sum) % 2 == 1) return 0; // 此时没有方案 if ((S + sum) % 2 == 1) return 0; // 此时没有方案
int bagSize = (S + sum) / 2; int bagSize = (S + sum) / 2;
if (bagsize < 0) return 0;
vector<int> dp(bagSize + 1, 0); vector<int> dp(bagSize + 1, 0);
dp[0] = 1; dp[0] = 1;
for (int i = 0; i < nums.size(); i++) { for (int i = 0; i < nums.size(); i++) {
@ -238,7 +251,7 @@ public:
本题还是有点难度,大家也可以记住,在求装满背包有几种方法的情况下,递推公式一般为: 本题还是有点难度,大家也可以记住,在求装满背包有几种方法的情况下,递推公式一般为:
``` ```CPP
dp[j] += dp[j - nums[i]]; dp[j] += dp[j - nums[i]];
``` ```

View File

@ -3,9 +3,10 @@
<img src="../pics/训练营.png" width="1000"/> <img src="../pics/训练营.png" width="1000"/>
</a> </a>
<p align="center"><strong><a href="https://mp.weixin.qq.com/s/tqCxrMEU-ajQumL1i8im9A">参与本项目</a>,贡献其他语言版本的代码,拥抱开源,让更多学习算法的小伙伴们收益!</strong></p> <p align="center"><strong><a href="https://mp.weixin.qq.com/s/tqCxrMEU-ajQumL1i8im9A">参与本项目</a>,贡献其他语言版本的代码,拥抱开源,让更多学习算法的小伙伴们收益!</strong></p>
# 动态规划:给你一些零钱,你要怎么凑?
## 518. 零钱兑换 II
# 518. 零钱兑换 II
[力扣题目链接](https://leetcode.cn/problems/coin-change-ii/) [力扣题目链接](https://leetcode.cn/problems/coin-change-ii/)
@ -15,22 +16,25 @@
示例 1: 示例 1:
输入: amount = 5, coins = [1, 2, 5] * 输入: amount = 5, coins = [1, 2, 5]
输出: 4 * 输出: 4
解释: 有四种方式可以凑成总金额: 解释: 有四种方式可以凑成总金额:
5=5
5=2+2+1 * 5=5
5=2+1+1+1 * 5=2+2+1
5=1+1+1+1+1 * 5=2+1+1+1
* 5=1+1+1+1+1
示例 2: 示例 2:
输入: amount = 3, coins = [2]
输出: 0 * 输入: amount = 3, coins = [2]
解释: 只用面额2的硬币不能凑成总金额3。 * 输出: 0
* 解释: 只用面额2的硬币不能凑成总金额3。
示例 3: 示例 3:
输入: amount = 10, coins = [10] * 输入: amount = 10, coins = [10]
输出: 1 * 输出: 1
注意,你可以假设: 注意,你可以假设:
@ -47,7 +51,7 @@
对完全背包还不了解的同学,可以看这篇:[动态规划:关于完全背包,你该了解这些!](https://programmercarl.com/背包问题理论基础完全背包.html) 对完全背包还不了解的同学,可以看这篇:[动态规划:关于完全背包,你该了解这些!](https://programmercarl.com/背包问题理论基础完全背包.html)
但本题和纯完全背包不一样,**纯完全背包是能否凑成总金额,而本题是要求凑成总金额的个数!** 但本题和纯完全背包不一样,**纯完全背包是凑成背包最大价值是多少,而本题是要求凑成总金额的物品组合个数!**
注意题目描述中是凑成总金额的硬币组合数,为什么强调是组合数呢? 注意题目描述中是凑成总金额的硬币组合数,为什么强调是组合数呢?
@ -73,17 +77,21 @@ dp[j]凑成总金额j的货币组合数为dp[j]
2. 确定递推公式 2. 确定递推公式
dp[j] 考虑coins[i]的组合总和) 就是所有的dp[j - coins[i]]考虑coins[i])相加。 dp[j] 就是所有的dp[j - coins[i]]考虑coins[i]的情况)相加。
所以递推公式dp[j] += dp[j - coins[i]]; 所以递推公式dp[j] += dp[j - coins[i]];
**这个递推公式大家应该不陌生了我在讲解01背包题目的时候在这篇[动态规划:目标和](https://programmercarl.com/0494.目标和.html)中就讲解了,求装满背包有几种方法,一般公式都是dp[j] += dp[j - nums[i]];** **这个递推公式大家应该不陌生了我在讲解01背包题目的时候在这篇[494. 目标和](https://programmercarl.com/0494.目标和.html)中就讲解了求装满背包有几种方法公式都是dp[j] += dp[j - nums[i]];**
3. dp数组如何初始化 3. dp数组如何初始化
首先dp[0]一定要为1dp[0] = 1是 递归公式的基础。 首先dp[0]一定要为1dp[0] = 1是 递归公式的基础。如果dp[0] = 0 的话后面所有推导出来的值都是0了。
从dp[i]的含义上来讲就是凑成总金额0的货币组合数为1 那么 dp[0] = 1 有没有含义,其实既可以说 凑成总金额0的货币组合数为1也可以说 凑成总金额0的货币组合数为0好像都没有毛病
但题目描述中,也没明确说 amount = 0 的情况,结果应该是多少。
这里我认为题目描述还是要说明一下因为后台测试数据是默认amount = 0 的情况组合数为1的。
下标非0的dp[j]初始化为0这样累计加dp[j - coins[i]]的时候才不会影响真正的dp[j] 下标非0的dp[j]初始化为0这样累计加dp[j - coins[i]]的时候才不会影响真正的dp[j]
@ -96,9 +104,9 @@ dp[j] 考虑coins[i]的组合总和) 就是所有的dp[j - coins[i]](不
**但本题就不行了!** **但本题就不行了!**
因为纯完全背包求得是能否凑成总和,和凑成总和的元素有没有顺序没关系,即:有顺序也行,没有顺序也行! 因为纯完全背包求得装满背包的最大价值是多少,和凑成总和的元素有没有顺序没关系,即:有顺序也行,没有顺序也行!
而本题要求凑成总和的组合数,元素之间要求没有顺序。 而本题要求凑成总和的组合数,元素之间明确要求没有顺序。
所以纯完全背包是能凑成总和就行,不用管怎么凑的。 所以纯完全背包是能凑成总和就行,不用管怎么凑的。
@ -126,7 +134,7 @@ for (int i = 0; i < coins.size(); i++) { // 遍历物品
如果把两个for交换顺序代码如下 如果把两个for交换顺序代码如下
``` ```CPP
for (int j = 0; j <= amount; j++) { // 遍历背包容量 for (int j = 0; j <= amount; j++) { // 遍历背包容量
for (int i = 0; i < coins.size(); i++) { // 遍历物品 for (int i = 0; i < coins.size(); i++) { // 遍历物品
if (j - coins[i] >= 0) dp[j] += dp[j - coins[i]]; if (j - coins[i] >= 0) dp[j] += dp[j - coins[i]];
@ -169,7 +177,7 @@ public:
## 总结 ## 总结
本题的递推公式,其实我们在[动态规划:目标和](https://programmercarl.com/0494.目标和.html)中就已经讲过了,**而难点在于遍历顺序!** 本题的递推公式,其实我们在[494. 目标和](https://programmercarl.com/0494.目标和.html)中就已经讲过了,**而难点在于遍历顺序!**
在求装满背包有几种方案的时候,认清遍历顺序是非常关键的。 在求装满背包有几种方案的时候,认清遍历顺序是非常关键的。
@ -181,8 +189,6 @@ public:
## 其他语言版本 ## 其他语言版本

View File

@ -294,6 +294,8 @@ func reverseStr(s string, k int) string {
ss := []byte(s) ss := []byte(s)
length := len(s) length := len(s)
for i := 0; i < length; i += 2 * k { for i := 0; i < length; i += 2 * k {
// 1. 每隔 2k 个字符的前 k 个字符进行反转
// 2. 剩余字符小于 2k 但大于或等于 k 个,则反转前 k 个字符
if i + k <= length { if i + k <= length {
reverse(ss[i:i+k]) reverse(ss[i:i+k])
} else { } else {
@ -326,7 +328,7 @@ javaScript:
var reverseStr = function(s, k) { var reverseStr = function(s, k) {
const len = s.length; const len = s.length;
let resArr = s.split(""); let resArr = s.split("");
for(let i = 0; i < len; i += 2 * k) { for(let i = 0; i < len; i += 2 * k) { // 每隔 2k 个字符的前 k 个字符进行反转
let l = i - 1, r = i + k > len ? len : i + k; let l = i - 1, r = i + k > len ? len : i + k;
while(++l < --r) [resArr[l], resArr[r]] = [resArr[r], resArr[l]]; while(++l < --r) [resArr[l], resArr[r]] = [resArr[r], resArr[l]];
} }

View File

@ -50,7 +50,7 @@
![1047.删除字符串中的所有相邻重复项](https://code-thinking.cdn.bcebos.com/gifs/1047.删除字符串中的所有相邻重复项.gif) ![1047.删除字符串中的所有相邻重复项](https://code-thinking.cdn.bcebos.com/gifs/1047.删除字符串中的所有相邻重复项.gif)
从栈中弹出剩余元素此时是字符串ac因为从栈里弹出的元素是倒序的所以对字符串进行反转一下,就得到了最终的结果。 从栈中弹出剩余元素此时是字符串ac因为从栈里弹出的元素是倒序的所以对字符串进行反转一下,就得到了最终的结果。
C++代码 : C++代码 :
@ -102,9 +102,9 @@ public:
## 题外话 ## 题外话
这道题目就像是我们玩过的游戏对对碰,如果相同的元素放在挨在一起就要消除。 这道题目就像是我们玩过的游戏对对碰,如果相同的元素挨在一起就要消除。
可能我们在玩游戏的时候感觉理所当然应该消除,但程序又怎么知道该如消除呢,特别是消除之后又有新的元素可能挨在一起。 可能我们在玩游戏的时候感觉理所当然应该消除,但程序又怎么知道该如消除呢,特别是消除之后又有新的元素可能挨在一起。
此时游戏的后端逻辑就可以用一个栈来实现(我没有实际考察对对碰或者爱消除游戏的代码实现,仅从原理上进行推断)。 此时游戏的后端逻辑就可以用一个栈来实现(我没有实际考察对对碰或者爱消除游戏的代码实现,仅从原理上进行推断)。

View File

@ -15,17 +15,20 @@
每一回合从中选出任意两块石头然后将它们一起粉碎。假设石头的重量分别为 x 和 y x <= y。那么粉碎的可能结果如下 每一回合从中选出任意两块石头然后将它们一起粉碎。假设石头的重量分别为 x 和 y x <= y。那么粉碎的可能结果如下
如果 x == y那么两块石头都会被完全粉碎 如果 x == y那么两块石头都会被完全粉碎
如果 x != y那么重量为 x 的石头将会完全粉碎而重量为 y 的石头新重量为 y-x。 如果 x != y那么重量为 x 的石头将会完全粉碎而重量为 y 的石头新重量为 y-x。
最后,最多只会剩下一块石头。返回此石头最小的可能重量。如果没有石头剩下,就返回 0。 最后,最多只会剩下一块石头。返回此石头最小的可能重量。如果没有石头剩下,就返回 0。
示例: 示例:
输入:[2,7,4,1,8,1] * 输入:[2,7,4,1,8,1]
输出1 * 输出1
解释: 解释:
组合 2 和 4得到 2所以数组转化为 [2,7,1,8,1] * 组合 2 和 4得到 2所以数组转化为 [2,7,1,8,1]
组合 7 和 8得到 1所以数组转化为 [2,1,1,1] * 组合 7 和 8得到 1所以数组转化为 [2,1,1,1]
组合 2 和 1得到 1所以数组转化为 [1,1,1] * 组合 2 和 1得到 1所以数组转化为 [1,1,1]
组合 1 和 1得到 0所以数组转化为 [1],这就是最优值。 * 组合 1 和 1得到 0所以数组转化为 [1],这就是最优值。
提示: 提示:
@ -51,7 +54,11 @@
1. 确定dp数组以及下标的含义 1. 确定dp数组以及下标的含义
**dp[j]表示容量这里说容量更形象其实就是重量为j的背包最多可以背dp[j]这么重的石头** **dp[j]表示容量这里说容量更形象其实就是重量为j的背包最多可以背最大重量为dp[j]**
可以回忆一下01背包中dp[j]的含义容量为j的背包最多可以装的价值为 dp[j]。
相对于 01背包本题中石头的重量是 stones[i],石头的价值也是 stones[i] ,可以 “最多可以装的价值为 dp[j]” == “最多可以背的重量为dp[j]”
2. 确定递推公式 2. 确定递推公式
@ -61,7 +68,7 @@
一些同学可能看到这dp[j - stones[i]] + stones[i]中 又有- stones[i] 又有+stones[i],看着有点晕乎。 一些同学可能看到这dp[j - stones[i]] + stones[i]中 又有- stones[i] 又有+stones[i],看着有点晕乎。
还是要牢记dp[j]的含义要知道dp[j - stones[i]]为 容量为j - stones[i]的背包最大所背重量 大家可以再去看 dp[j]的含义
3. dp数组如何初始化 3. dp数组如何初始化

View File

@ -36,7 +36,7 @@ i指向新长度的末尾j指向旧长度的末尾。
这么做有两个好处: 这么做有两个好处:
1. 不用申请新数组。 1. 不用申请新数组。
2. 从后向前填充元素,避免了从前后填充元素要来的 每次添加元素都要将添加元素之后的所有元素向后移动。 2. 从后向前填充元素,避免了从前后填充元素时,每次添加元素都要将添加元素之后的所有元素向后移动的问题
时间复杂度空间复杂度均超过100%的用户。 时间复杂度空间复杂度均超过100%的用户。

View File

@ -31,7 +31,7 @@
不能使用额外空间的话模拟在本串操作要实现左旋转字符串的功能还是有点困难的 不能使用额外空间的话模拟在本串操作要实现左旋转字符串的功能还是有点困难的
那么我们可以想一下上一题目[字符串:花式反转还不够!](https://programmercarl.com/0151.翻转字符串里的单词.html)中讲过使用整体反转+局部反转就可以实现反转单词顺序的目的 那么我们可以想一下上一题目[字符串:花式反转还不够!](https://programmercarl.com/0151.翻转字符串里的单词.html)中讲过使用整体反转+局部反转就可以实现反转单词顺序的目的
这道题目也非常类似依然可以通过局部反转+整体反转 达到左旋转的目的 这道题目也非常类似依然可以通过局部反转+整体反转 达到左旋转的目的
@ -41,7 +41,7 @@
2. 反转区间为n到末尾的子串 2. 反转区间为n到末尾的子串
3. 反转整个字符串 3. 反转整个字符串
最后就可以到左旋n的目的而不用定义新的字符串完全在本串上操作 最后就可以到左旋n的目的而不用定义新的字符串完全在本串上操作
例如 示例1中 输入字符串abcdefgn=2 例如 示例1中 输入字符串abcdefgn=2
@ -75,7 +75,7 @@ public:
在这篇文章[344.反转字符串](https://programmercarl.com/0344.反转字符串.html),第一次讲到反转一个字符串应该怎么做,使用了双指针法。 在这篇文章[344.反转字符串](https://programmercarl.com/0344.反转字符串.html),第一次讲到反转一个字符串应该怎么做,使用了双指针法。
然后发现[541. 反转字符串II](https://programmercarl.com/0541.反转字符串II.html),这里开始给反转加上了一些条件,当需要固定规律一段一段去处理字符串的时候,要想想在for循环的表达式上做做文章。 然后发现[541. 反转字符串II](https://programmercarl.com/0541.反转字符串II.html)这里开始给反转加上了一些条件当需要固定规律一段一段去处理字符串的时候要想想在for循环的表达式上做做文章。
后来在[151.翻转字符串里的单词](https://programmercarl.com/0151.翻转字符串里的单词.html)中,要对一句话里的单词顺序进行反转,发现先整体反转再局部反转 是一个很妙的思路。 后来在[151.翻转字符串里的单词](https://programmercarl.com/0151.翻转字符串里的单词.html)中,要对一句话里的单词顺序进行反转,发现先整体反转再局部反转 是一个很妙的思路。

View File

@ -108,7 +108,7 @@ std::unordered_map 底层实现为哈希表std::map 和std::multimap 的底
其他语言例如java里的HashMap TreeMap 都是一样的原理。可以灵活贯通。 其他语言例如java里的HashMap TreeMap 都是一样的原理。可以灵活贯通。
虽然std::set、std::multiset 的底层实现是红黑树,不是哈希表,但是std::set、std::multiset 依然使用哈希函数来做映射,只不过底层的符号表使用了红黑树来存储数据,所以使用这些数据结构来解决映射问题的方法,我们依然称之为哈希法。 map也是一样的道理。 虽然std::set、std::multiset 的底层实现是红黑树不是哈希表std::set、std::multiset 使用红黑树来索引和存储不过给我们的使用方式还是哈希法的使用方式即key和value。所以使用这些数据结构来解决映射问题的方法,我们依然称之为哈希法。 map也是一样的道理。
这里在说一下一些C++的经典书籍上 例如STL源码剖析说到了hash_set hash_map这个与unordered_setunordered_map又有什么关系呢 这里在说一下一些C++的经典书籍上 例如STL源码剖析说到了hash_set hash_map这个与unordered_setunordered_map又有什么关系呢

View File

@ -6,10 +6,18 @@
# 深度优先搜索理论基础 # 深度优先搜索理论基础
提到深度优先搜索dfs就不得不说和广度优先有什么区别bfs 录友们期待图论内容已久了,为什么鸽了这么久,主要是最近半年开始更新[代码随想录算法公开课](https://mp.weixin.qq.com/s/xncn6IHJGs45sJOChN6V_g)是开源在B站的算法视频已经帮助非常多基础不好的录友学习算法。
录视频其实是非常累的,也要花很多时间,所以图论这边就没抽出时间来。
后面计划先给大家讲图论里大家特别需要的深搜和广搜。
以下,开始讲解深度优先搜索理论基础:
## dfs 与 bfs 区别 ## dfs 与 bfs 区别
提到深度优先搜索dfs就不得不说和广度优先有什么区别bfs
先来了解dfs的过程很多录友可能对dfs深度优先搜索bfs广度优先搜索分不清。 先来了解dfs的过程很多录友可能对dfs深度优先搜索bfs广度优先搜索分不清。
先给大家说一下两者大概的区别: 先给大家说一下两者大概的区别:
@ -35,7 +43,7 @@
![图三](https://code-thinking-1253855093.file.myqcloud.com/pics/20220707094011.png) ![图三](https://code-thinking-1253855093.file.myqcloud.com/pics/20220707094011.png)
路径2撤销了改变了方向走路径3红色线 接着也找到终点6。 那么撤销路径2改为路径3在dfs中其实就是回溯的过程这一点很重要很多录友,都不理解dfs代码中回溯是用来干什么的 路径2撤销了改变了方向走路径3红色线 接着也找到终点6。 那么撤销路径2改为路径3在dfs中其实就是回溯的过程这一点很重要很多录友不理解dfs代码中回溯是用来干什么的
又找到了一条从节点1到节点6的路径又到黄河了此时再回头下图图四中路径4撤销回溯的过程改为路径5。 又找到了一条从节点1到节点6的路径又到黄河了此时再回头下图图四中路径4撤销回溯的过程改为路径5。
@ -55,7 +63,6 @@
* 搜索方向,是认准一个方向搜,直到碰壁之后在换方向 * 搜索方向,是认准一个方向搜,直到碰壁之后在换方向
* 换方向是撤销原路径,改为节点链接的下一个路径,回溯的过程。 * 换方向是撤销原路径,改为节点链接的下一个路径,回溯的过程。
## 代码框架 ## 代码框架
正式因为dfs搜索可一个方向并需要回溯所以用递归的方式来实现是最方便的。 正式因为dfs搜索可一个方向并需要回溯所以用递归的方式来实现是最方便的。
@ -65,6 +72,7 @@
有递归的地方就有回溯,那么回溯在哪里呢? 有递归的地方就有回溯,那么回溯在哪里呢?
就地递归函数的下面,例如如下代码: 就地递归函数的下面,例如如下代码:
``` ```
void dfs(参数) { void dfs(参数) {
处理节点 处理节点
@ -160,8 +168,6 @@ if (终止条件) {
终止添加不仅是结束本层递归,同时也是我们收获结果的时候。 终止添加不仅是结束本层递归,同时也是我们收获结果的时候。
另外其实很多dfs写法没有写终止条件其实终止条件写在了 下面dfs递归的逻辑里了也就是不符合条件直接不会向下递归。这里如果大家不理解的话没关系后面会有具体题目来讲解。 另外其实很多dfs写法没有写终止条件其实终止条件写在了 下面dfs递归的逻辑里了也就是不符合条件直接不会向下递归。这里如果大家不理解的话没关系后面会有具体题目来讲解。
* 841.钥匙和房间
* 200. 岛屿数量
3. 处理目前搜索节点出发的路径 3. 处理目前搜索节点出发的路径
@ -190,6 +196,9 @@ for (选择:本节点所连接的其他节点) {
以上如果大家都能理解了,其实搜索的代码就很好写,具体题目套用具体场景就可以了。 以上如果大家都能理解了,其实搜索的代码就很好写,具体题目套用具体场景就可以了。
后面我也会给大家安排具体练习的题目,依旧是代码随想录的风格,循序渐进由浅入深!
<p align="center"> <p align="center">
<a href="https://programmercarl.com/other/kstar.html" target="_blank"> <a href="https://programmercarl.com/other/kstar.html" target="_blank">
<img src="../pics/网站星球宣传海报.jpg" width="1000"/> <img src="../pics/网站星球宣传海报.jpg" width="1000"/>

View File

@ -12,7 +12,7 @@
![栈与队列理论1](https://img-blog.csdnimg.cn/20210104235346563.png) ![栈与队列理论1](https://img-blog.csdnimg.cn/20210104235346563.png)
那么我这里列出四个关于栈的问题大家可以思考一下。以下是以C++为例,相信使用其他编程语言的同学也对应思考一下,自己使用的编程语言里栈和队列是什么样的。 那么我这里列出四个关于栈的问题大家可以思考一下。以下是以C++为例,使用其他编程语言的同学也对应思考一下,自己使用的编程语言里栈和队列是什么样的。
1. C++中stack 是容器么? 1. C++中stack 是容器么?
2. 我们使用的stack是属于哪个版本的STL 2. 我们使用的stack是属于哪个版本的STL
@ -23,7 +23,7 @@
有的同学可能仅仅知道有栈和队列这么个数据结构却不知道底层实现也不清楚所使用栈和队列和STL是什么关系。 有的同学可能仅仅知道有栈和队列这么个数据结构却不知道底层实现也不清楚所使用栈和队列和STL是什么关系。
所以这里我给大家扫一遍基础知识, 所以这里我给大家扫一遍基础知识,
首先大家要知道 栈和队列是STLC++标准库)里面的两个数据结构。 首先大家要知道 栈和队列是STLC++标准库)里面的两个数据结构。
@ -83,7 +83,7 @@ std::queue<int, std::list<int>> third; // 定义以list为底层容器的队列
所以STL 队列也不被归类为容器而被归类为container adapter 容器适配器)。 所以STL 队列也不被归类为容器而被归类为container adapter 容器适配器)。
我这里讲的都是C++ 语言中情况, 使用其他语言的同学也要思考栈与队列的底层实现问题, 不要对数据结构的使用浅尝辄止,而要深挖内部原理,才能夯实基础。 我这里讲的都是C++ 语言中情况, 使用其他语言的同学也要思考栈与队列的底层实现问题, 不要对数据结构的使用浅尝辄止,而要深挖内部原理,才能夯实基础。

View File

@ -82,7 +82,7 @@ dp状态图如下
就知道了01背包中二维dp数组的两个for遍历的先后循序是可以颠倒了一维dp数组的两个for循环先后循序一定是先遍历物品再遍历背包容量。 就知道了01背包中二维dp数组的两个for遍历的先后循序是可以颠倒了一维dp数组的两个for循环先后循序一定是先遍历物品再遍历背包容量。
**在完全背包中对于一维dp数组来说其实两个for循环嵌套顺序同样无所谓!** **在完全背包中对于一维dp数组来说其实两个for循环嵌套顺序无所谓**
因为dp[j] 是根据 下标j之前所对应的dp[j]计算出来的。 只要保证下标j之前的dp[j]都是经过计算的就可以了。 因为dp[j] 是根据 下标j之前所对应的dp[j]计算出来的。 只要保证下标j之前的dp[j]都是经过计算的就可以了。