diff --git a/problems/0020.有效的括号.md b/problems/0020.有效的括号.md index 69713c62..737fab86 100644 --- a/problems/0020.有效的括号.md +++ b/problems/0020.有效的括号.md @@ -76,7 +76,7 @@ cd a/b/c/../../ **一些同学,在面试中看到这种题目上来就开始写代码,然后就越写越乱。** -建议要写代码之前要分析好有哪几种不匹配的情况,如果不动手之前分析好,写出的代码也会有很多问题。 +建议在写代码之前要分析好有哪几种不匹配的情况,如果不在动手之前分析好,写出的代码也会有很多问题。 先来分析一下 这里有三种不匹配的情况, diff --git a/problems/0028.实现strStr.md b/problems/0028.实现strStr.md index 19d16e9f..fc222441 100644 --- a/problems/0028.实现strStr.md +++ b/problems/0028.实现strStr.md @@ -94,7 +94,7 @@ next数组就是一个前缀表(prefix table)。 **前缀表是用来回退的,它记录了模式串与主串(文本串)不匹配的时候,模式串应该从哪里开始重新匹配。** -为了清楚的了解前缀表的来历,我们来举一个例子: +为了清楚地了解前缀表的来历,我们来举一个例子: 要在文本串: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) -动画里,我特意把 子串`aa` 标记上了,这是有原因的,大家先注意一下,后面还会说道。 +动画里,我特意把 子串`aa` 标记上了,这是有原因的,大家先注意一下,后面还会说到。 -可以看出,文本串中第六个字符b 和 模式串的第六个字符f,不匹配了。如果暴力匹配,会发现不匹配,此时就要从头匹配了。 +可以看出,文本串中第六个字符b 和 模式串的第六个字符f,不匹配了。如果暴力匹配,发现不匹配,此时就要从头匹配了。 但如果使用前缀表,就不会从头匹配,而是从上次已经匹配的内容开始匹配,找到了模式串中第三个字符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)的。 -暴力的解法显而易见是O(n × m),所以**KMP在字符串匹配中极大的提高的搜索的效率。** +暴力的解法显而易见是O(n × m),所以**KMP在字符串匹配中极大地提高了搜索的效率。** 为了和力扣题目28.实现strStr保持一致,方便大家理解,以下文章统称haystack为文本串, needle为模式串。 @@ -251,7 +251,7 @@ void getNext(int* next, const string& s) 2. 处理前后缀不相同的情况 3. 处理前后缀相同的情况 -接下来我们详解详解一下。 +接下来我们详解一下。 1. 初始化: @@ -613,12 +613,12 @@ class Solution { public void getNext(int[] next, String s){ int j = -1; next[0] = j; - for (int i = 1; i=0 && s.charAt(i) != s.charAt(j+1)){ + for (int i = 1; i < s.length(); i++){ + while(j >= 0 && s.charAt(i) != s.charAt(j+1)){ j=next[j]; } - if(s.charAt(i)==s.charAt(j+1)){ + if(s.charAt(i) == s.charAt(j+1)){ j++; } next[i] = j; @@ -632,14 +632,14 @@ class Solution { int[] next = new int[needle.length()]; getNext(next, needle); int j = -1; - for(int i = 0; i=0 && haystack.charAt(i) != needle.charAt(j+1)){ j = next[j]; } - if(haystack.charAt(i)==needle.charAt(j+1)){ + if(haystack.charAt(i) == needle.charAt(j+1)){ j++; } - if(j==needle.length()-1){ + if(j == needle.length()-1){ return (i-needle.length()+1); } } @@ -694,9 +694,9 @@ class Solution(object): :type needle: str :rtype: int """ - m,n=len(haystack),len(needle) + m, n = len(haystack), len(needle) for i in range(m): - if haystack[i:i+n]==needle: + if haystack[i:i+n] == needle: return i return -1 ``` @@ -704,31 +704,31 @@ class Solution(object): // 方法一 class Solution: def strStr(self, haystack: str, needle: str) -> int: - a=len(needle) - b=len(haystack) - if a==0: + a = len(needle) + b = len(haystack) + if a == 0: return 0 - next=self.getnext(a,needle) + next = self.getnext(a,needle) p=-1 for j in range(b): - while p>=0 and needle[p+1]!=haystack[j]: - p=next[p] - if needle[p+1]==haystack[j]: - p+=1 - if p==a-1: + while p >= 0 and needle[p+1] != haystack[j]: + p = next[p] + if needle[p+1] == haystack[j]: + p += 1 + if p == a-1: return j-a+1 return -1 def getnext(self,a,needle): - next=['' for i in range(a)] - k=-1 - next[0]=k - for i in range(1,len(needle)): - while (k>-1 and needle[k+1]!=needle[i]): - k=next[k] - if needle[k+1]==needle[i]: - k+=1 - next[i]=k + next = ['' for i in range(a)] + k = -1 + next[0] = k + for i in range(1, len(needle)): + while (k > -1 and needle[k+1] != needle[i]): + k = next[k] + if needle[k+1] == needle[i]: + k += 1 + next[i] = k return next ``` @@ -736,34 +736,34 @@ class Solution: // 方法二 class Solution: def strStr(self, haystack: str, needle: str) -> int: - a=len(needle) - b=len(haystack) - if a==0: + a = len(needle) + b = len(haystack) + if a == 0: return 0 - i=j=0 - next=self.getnext(a,needle) - while(i= 0 && s[i] != s[j+1] { - j = next[j] // 回退前一位 + j = next[j] // 回退前一位 } if s[i] == s[j+1] { j++ } - next[i] = j // next[i]是i(包括i)之前的最长相等前后缀长度 + next[i] = j // next[i]是i(包括i)之前的最长相等前后缀长度 } } func strStr(haystack string, needle string) int { @@ -796,15 +796,15 @@ func strStr(haystack string, needle string) int { } next := make([]int, len(needle)) getNext(next, needle) - j := -1 // 模式串的起始位置 next为-1 因此也为-1 + j := -1 // 模式串的起始位置 next为-1 因此也为-1 for i := 0; i < len(haystack); i++ { for j >= 0 && haystack[i] != needle[j+1] { - j = next[j] // 寻找下一个匹配点 + j = next[j] // 寻找下一个匹配点 } if haystack[i] == needle[j+1] { j++ } - if j == len(needle)-1 { // j指向了模式串的末尾 + if j == len(needle)-1 { // j指向了模式串的末尾 return i - len(needle) + 1 } } @@ -842,7 +842,7 @@ func strStr(haystack string, needle string) int { getNext(next, needle) for i := 0; i < len(haystack); i++ { for j > 0 && haystack[i] != needle[j] { - j = next[j-1] // 回退到j的前一位 + j = next[j-1] // 回退到j的前一位 } if haystack[i] == needle[j] { j++ diff --git a/problems/0101.对称二叉树.md b/problems/0101.对称二叉树.md index 3bdd862f..40e3e18e 100644 --- a/problems/0101.对称二叉树.md +++ b/problems/0101.对称二叉树.md @@ -21,7 +21,7 @@ 对于二叉树是否对称,要比较的是根节点的左子树与右子树是不是相互翻转的,理解这一点就知道了**其实我们要比较的是两个树(这两个树是根节点的左右子树)**,所以在递归遍历的过程中,也是要同时遍历两棵树。 -那么如果比较呢? +那么如何比较呢? 比较的是两个子树的里侧和外侧的元素是否相等。如图所示: @@ -80,7 +80,7 @@ else if (left == NULL && right == NULL) return true; else if (left->val != right->val) return false; // 注意这里我没有使用else ``` -注意上面最后一种情况,我没有使用else,而是elseif, 因为我们把以上情况都排除之后,剩下的就是 左右节点都不为空,且数值相同的情况。 +注意上面最后一种情况,我没有使用else,而是else if, 因为我们把以上情况都排除之后,剩下的就是 左右节点都不为空,且数值相同的情况。 3. 确定单层递归的逻辑 @@ -244,7 +244,7 @@ public: 这次我们又深度剖析了一道二叉树的“简单题”,大家会发现,真正的把题目搞清楚其实并不简单,leetcode上accept了和真正掌握了还是有距离的。 -我们介绍了递归法和迭代法,递归依然通过递归三部曲来解决了这道题目,如果只看精简的代码根本看不出来递归三部曲是如果解题的。 +我们介绍了递归法和迭代法,递归依然通过递归三部曲来解决了这道题目,如果只看精简的代码根本看不出来递归三部曲是如何解题的。 在迭代法中我们使用了队列,需要注意的是这不是层序遍历,而且仅仅通过一个容器来成对的存放我们要比较的元素,知道这一本质之后就发现,用队列,用栈,甚至用数组,都是可以的。 @@ -259,7 +259,7 @@ public: # 其他语言版本 -## Java +Java ```Java /** @@ -364,7 +364,7 @@ public: ``` -## Python +Python 递归法: ```python @@ -464,8 +464,7 @@ class Solution: return True ``` -## Go - +Go ```go /** * Definition for a binary tree node. @@ -488,10 +487,12 @@ func defs(left *TreeNode, right *TreeNode) bool { } return defs(left.Left, right.Right) && defs(right.Left, left.Right); } + func isSymmetric(root *TreeNode) bool { return defs(root.Left, root.Right); } + // 迭代 func isSymmetric(root *TreeNode) bool { var queue []*TreeNode; @@ -515,59 +516,60 @@ func isSymmetric(root *TreeNode) bool { ``` -## JavaScript +JavaScript 递归判断是否为对称二叉树: ```javascript var isSymmetric = function(root) { - //使用递归遍历左右子树 递归三部曲 + // 使用递归遍历左右子树 递归三部曲 // 1. 确定递归的参数 root.left root.right和返回值true false - const compareNode=function(left,right){ - //2. 确定终止条件 空的情况 - if(left===null&&right!==null||left!==null&&right===null){ + const compareNode = function(left, right) { + // 2. 确定终止条件 空的情况 + if(left === null && right !== null || left !== null && right === null) { return false; - }else if(left===null&&right===null){ + } else if(left === null && right === null) { return true; - }else if(left.val!==right.val){ + } else if(left.val !== right.val) { return false; } - //3. 确定单层递归逻辑 - let outSide=compareNode(left.left,right.right); - let inSide=compareNode(left.right,right.left); - return outSide&&inSide; + // 3. 确定单层递归逻辑 + let outSide = compareNode(left.left, right.right); + let inSide = compareNode(left.right, right.left); + return outSide && inSide; } - if(root===null){ + if(root === null) { return true; } - return compareNode(root.left,root.right); + return compareNode(root.left, root.right); }; ``` 队列实现迭代判断是否为对称二叉树: ```javascript var isSymmetric = function(root) { - //迭代方法判断是否是对称二叉树 - //首先判断root是否为空 - if(root===null){ + // 迭代方法判断是否是对称二叉树 + // 首先判断root是否为空 + if(root === null) { return true; } - let queue=[]; + let queue = []; queue.push(root.left); queue.push(root.right); - while(queue.length){ - let leftNode=queue.shift();//左节点 - let rightNode=queue.shift();//右节点 - if(leftNode===null&&rightNode===null){ + while(queue.length) { + let leftNode = queue.shift(); //左节点 + let rightNode = queue.shift(); //右节点 + if(leftNode === null && rightNode === null) { continue; } - if(leftNode===null||rightNode===null||leftNode.val!==rightNode.val){ + if(leftNode === null || rightNode === null || leftNode.val !== rightNode.val) { return false; } - queue.push(leftNode.left);//左节点左孩子入队 - queue.push(rightNode.right);//右节点右孩子入队 - queue.push(leftNode.right);//左节点右孩子入队 - queue.push(rightNode.left);//右节点左孩子入队 + queue.push(leftNode.left); //左节点左孩子入队 + queue.push(rightNode.right); //右节点右孩子入队 + queue.push(leftNode.right); //左节点右孩子入队 + queue.push(rightNode.left); //右节点左孩子入队 } + return true; }; ``` @@ -575,33 +577,34 @@ var isSymmetric = function(root) { 栈实现迭代判断是否为对称二叉树: ```javascript var isSymmetric = function(root) { - //迭代方法判断是否是对称二叉树 - //首先判断root是否为空 - if(root===null){ + // 迭代方法判断是否是对称二叉树 + // 首先判断root是否为空 + if(root === null) { return true; } - let stack=[]; + let stack = []; stack.push(root.left); stack.push(root.right); - while(stack.length){ - let rightNode=stack.pop();//左节点 - let leftNode=stack.pop();//右节点 - if(leftNode===null&&rightNode===null){ + while(stack.length) { + let rightNode = stack.pop(); //左节点 + let leftNode=stack.pop(); //右节点 + if(leftNode === null && rightNode === null) { continue; } - if(leftNode===null||rightNode===null||leftNode.val!==rightNode.val){ + if(leftNode === null || rightNode === null || leftNode.val !== rightNode.val) { return false; } - stack.push(leftNode.left);//左节点左孩子入队 - stack.push(rightNode.right);//右节点右孩子入队 - stack.push(leftNode.right);//左节点右孩子入队 - stack.push(rightNode.left);//右节点左孩子入队 + stack.push(leftNode.left); //左节点左孩子入队 + stack.push(rightNode.right); //右节点右孩子入队 + stack.push(leftNode.right); //左节点右孩子入队 + stack.push(rightNode.left); //右节点左孩子入队 } + return true; }; ``` -## TypeScript: +TypeScript: > 递归法 @@ -670,7 +673,7 @@ function isSymmetric(root: TreeNode | null): boolean { }; ``` -## Swift: +Swift: > 递归 ```swift @@ -752,7 +755,7 @@ func isSymmetric3(_ root: TreeNode?) -> Bool { } ``` -## Scala +Scala > 递归: ```scala diff --git a/problems/0102.二叉树的层序遍历.md b/problems/0102.二叉树的层序遍历.md index 1a01c0ae..9985971f 100644 --- a/problems/0102.二叉树的层序遍历.md +++ b/problems/0102.二叉树的层序遍历.md @@ -47,7 +47,7 @@ 层序遍历一个二叉树。就是从左到右一层一层的去遍历二叉树。这种遍历的方式和我们之前讲过的都不太一样。 -需要借用一个辅助数据结构即队列来实现,**队列先进先出,符合一层一层遍历的逻辑,而是用栈先进后出适合模拟深度优先遍历也就是递归的逻辑。** +需要借用一个辅助数据结构即队列来实现,**队列先进先出,符合一层一层遍历的逻辑,而用栈先进后出适合模拟深度优先遍历也就是递归的逻辑。** **而这种层序遍历方式就是图论中的广度优先遍历,只不过我们应用在二叉树上。** @@ -106,50 +106,6 @@ public: }; ``` -python3代码: - - -```python - -class Solution: - """二叉树层序遍历迭代解法""" - - def levelOrder(self, root: TreeNode) -> List[List[int]]: - results = [] - if not root: - return results - - from collections import deque - que = deque([root]) - - while que: - size = len(que) - result = [] - for _ in range(size): - cur = que.popleft() - result.append(cur.val) - if cur.left: - que.append(cur.left) - if cur.right: - que.append(cur.right) - results.append(result) - - return results -``` -```python -# 递归法 -class Solution: - def levelOrder(self, root: TreeNode) -> List[List[int]]: - res = [] - def helper(root, depth): - if not root: return [] - if len(res) == depth: res.append([]) # start the current depth - res[depth].append(root.val) # fulfil the current depth - if root.left: helper(root.left, depth + 1) # process child nodes for the next depth - if root.right: helper(root.right, depth + 1) - helper(root, 0) - return res -``` java: ```Java @@ -206,6 +162,51 @@ class Solution { } ``` +python3代码: + + +```python + +class Solution: + """二叉树层序遍历迭代解法""" + + def levelOrder(self, root: TreeNode) -> List[List[int]]: + results = [] + if not root: + return results + + from collections import deque + que = deque([root]) + + while que: + size = len(que) + result = [] + for _ in range(size): + cur = que.popleft() + result.append(cur.val) + if cur.left: + que.append(cur.left) + if cur.right: + que.append(cur.right) + results.append(result) + + return results +``` +```python +# 递归法 +class Solution: + def levelOrder(self, root: TreeNode) -> List[List[int]]: + res = [] + def helper(root, depth): + if not root: return [] + if len(res) == depth: res.append([]) # start the current depth + res[depth].append(root.val) # fulfil the current depth + if root.left: helper(root.left, depth + 1) # process child nodes for the next depth + if root.right: helper(root.right, depth + 1) + helper(root, 0) + return res +``` + go: ```go @@ -243,28 +244,31 @@ func levelOrder(root *TreeNode) [][]int { 102. 二叉树的层序遍历 */ func levelOrder(root *TreeNode) [][]int { - res:=[][]int{} - if root==nil{//防止为空 + res := [][]int{} + if root == nil{//防止为空 return res } - queue:=list.New() + queue := list.New() queue.PushBack(root) + var tmpArr []int - for queue.Len()>0 { - length:=queue.Len()//保存当前层的长度,然后处理当前层(十分重要,防止添加下层元素影响判断层中元素的个数) - for i:=0;i 0 { + length := queue.Len() //保存当前层的长度,然后处理当前层(十分重要,防止添加下层元素影响判断层中元素的个数) + for i := 0; i < length; i++ { + node := queue.Remove(queue.Front()).(*TreeNode) //出队列 + if node.Left != nil { queue.PushBack(node.Left) } - if node.Right!=nil{ + if node.Right != nil { queue.PushBack(node.Right) } - tmpArr=append(tmpArr,node.Val)//将值加入本层切片中 + tmpArr = append(tmpArr, node.Val) //将值加入本层切片中 } - res=append(res,tmpArr)//放入结果集 - tmpArr=[]int{}//清空层的数据 + res = append(res, tmpArr) //放入结果集 + tmpArr = []int{} //清空层的数据 } + return res } ``` @@ -274,22 +278,22 @@ javascript代码: ```javascript var levelOrder = function(root) { //二叉树的层序遍历 - let res=[],queue=[]; + let res = [], queue = []; queue.push(root); - if(root===null){ + if(root === null) { return res; } - while(queue.length!==0){ + while(queue.length !== 0) { // 记录当前层级节点数 - let length=queue.length; + let length = queue.length; //存放每一层的节点 - let curLevel=[]; - for(let i=0;i0{ - length:=queue.Len() - tmp:=[]int{} - for i:=0;i 0 { + length := queue.Len() + tmp := []int{} + for i := 0; i < length; i++ { + node := queue.Remove(queue.Front()).(*TreeNode) + if node.Left != nil { queue.PushBack(node.Left) } - if node.Right!=nil{ + if node.Right != nil { queue.PushBack(node.Right) } - tmp=append(tmp,node.Val) + tmp = append(tmp, node.Val) } - res=append(res,tmp) + res=append(res, tmp) } + //反转结果集 - for i:=0;i0{ - length:=queue.Len()//记录当前层的数量 + for queue.Len() > 0 { + length := queue.Len() //记录当前层的数量 var tmp []int - for T:=0;Tnode.val?max:node.val; - node.left&&queue.push(node.left); - node.right&&queue.push(node.right); + max = max > node.val ? max : node.val; + node.left && queue.push(node.left); + node.right && queue.push(node.right); } //把每一层的最大值放到res数组 res.push(max); } + return res; }; ``` @@ -1964,66 +1981,6 @@ class Solution: first = first.left # 从本层扩展到下一层 return root ``` -JavaScript: -```javascript - -/** - * // Definition for a Node. - * function Node(val, left, right, next) { - * this.val = val === undefined ? null : val; - * this.left = left === undefined ? null : left; - * this.right = right === undefined ? null : right; - * this.next = next === undefined ? null : next; - * }; - */ - -/** - * @param {Node} root - * @return {Node} - */ -var connect = function(root) { - if (root === null) return root; - let queue = [root]; - while (queue.length) { - let n = queue.length; - for (let i=0; i 0) { - for (let i = 0, length = helperQueue.length; i < length; i++) { - if (i === 0) { - preNode = helperQueue.shift()!; - } else { - curNode = helperQueue.shift()!; - preNode.next = curNode; - preNode = curNode; - } - if (preNode.left) helperQueue.push(preNode.left); - if (preNode.right) helperQueue.push(preNode.right); - } - preNode.next = null; - } - return root; -}; -``` - go: ```GO @@ -2064,6 +2021,66 @@ func connect(root *Node) *Node { ``` +JavaScript: +```javascript + +/** + * // Definition for a Node. + * function Node(val, left, right, next) { + * this.val = val === undefined ? null : val; + * this.left = left === undefined ? null : left; + * this.right = right === undefined ? null : right; + * this.next = next === undefined ? null : next; + * }; + */ + +/** + * @param {Node} root + * @return {Node} + */ +var connect = function(root) { + if (root === null) return root; + let queue = [root]; + while (queue.length) { + let n = queue.length; + for (let i = 0; i < n; i++) { + let node = queue.shift(); + if (i < n-1) { + node.next = queue[0]; + } + node.left && queue.push(node.left); + node.right && queue.push(node.right); + } + } + return root; +}; + +``` +TypeScript: + +```typescript +function connect(root: Node | null): Node | null { + let helperQueue: Node[] = []; + let preNode: Node, curNode: Node; + if (root !== null) helperQueue.push(root); + while (helperQueue.length > 0) { + for (let i = 0, length = helperQueue.length; i < length; i++) { + if (i === 0) { + preNode = helperQueue.shift()!; + } else { + curNode = helperQueue.shift()!; + preNode.next = curNode; + preNode = curNode; + } + if (preNode.left) helperQueue.push(preNode.left); + if (preNode.right) helperQueue.push(preNode.right); + } + preNode.next = null; + } + return root; +}; +``` + Swift: ```swift @@ -2226,6 +2243,45 @@ class Solution: return root ``` +go: + +```GO +/** +116. 填充每个节点的下一个右侧节点指针 +117. 填充每个节点的下一个右侧节点指针 II + */ + +func connect(root *Node) *Node { + if root == nil { //防止为空 + return root + } + queue := list.New() + queue.PushBack(root) + tmpArr := make([]*Node, 0) + for queue.Len() > 0 { + length := queue.Len() //保存当前层的长度,然后处理当前层(十分重要,防止添加下层元素影响判断层中元素的个数) + for i := 0; i < length; i++ { + node := queue.Remove(queue.Front()).(*Node) //出队列 + if node.Left != nil { + queue.PushBack(node.Left) + } + if node.Right != nil { + queue.PushBack(node.Right) + } + tmpArr = append(tmpArr, node) //将值加入本层切片中 + } + if len(tmpArr) > 1 { + // 遍历每层元素,指定next + for i := 0; i < len(tmpArr)-1; i++ { + tmpArr[i].Next = tmpArr[i+1] + } + } + tmpArr = []*Node{} //清空层的数据 + } + return root +} +``` + JavaScript: ```javascript /** @@ -2284,44 +2340,6 @@ function connect(root: Node | null): Node | null { }; ``` -go: - -```GO -/** -116. 填充每个节点的下一个右侧节点指针 -117. 填充每个节点的下一个右侧节点指针 II - */ - -func connect(root *Node) *Node { - if root == nil { //防止为空 - return root - } - queue := list.New() - queue.PushBack(root) - tmpArr := make([]*Node, 0) - for queue.Len() > 0 { - length := queue.Len() //保存当前层的长度,然后处理当前层(十分重要,防止添加下层元素影响判断层中元素的个数) - for i := 0; i < length; i++ { - node := queue.Remove(queue.Front()).(*Node) //出队列 - if node.Left != nil { - queue.PushBack(node.Left) - } - if node.Right != nil { - queue.PushBack(node.Right) - } - tmpArr = append(tmpArr, node) //将值加入本层切片中 - } - if len(tmpArr) > 1 { - // 遍历每层元素,指定next - for i := 0; i < len(tmpArr)-1; i++ { - tmpArr[i].Next = tmpArr[i+1] - } - } - tmpArr = []*Node{} //清空层的数据 - } - return root -} -``` Swift: ```swift @@ -2461,7 +2479,6 @@ class Solution { } ``` - Python: ```python 3 class Solution: @@ -2498,20 +2515,20 @@ Go: * } */ func maxDepth(root *TreeNode) int { - ans:=0 - if root==nil{ + ans := 0 + if root == nil { return 0 } - queue:=list.New() + queue := list.New() queue.PushBack(root) - for queue.Len()>0{ - length:=queue.Len() - for i:=0;i 0 { + length := queue.Len() + for i := 0; i < length; i++ { + node := queue.Remove(queue.Front()).(*TreeNode) + if node.Left != nil { queue.PushBack(node.Left) } - if node.Right!=nil{ + if node.Right != nil { queue.PushBack(node.Right) } } @@ -2521,8 +2538,6 @@ func maxDepth(root *TreeNode) int { } ``` - - JavaScript: ```javascript /** @@ -2763,30 +2778,29 @@ Go: * } */ func minDepth(root *TreeNode) int { - ans:=0 - if root==nil{ + ans := 0 + if root == nil { return 0 } - queue:=list.New() + queue := list.New() queue.PushBack(root) - for queue.Len()>0{ - length:=queue.Len() - for i:=0;i 0 { + length := queue.Len() + for i := 0; i < length; i++ { + node := queue.Remove(queue.Front()).(*TreeNode) + if node.Left == nil && node.Right == nil { //当前节点没有左右节点,则代表此层是最小层 + return ans+1 //返回当前层 ans代表是上一层 } - if node.Left!=nil{ + if node.Left != nil { queue.PushBack(node.Left) } - if node.Right!=nil{ + if node.Right != nil { queue.PushBack(node.Right) } } ans++//记录层数 - - } + return ans+1 } ``` diff --git a/problems/0108.将有序数组转换为二叉搜索树.md b/problems/0108.将有序数组转换为二叉搜索树.md index fd634094..9318a0e2 100644 --- a/problems/0108.将有序数组转换为二叉搜索树.md +++ b/problems/0108.将有序数组转换为二叉搜索树.md @@ -248,7 +248,7 @@ class Solution { return root; } - // 左闭右闭区间[left, right) + // 左闭右闭区间[left, right] private TreeNode traversal(int[] nums, int left, int right) { if (left > right) return null; diff --git a/problems/0111.二叉树的最小深度.md b/problems/0111.二叉树的最小深度.md index 470e1919..0b2ec0f6 100644 --- a/problems/0111.二叉树的最小深度.md +++ b/problems/0111.二叉树的最小深度.md @@ -574,66 +574,48 @@ object Solution { rust: ```rust impl Solution { - pub fn min_depth(root: Option>>) -> i32 { - return Solution::bfs(root) - } - // 递归 - pub fn dfs(node: Option>>) -> i32{ - if node.is_none(){ - return 0; - } - let parent = node.unwrap(); - 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; - if left_child.is_some(){ - let left_depth = Solution::dfs(left_child); - if left_depth <= min_depth{ - min_depth = left_depth + pub fn min_depth(root: Option>>) -> i32 { + if let Some(node) = root { + 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))) + } + _ => 1, } + } else { + 0 } - 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>>) -> i32{ - let mut min_depth = 0; - if node.is_none(){ - return min_depth + // 需要 use std::collections::VecDeque; + pub fn min_depth(root: Option>>) -> i32 { + let mut res = 0; + let mut queue = VecDeque::new(); + if root.is_some() { + queue.push_back(root); } - let mut stack = vec![node.unwrap()]; - while !stack.is_empty(){ - min_depth += 1; - let num = stack.len(); - for _i in 0..num{ - let top = stack.remove(0); - 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; + while !queue.is_empty() { + res += 1; + for _ in 0..queue.len() { + let node = queue.pop_front().unwrap().unwrap(); + if node.borrow().left.is_none() && node.borrow().right.is_none() { + return res; } - if left_child.is_some(){ - stack.push(left_child.unwrap()); + if node.borrow().left.is_some() { + queue.push_back(node.borrow().left.clone()); } - if right_child.is_some(){ - stack.push(right_child.unwrap()); + if node.borrow().right.is_some() { + queue.push_back(node.borrow().right.clone()); } } } - min_depth + res } - +} ```

diff --git a/problems/0139.单词拆分.md b/problems/0139.单词拆分.md index c9e42dc7..f06bd40c 100644 --- a/problems/0139.单词拆分.md +++ b/problems/0139.单词拆分.md @@ -6,7 +6,7 @@ -## 139.单词拆分 +# 139.单词拆分 [力扣题目链接](https://leetcode.cn/problems/word-break/) @@ -19,19 +19,19 @@ 你可以假设字典中没有重复的单词。 示例 1: -输入: s = "leetcode", wordDict = ["leet", "code"] -输出: true -解释: 返回 true 因为 "leetcode" 可以被拆分成 "leet code"。 +* 输入: s = "leetcode", wordDict = ["leet", "code"] +* 输出: true +* 解释: 返回 true 因为 "leetcode" 可以被拆分成 "leet code"。 示例 2: -输入: s = "applepenapple", wordDict = ["apple", "pen"] -输出: true -解释: 返回 true 因为 "applepenapple" 可以被拆分成 "apple pen apple"。 -  注意你可以重复使用字典中的单词。 +* 输入: s = "applepenapple", wordDict = ["apple", "pen"] +* 输出: true +* 解释: 返回 true 因为 "applepenapple" 可以被拆分成 "apple pen apple"。 +* 注意你可以重复使用字典中的单词。 示例 3: -输入: s = "catsandog", wordDict = ["cats", "dog", "sand", "and", "cat"] -输出: false +* 输入: s = "catsandog", wordDict = ["cats", "dog", "sand", "and", "cat"] +* 输出: false ## 思路 @@ -158,24 +158,19 @@ dp[0]表示如果字符串为空的话,说明出现在字典里。 **如果求排列数就是外层for遍历背包,内层for循环遍历物品**。 -对这个结论还有疑问的同学可以看这篇[本周小结!(动态规划系列五)](https://programmercarl.com/%E5%91%A8%E6%80%BB%E7%BB%93/20210204动规周末总结.html),这篇本周小节中,我做了如下总结: +我在这里做一个一个总结: 求组合数:[动态规划:518.零钱兑换II](https://programmercarl.com/0518.零钱兑换II.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) -本题最终要求的是是否都出现过,所以对出现单词集合里的元素是组合还是排列,并不在意! +而本题其实我们求的是排列数,为什么呢。 拿 s = "applepenapple", wordDict = ["apple", "pen"] 举例。 -**那么本题使用求排列的方式,还是求组合的方式都可以**。 +"apple", "pen" 是物品,那么我们要求 物品的组合一定是 "apple" + "pen" + "apple" 才能组成 "applepenapple"。 -即:外层for循环遍历物品,内层for遍历背包 或者 外层for遍历背包,内层for循环遍历物品 都是可以的。 - -但本题还有特殊性,因为是要求子串,最好是遍历背包放在外循环,将遍历物品放在内循环。 - -如果要是外层for循环遍历物品,内层for遍历背包,就需要把所有的子串都预先放在一个容器里。(如果不理解的话,可以自己尝试这么写一写就理解了) - -**所以最终我选择的遍历顺序为:遍历背包放在外循环,将遍历物品放在内循环。内循环从前到后**。 +"apple" + "apple" + "pen" 或者 "pen" + "apple" + "apple" 是不可以的,那么我们就是强调物品之间顺序。 +所以说,本题一定是 先遍历 背包,在遍历物品。 5. 举例推导dp[i] @@ -210,22 +205,51 @@ public: * 时间复杂度:O(n^3),因为substr返回子串的副本是O(n)的复杂度(这里的n是substring的长度) * 空间复杂度:O(n) +## 拓展 + +关于遍历顺序,再给大家讲一下为什么 先遍历物品再遍历背包不行。 + +这里可以给出先遍历物品在遍历背包的代码: + +```CPP +class Solution { +public: + bool wordBreak(string s, vector& wordDict) { + unordered_set wordSet(wordDict.begin(), wordDict.end()); + vector 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)非常像,所以我也给出了对应的回溯解法。 -稍加分析,便可知道本题是完全背包,而且是求能否组成背包,所以遍历顺序理论上来讲 两层for循环谁先谁后都可以! - -但因为分割子串的特殊性,遍历背包放在外循环,将遍历物品放在内循环更方便一些。 - -本题其实递推公式都不是重点,遍历顺序才是重点,如果我直接把代码贴出来,估计同学们也会想两个for循环的顺序理所当然就是这样,甚至都不会想为什么遍历背包的for循环在外层。 - -不分析透彻不是Carl的风格啊,哈哈 - - - - +稍加分析,便可知道本题是完全背包,是求能否组成背包,而且这里要求物品是要有顺序的。 ## 其他语言版本 diff --git a/problems/0150.逆波兰表达式求值.md b/problems/0150.逆波兰表达式求值.md index 78dfae3e..176ea687 100644 --- a/problems/0150.逆波兰表达式求值.md +++ b/problems/0150.逆波兰表达式求值.md @@ -51,7 +51,7 @@ ``` -逆波兰表达式:是一种后缀表达式,所谓后缀就是指算符写在后面。 +逆波兰表达式:是一种后缀表达式,所谓后缀就是指运算符写在后面。 平常使用的算式则是一种中缀表达式,如 ( 1 + 2 ) * ( 3 + 4 ) 。 @@ -61,11 +61,11 @@ * 去掉括号后表达式无歧义,上式即便写成 1 2 + 3 4 + * 也可以依据次序计算出正确结果。 -* 适合用栈操作运算:遇到数字则入栈;遇到算符则取出栈顶两个数字进行计算,并将结果压入栈中。 +* 适合用栈操作运算:遇到数字则入栈;遇到运算符则取出栈顶两个数字进行计算,并将结果压入栈中。 # 思路 -《代码随想录》算法视频公开课:[栈的最后表演! | LeetCode:150. 逆波兰表达式求值](https://www.bilibili.com/video/BV1kd4y1o7on),相信结合视频在看本篇题解,更有助于大家对本题的理解。 +《代码随想录》算法视频公开课:[栈的最后表演! | LeetCode:150. 逆波兰表达式求值](https://www.bilibili.com/video/BV1kd4y1o7on),相信结合视频再看本篇题解,更有助于大家对本题的理解。 在上一篇文章中[1047.删除字符串中的所有相邻重复项](https://programmercarl.com/1047.删除字符串中的所有相邻重复项.html)提到了 递归就是用栈来实现的。 @@ -73,7 +73,7 @@ 那么来看一下本题,**其实逆波兰表达式相当于是二叉树中的后序遍历**。 大家可以把运算符作为中间节点,按照后序遍历的规则画出一个二叉树。 -但我们没有必要从二叉树的角度去解决这个问题,只要知道逆波兰表达式是用后续遍历的方式把二叉树序列化了,就可以了。 +但我们没有必要从二叉树的角度去解决这个问题,只要知道逆波兰表达式是用后序遍历的方式把二叉树序列化了,就可以了。 在进一步看,本题中每一个子表达式要得出一个结果,然后拿这个结果再进行运算,那么**这岂不就是一个相邻字符串消除的过程,和[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 func evalRPN(tokens []string) int { @@ -169,7 +187,7 @@ func evalRPN(tokens []string) int { val, err := strconv.Atoi(token) if err == nil { stack = append(stack, val) - } else { + } else { // 如果err不为nil说明不是数字 num1, num2 := stack[len(stack)-2], stack[(len(stack))-1] stack = stack[:len(stack)-2] switch token { @@ -191,27 +209,31 @@ func evalRPN(tokens []string) int { javaScript: ```js - -/** - * @param {string[]} tokens - * @return {number} - */ -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] - ]); +var evalRPN = function (tokens) { const stack = []; - for (const i of tokens) { - if(!s.has(i)) { - stack.push(i); - continue; + for (const token of tokens) { + if (isNaN(Number(token))) { // 非数字 + const n2 = stack.pop(); // 出栈两个数字 + 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; + } + } else { // 数字 + stack.push(Number(token)); } - stack.push(s.get(i)(stack.pop(),stack.pop())) } - 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 func evalRPN(_ tokens: [String]) -> Int { diff --git a/problems/0151.翻转字符串里的单词.md b/problems/0151.翻转字符串里的单词.md index 450d7258..b82204fa 100644 --- a/problems/0151.翻转字符串里的单词.md +++ b/problems/0151.翻转字符串里的单词.md @@ -119,7 +119,7 @@ void removeExtraSpaces(string& s) { 1. leetcode上的测试集里,字符串的长度不够长,如果足够长,性能差距会非常明显。 2. leetcode的测程序耗时不是很准确的。 -版本一的代码是比较如何一般思考过程,就是 先移除字符串钱的空格,在移除中间的,在移除后面部分。 +版本一的代码是一般的思考过程,就是 先移除字符串前的空格,再移除中间的,再移除后面部分。 不过其实还可以优化,这部分和[27.移除元素](https://programmercarl.com/0027.移除元素.html)的逻辑是一样一样的,本题是移除空格,而 27.移除元素 就是移除元素。 @@ -145,7 +145,7 @@ void removeExtraSpaces(string& s) {//去除所有空格并在相邻单词之间 此时我们已经实现了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)里已经讲过了。 代码如下: @@ -434,49 +434,51 @@ python: ```Python class Solution: #1.去除多余的空格 - def trim_spaces(self,s): - n=len(s) - left=0 - right=n-1 + def trim_spaces(self, s): + n = len(s) + left = 0 + right = n-1 - while left<=right and s[left]==' ': #去除开头的空格 - left+=1 - while left<=right and s[right]==' ': #去除结尾的空格 - right=right-1 - tmp=[] - while left<=right: #去除单词中间多余的空格 - if s[left]!=' ': + while left <= right and s[left] == ' ': #去除开头的空格 + left += 1 + while left <= right and s[right] == ' ': #去除结尾的空格 + right = right-1 + tmp = [] + while left <= right: #去除单词中间多余的空格 + if s[left] != ' ': tmp.append(s[left]) - elif tmp[-1]!=' ': #当前位置是空格,但是相邻的上一个位置不是空格,则该空格是合理的 + elif tmp[-1] != ' ': #当前位置是空格,但是相邻的上一个位置不是空格,则该空格是合理的 tmp.append(s[left]) - left+=1 + left += 1 return tmp -#2.翻转字符数组 - def reverse_string(self,nums,left,right): - while left>>) -> i32 { + if root.is_none() { + return 0; + } + 1 + Self::count_nodes(Rc::clone(root.as_ref().unwrap()).borrow().left.clone()) + + Self::count_nodes(root.unwrap().borrow().right.clone()) + } +} +``` + +// 迭代 +```rust +use std::rc::Rc; +use std::cell::RefCell; +use std::collections::VecDeque; +impl Solution { + pub fn count_nodes(root: Option>>) -> i32 { + let mut res = 0; + let mut queue = VecDeque::new(); + if root.is_some() { + queue.push_back(root); + } + while !queue.is_empty() { + for _ in 0..queue.len() { + let node = queue.pop_front().unwrap().unwrap(); + if node.borrow().left.is_some() { + queue.push_back(node.borrow().left.clone()); + } + if node.borrow().right.is_some() { + queue.push_back(node.borrow().right.clone()); + } + res += 1; + } + } + res + } +} +``` +

diff --git a/problems/0225.用队列实现栈.md b/problems/0225.用队列实现栈.md index 2f015272..16b9e47c 100644 --- a/problems/0225.用队列实现栈.md +++ b/problems/0225.用队列实现栈.md @@ -29,7 +29,7 @@ # 思路 -《代码随想录》算法公开课:[队列的基本操作! | LeetCode:225. 用队列实现栈](https://www.bilibili.com/video/BV1Fd4y1K7sm),相信结合视频在看本篇题解,更有助于大家对链表的理解。 +《代码随想录》算法公开课:[队列的基本操作! | LeetCode:225. 用队列实现栈](https://www.bilibili.com/video/BV1Fd4y1K7sm),相信结合视频再看本篇题解,更有助于大家对链表的理解。 (这里要强调是单向队列) @@ -44,7 +44,7 @@ 所以用栈实现队列, 和用队列实现栈的思路还是不一样的,这取决于这两个数据结构的性质。 -但是依然还是要用两个队列来模拟栈,只不过没有输入和输出的关系,而是另一个队列完全用又来备份的! +但是依然还是要用两个队列来模拟栈,只不过没有输入和输出的关系,而是另一个队列完全用来备份的! 如下面动画所示,**用两个队列que1和que2实现队列的功能,que2其实完全就是一个备份的作用**,把que1最后面的元素以外的元素都备份到que2,然后弹出最后面的元素,再把其他元素从que2导回que1。 @@ -116,7 +116,7 @@ public: 其实这道题目就是用一个队列就够了。 -**一个队列在模拟栈弹出元素的时候只要将队列头部的元素(除了最后一个元素外) 重新添加到队列尾部,此时在去弹出元素就是栈的顺序了。** +**一个队列在模拟栈弹出元素的时候只要将队列头部的元素(除了最后一个元素外) 重新添加到队列尾部,此时再去弹出元素就是栈的顺序了。** C++优化代码 diff --git a/problems/0226.翻转二叉树.md b/problems/0226.翻转二叉树.md index 3136c0be..ad2a7de2 100644 --- a/problems/0226.翻转二叉树.md +++ b/problems/0226.翻转二叉树.md @@ -104,7 +104,7 @@ public: ### 深度优先遍历 -[二叉树:听说递归能做的,栈也能做!](https://programmercarl.com/二叉树的迭代遍历.html)中给出了前中后序迭代方式的写法,所以本地可以很轻松的切出如下迭代法的代码: +[二叉树:听说递归能做的,栈也能做!](https://programmercarl.com/二叉树的迭代遍历.html)中给出了前中后序迭代方式的写法,所以本题可以很轻松的写出如下迭代法的代码: C++代码迭代法(前序遍历) @@ -126,7 +126,7 @@ public: } }; ``` -如果这个代码看不懂的话可以在回顾一下[二叉树:听说递归能做的,栈也能做!](https://programmercarl.com/二叉树的迭代遍历.html)。 +如果这个代码看不懂的话可以再回顾一下[二叉树:听说递归能做的,栈也能做!](https://programmercarl.com/二叉树的迭代遍历.html)。 我们在[二叉树:前中后序迭代方式的统一写法](https://programmercarl.com/二叉树的统一迭代法.html)中介绍了统一的写法,所以,本题也只需将文中的代码少做修改便可。 @@ -258,7 +258,6 @@ public: ### Java - ```Java //DFS递归 class Solution { @@ -294,8 +293,8 @@ class Solution { while (size-- > 0) { TreeNode node = deque.poll(); swap(node); - if (node.left != null) {deque.offer(node.left);} - if (node.right != null) {deque.offer(node.right);} + if (node.left != null) deque.offer(node.left); + if (node.right != null) deque.offer(node.right); } } return root; @@ -364,13 +363,12 @@ class Solution: ### Go 递归版本的前序遍历 - ```Go func invertTree(root *TreeNode) *TreeNode { - if root ==nil{ + if root == nil { return nil } - root.Left,root.Right=root.Right,root.Left//交换 + root.Left, root.Right = root.Right, root.Left //交换 invertTree(root.Left) invertTree(root.Right) @@ -383,12 +381,14 @@ func invertTree(root *TreeNode) *TreeNode { ```go func invertTree(root *TreeNode) *TreeNode { - if root==nil{ + if root == nil { return root } - invertTree(root.Left)//遍历左节点 - invertTree(root.Right)//遍历右节点 - root.Left,root.Right=root.Right,root.Left//交换 + + invertTree(root.Left) //遍历左节点 + invertTree(root.Right) //遍历右节点 + root.Left, root.Right = root.Right, root.Left //交换 + return root } ``` @@ -397,18 +397,19 @@ func invertTree(root *TreeNode) *TreeNode { ```go func invertTree(root *TreeNode) *TreeNode { - stack:=[]*TreeNode{} - node:=root - for node!=nil||len(stack)>0{ - for node!=nil{ - node.Left,node.Right=node.Right,node.Left//交换 - stack=append(stack,node) - node=node.Left + stack := []*TreeNode{} + node := root + for node != nil || len(stack) > 0 { + for node != nil { + node.Left, node.Right = node.Right, node.Left //交换 + stack = append(stack,node) + node = node.Left } - node=stack[len(stack)-1] - stack=stack[:len(stack)-1] - node=node.Right + node = stack[len(stack)-1] + stack = stack[:len(stack)-1] + node = node.Right } + return root } ``` @@ -417,25 +418,26 @@ func invertTree(root *TreeNode) *TreeNode { ```go func invertTree(root *TreeNode) *TreeNode { - stack:=[]*TreeNode{} - node:=root + stack := []*TreeNode{} + node := root var prev *TreeNode - for node!=nil||len(stack)>0{ - for node!=nil{ - stack=append(stack,node) - node=node.Left + for node != nil || len(stack) > 0 { + for node != nil { + stack = append(stack, node) + node = node.Left } - node=stack[len(stack)-1] - stack=stack[:len(stack)-1] - if node.Right==nil||node.Right==prev{ - node.Left,node.Right=node.Right,node.Left//交换 - prev=node - node=nil - }else { - stack=append(stack,node) - node=node.Right + node = stack[len(stack)-1] + stack = stack[:len(stack)-1] + if node.Right == nil || node.Right == prev { + node.Left, node.Right = node.Right, node.Left //交换 + prev = node + node = nil + } else { + stack = append(stack, node) + node = node.Right } } + return root } ``` @@ -444,21 +446,21 @@ func invertTree(root *TreeNode) *TreeNode { ```go func invertTree(root *TreeNode) *TreeNode { - if root==nil{ + if root == nil{ return root } - queue:=list.New() - node:=root + queue := list.New() + node := root queue.PushBack(node) - for queue.Len()>0{ - length:=queue.Len() - for i:=0;i 0 { + length := queue.Len() + for i := 0; i < length; i++ { + e := queue.Remove(queue.Front()).(*TreeNode) + e.Left, e.Right = e.Right, e.Left //交换 + if e.Left != nil { queue.PushBack(e.Left) } - if e.Right!=nil{ + if e.Right != nil { queue.PushBack(e.Right) } } @@ -487,31 +489,31 @@ var invertTree = function(root) { ```javascript var invertTree = function(root) { //我们先定义节点交换函数 - const invertNode=function(root,left,right){ - let temp=left; - left=right; - right=temp; - root.left=left; - root.right=right; + const invertNode = function(root, left, right) { + let temp = left; + left = right; + right = temp; + root.left = left; + root.right = right; } //使用迭代方法的前序遍历 - let stack=[]; - if(root===null){ + let stack = []; + if(root === null) { return root; } stack.push(root); - while(stack.length){ - let node=stack.pop(); - if(node!==null){ + while(stack.length) { + let node = stack.pop(); + if(node !== null) { //前序遍历顺序中左右 入栈顺序是前序遍历的倒序右左中 - node.right&&stack.push(node.right); - node.left&&stack.push(node.left); + node.right && stack.push(node.right); + node.left && stack.push(node.left); stack.push(node); stack.push(null); - }else{ - node=stack.pop(); + } else { + node = stack.pop(); //节点处理逻辑 - invertNode(node,node.left,node.right); + invertNode(node, node.left, node.right); } } return root; @@ -521,27 +523,27 @@ var invertTree = function(root) { ```javascript var invertTree = function(root) { //我们先定义节点交换函数 - const invertNode=function(root,left,right){ - let temp=left; - left=right; - right=temp; - root.left=left; - root.right=right; + const invertNode = function(root, left, right) { + let temp = left; + left = right; + right = temp; + root.left = left; + root.right = right; } //使用层序遍历 - let queue=[]; - if(root===null){ + let queue = []; + if(root === null) { return root; } queue.push(root); - while(queue.length){ - let length=queue.length; - while(length--){ - let node=queue.shift(); + while(queue.length) { + let length = queue.length; + while(length--) { + let node = queue.shift(); //节点处理逻辑 - invertNode(node,node.left,node.right); - node.left&&queue.push(node.left); - node.right&&queue.push(node.right); + invertNode(node, node.left, node.right); + node.left && queue.push(node.left); + node.right && queue.push(node.right); } } return root; @@ -760,7 +762,6 @@ func invertTree1(_ root: TreeNode?) -> TreeNode? { } ``` -### Swift 深度优先递归。 @@ -808,9 +809,6 @@ func invertTree(_ root: TreeNode?) -> TreeNode? { return root } ``` - -### Scala - 深度优先遍历(前序遍历): ```scala object Solution { diff --git a/problems/0232.用栈实现队列.md b/problems/0232.用栈实现队列.md index 5858f9e0..4983c93a 100644 --- a/problems/0232.用栈实现队列.md +++ b/problems/0232.用栈实现队列.md @@ -38,14 +38,14 @@ queue.empty(); // 返回 false ## 思路 -《代码随想录》算法公开课:[栈的基本操作! | LeetCode:232.用栈实现队列](https://www.bilibili.com/video/BV1nY4y1w7VC),相信结合视频在看本篇题解,更有助于大家对链表的理解。 +《代码随想录》算法公开课:[栈的基本操作! | LeetCode:232.用栈实现队列](https://www.bilibili.com/video/BV1nY4y1w7VC),相信结合视频再看本篇题解,更有助于大家对链表的理解。 这是一道模拟题,不涉及到具体算法,考察的就是对栈和队列的掌握程度。 使用栈来模式队列的行为,如果仅仅用一个栈,是一定不行的,所以需要两个栈**一个输入栈,一个输出栈**,这里要注意输入栈和输出栈的关系。 -下面动画模拟以下队列的执行过程如下: +下面动画模拟以下队列的执行过程: 执行语句: queue.push(1); @@ -120,7 +120,7 @@ public: 这样的项目代码会越来越乱,**一定要懂得复用,功能相近的函数要抽象出来,不要大量的复制粘贴,很容易出问题!(踩过坑的人自然懂)** -工作中如果发现某一个功能自己要经常用,同事们可能也会用到,自己就花点时间把这个功能抽象成一个好用的函数或者工具类,不仅自己方便,也方面了同事们。 +工作中如果发现某一个功能自己要经常用,同事们可能也会用到,自己就花点时间把这个功能抽象成一个好用的函数或者工具类,不仅自己方便,也方便了同事们。 同事们就会逐渐认可你的工作态度和工作能力,自己的口碑都是这么一点一点积累起来的!在同事圈里口碑起来了之后,你就发现自己走上了一个正循环,以后的升职加薪才少不了你!哈哈哈 @@ -231,56 +231,51 @@ class MyQueue: Go: ```Go type MyQueue struct { - stack []int - back []int + stackIn []int //输入栈 + stackOut []int //输出栈 } -/** Initialize your data structure here. */ func Constructor() MyQueue { return MyQueue{ - stack: make([]int, 0), - back: make([]int, 0), + stackIn: make([]int, 0), + stackOut: make([]int, 0), } } -/** Push element x to the back of queue. */ +// 往输入栈做push func (this *MyQueue) Push(x int) { - for len(this.back) != 0 { - 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) + this.stackIn = append(this.stackIn, x) } -/** Removes the element from in front of queue and returns that element. */ +// 在输出栈做pop,pop时如果输出栈数据为空,需要将输入栈全部数据导入,如果非空,则可直接使用 func (this *MyQueue) Pop() int { - for len(this.stack) != 0 { - val := this.stack[len(this.stack)-1] - this.stack = this.stack[:len(this.stack)-1] - this.back = append(this.back, val) + inLen, outLen := len(this.stackIn), len(this.stackOut) + if outLen == 0 { + if inLen == 0 { + return -1 + } + for i := inLen - 1; i >= 0; i-- { + this.stackOut = append(this.stackOut, this.stackIn[i]) + } + this.stackIn = []int{} //导出后清空 + outLen = len(this.stackOut) //更新长度值 } - if len(this.back) == 0 { - return 0 - } - val := this.back[len(this.back)-1] - this.back = this.back[:len(this.back)-1] + val := this.stackOut[outLen-1] + this.stackOut = this.stackOut[:outLen-1] return val } -/** Get the front element. */ func (this *MyQueue) Peek() int { val := this.Pop() - if val == 0 { - return 0 + if val == -1 { + return -1 } - this.back = append(this.back, val) + this.stackOut = append(this.stackOut, val) return val } -/** Returns whether the queue is empty. */ func (this *MyQueue) Empty() bool { - return len(this.stack) == 0 && len(this.back) == 0 + return len(this.stackIn) == 0 && len(this.stackOut) == 0 } ``` diff --git a/problems/0239.滑动窗口最大值.md b/problems/0239.滑动窗口最大值.md index 40da879c..9f2e96a6 100644 --- a/problems/0239.滑动窗口最大值.md +++ b/problems/0239.滑动窗口最大值.md @@ -38,7 +38,7 @@ 难点是如何求一个区间里的最大值呢? (这好像是废话),暴力一下不就得了。 -暴力方法,遍历一遍的过程中每次从窗口中在找到最大的数值,这样很明显是O(n × k)的算法。 +暴力方法,遍历一遍的过程中每次从窗口中再找到最大的数值,这样很明显是O(n × 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)。 diff --git a/problems/0279.完全平方数.md b/problems/0279.完全平方数.md index 6acc4891..50ddf5f9 100644 --- a/problems/0279.完全平方数.md +++ b/problems/0279.完全平方数.md @@ -131,10 +131,8 @@ public: vector dp(n + 1, INT_MAX); dp[0] = 0; for (int i = 1; i * i <= n; i++) { // 遍历物品 - for (int j = 1; j <= n; j++) { // 遍历背包 - if (j - i * i >= 0) { - dp[j] = min(dp[j - i * i] + 1, dp[j]); - } + for (int j = i * i; j <= n; j++) { // 遍历背包 + dp[j] = min(dp[j - i * i] + 1, dp[j]); } } return dp[n]; diff --git a/problems/0322.零钱兑换.md b/problems/0322.零钱兑换.md index e337717f..45974112 100644 --- a/problems/0322.零钱兑换.md +++ b/problems/0322.零钱兑换.md @@ -53,8 +53,6 @@ 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]) 所以dp[j] 要取所有 dp[j - coins[i]] + 1 中最小的。 diff --git a/problems/0337.打家劫舍III.md b/problems/0337.打家劫舍III.md index 3b7e7cbf..c69279fd 100644 --- a/problems/0337.打家劫舍III.md +++ b/problems/0337.打家劫舍III.md @@ -50,8 +50,8 @@ public: }; ``` -* 时间复杂度:$O(n^2)$,这个时间复杂度不太标准,也不容易准确化,例如越往下的节点重复计算次数就越多 -* 空间复杂度:$O(\log n)$,算上递推系统栈的空间 +* 时间复杂度:O(n^2),这个时间复杂度不太标准,也不容易准确化,例如越往下的节点重复计算次数就越多 +* 空间复杂度:O(log n),算上递推系统栈的空间 当然以上代码超时了,这个递归的过程中其实是有重复计算了。 @@ -84,8 +84,8 @@ public: ``` -* 时间复杂度:$O(n)$ -* 空间复杂度:$O(\log n)$,算上递推系统栈的空间 +* 时间复杂度:O(n) +* 空间复杂度:O(log n),算上递推系统栈的空间 ### 动态规划 diff --git a/problems/0344.反转字符串.md b/problems/0344.反转字符串.md index 2807d04a..c4803311 100644 --- a/problems/0344.反转字符串.md +++ b/problems/0344.反转字符串.md @@ -190,11 +190,11 @@ class Solution: Go: ```Go -func reverseString(s []byte) { - left:=0 - right:=len(s)-1 - for left

参与本项目,贡献其他语言版本的代码,拥抱开源,让更多学习算法的小伙伴们收益!

-# 动态规划:Carl称它为排列总和! -## 377. 组合总和 Ⅳ +# 377. 组合总和 Ⅳ [力扣题目链接](https://leetcode.cn/problems/combination-sum-iv/) diff --git a/problems/0416.分割等和子集.md b/problems/0416.分割等和子集.md index db080203..52b48264 100644 --- a/problems/0416.分割等和子集.md +++ b/problems/0416.分割等和子集.md @@ -79,7 +79,17 @@ 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. 确定递推公式 diff --git a/problems/0459.重复的子字符串.md b/problems/0459.重复的子字符串.md index b7d86880..5d870219 100644 --- a/problems/0459.重复的子字符串.md +++ b/problems/0459.重复的子字符串.md @@ -46,17 +46,17 @@ ## 移动匹配 -当一个字符串s:abcabc,内部又重复的子串组成,那么这个字符串的结构一定是这样的: +当一个字符串s:abcabc,内部由重复的子串组成,那么这个字符串的结构一定是这样的: ![图一](https://code-thinking-1253855093.file.myqcloud.com/pics/20220728104518.png) -也就是又前后又相同的子串组成。 +也就是由前后相同的子串组成。 那么既然前面有相同的子串,后面有相同的子串,用 s + s,这样组成的字符串中,后面的子串做前串,前后的子串做后串,就一定还能组成一个s,如图: ![图二](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。 @@ -81,7 +81,7 @@ public: ## KMP ### 为什么会使用KMP -以下使用KMP方式讲解,强烈建议大家先把一下两个视频看了,理解KMP算法,在来看下面讲解,否则会很懵。 +以下使用KMP方式讲解,强烈建议大家先把以下两个视频看了,理解KMP算法,再来看下面讲解,否则会很懵。 * [视频讲解版:帮你把KMP算法学个通透!(理论篇)](https://www.bilibili.com/video/BV1PD4y1o7nd/) * [视频讲解版:帮你把KMP算法学个通透!(求next数组代码篇)](https://www.bilibili.com/video/BV1M5411j7Xx) @@ -93,12 +93,12 @@ KMP算法中next数组为什么遇到字符不匹配的时候可以找到上一 那么 最长相同前后缀和重复子串的关系又有什么关系呢。 -可能很多录友又忘了 前缀和后缀的定义,在回顾一下: +可能很多录友又忘了 前缀和后缀的定义,再回顾一下: * 前缀是指不包含最后一个字符的所有以第一个字符开头的连续子串; * 后缀是指不包含第一个字符的所有以最后一个字符结尾的连续子串 -在由重复子串组成的字符串中,最长相等前后缀不包含的子串就是最小重复子串,这里那字符串s:abababab 来举例,ab就是最小重复单位,如图所示: +在由重复子串组成的字符串中,最长相等前后缀不包含的子串就是最小重复子串,这里拿字符串s:abababab 来举例,ab就是最小重复单位,如图所示: ![图三](https://code-thinking-1253855093.file.myqcloud.com/pics/20220728205249.png) @@ -123,11 +123,11 @@ KMP算法中next数组为什么遇到字符不匹配的时候可以找到上一 ### 简单推理 -这里在给出一个数推导,就容易理解很多。 +这里再给出一个数学推导,就容易理解很多。 假设字符串s使用多个重复子串构成(这个子串是最小重复单位),重复出现的子字符串长度是x,所以s是由n * x组成。 -因为字符串s的最长相同前后缀的的长度一定是不包含s本身,所以 最长相同前后缀长度必然是m * x,而且 n - m = 1,(这里如果不懂,看上面的推理) +因为字符串s的最长相同前后缀的长度一定是不包含s本身,所以 最长相同前后缀长度必然是m * x,而且 n - m = 1,(这里如果不懂,看上面的推理) 所以如果 nx % (n - m)x = 0,就可以判定有重复出现的子字符串。 diff --git a/problems/0474.一和零.md b/problems/0474.一和零.md index 21fbf84b..45a0a270 100644 --- a/problems/0474.一和零.md +++ b/problems/0474.一和零.md @@ -3,9 +3,8 @@

参与本项目,贡献其他语言版本的代码,拥抱开源,让更多学习算法的小伙伴们收益!

-# 动态规划:一和零! -## 474.一和零 +# 474.一和零 [力扣题目链接](https://leetcode.cn/problems/ones-and-zeroes/) @@ -42,7 +41,7 @@ * [动态规划:关于01背包问题,你该了解这些!](https://programmercarl.com/背包理论基础01背包-1.html) * [动态规划:关于01背包问题,你该了解这些!(滚动数组)](https://programmercarl.com/背包理论基础01背包-2.html) -这道题目,还是比较难的,也有点像程序员自己给自己出个脑筋急转弯,程序员何苦为难程序员呢哈哈。 +这道题目,还是比较难的,也有点像程序员自己给自己出个脑筋急转弯,程序员何苦为难程序员呢。 来说题,本题不少同学会认为是多重背包,一些题解也是这么写的。 @@ -82,7 +81,7 @@ dp[i][j] 就可以是 dp[i - zeroNum][j - oneNum] + 1。 对比一下就会发现,字符串的zeroNum和oneNum相当于物品的重量(weight[i]),字符串本身的个数相当于物品的价值(value[i])。 -**这就是一个典型的01背包!** 只不过物品的重量有了两个维度而已。 +**这就是一个典型的01背包!** 只不过物品的重量有了两个维度而已。 3. dp数组如何初始化 @@ -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背包不同维度上的应用,大家可以细心体会! diff --git a/problems/0494.目标和.md b/problems/0494.目标和.md index 7882229c..946e52f2 100644 --- a/problems/0494.目标和.md +++ b/problems/0494.目标和.md @@ -3,9 +3,11 @@

参与本项目,贡献其他语言版本的代码,拥抱开源,让更多学习算法的小伙伴们收益!

-# 动态规划:目标和! -## 494. 目标和 + + + +# 494. 目标和 [力扣题目链接](https://leetcode.cn/problems/target-sum/) @@ -52,9 +54,9 @@ 既然为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就可以求出来。 @@ -117,22 +119,26 @@ public: 假设加法的总和为x,那么减法对应的总和就是sum - x。 -所以我们要求的是 x - (sum - x) = S +所以我们要求的是 x - (sum - x) = target -x = (S + sum) / 2 +x = (target + sum) / 2 **此时问题就转化为,装满容量为x背包,有几种方法**。 -大家看到(S + sum) / 2 应该担心计算的过程中向下取整有没有影响。 +这里的x,就是bagSize,也就是我们后面要求的背包容量。 + +大家看到(target + sum) / 2 应该担心计算的过程中向下取整有没有影响。 这么担心就对了,例如sum 是5,S是2的话其实就是无解的,所以: -```CPP +```CPP +(C++代码中,输入的S 就是题目描述的 target) if ((S + sum) % 2 == 1) return 0; // 此时没有方案 ``` 同时如果 S的绝对值已经大于sum,那么也是没有方案的。 ```CPP +(C++代码中,输入的S 就是题目描述的 target) if (abs(S) > sum) return 0; // 此时没有方案 ``` @@ -156,17 +162,15 @@ dp[j] 表示:填满j(包括j)这么大容积的包,有dp[j]种方法 有哪些来源可以推出dp[j]呢? -不考虑nums[i]的情况下,填满容量为j的背包,有dp[j]种方法。 - -那么考虑nums[i]的话(只要搞到nums[i]),凑成dp[j]就有dp[j - nums[i]] 种方法。 +只要搞到nums[i]),凑成dp[j]就有dp[j - nums[i]] 种方法。 例如:dp[j],j 为5, -* 已经有一个1(nums[i]) 的话,有 dp[4]种方法 凑成 dp[5]。 -* 已经有一个2(nums[i]) 的话,有 dp[3]种方法 凑成 dp[5]。 -* 已经有一个3(nums[i]) 的话,有 dp[2]中方法 凑成 dp[5] -* 已经有一个4(nums[i]) 的话,有 dp[1]中方法 凑成 dp[5] -* 已经有一个5 (nums[i])的话,有 dp[0]中方法 凑成 dp[5] +* 已经有一个1(nums[i]) 的话,有 dp[4]种方法 凑成 容量为5的背包。 +* 已经有一个2(nums[i]) 的话,有 dp[3]种方法 凑成 容量为5的背包。 +* 已经有一个3(nums[i]) 的话,有 dp[2]中方法 凑成 容量为5的背包 +* 已经有一个4(nums[i]) 的话,有 dp[1]中方法 凑成 容量为5的背包 +* 已经有一个5 (nums[i])的话,有 dp[0]中方法 凑成 容量为5的背包 那么凑整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,理论上也很好解释,装满容量为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. 确定遍历顺序 @@ -213,7 +227,6 @@ public: if (abs(S) > sum) return 0; // 此时没有方案 if ((S + sum) % 2 == 1) return 0; // 此时没有方案 int bagSize = (S + sum) / 2; - if (bagsize < 0) return 0; vector dp(bagSize + 1, 0); dp[0] = 1; for (int i = 0; i < nums.size(); i++) { @@ -238,7 +251,7 @@ public: 本题还是有点难度,大家也可以记住,在求装满背包有几种方法的情况下,递推公式一般为: -``` +```CPP dp[j] += dp[j - nums[i]]; ``` diff --git a/problems/0518.零钱兑换II.md b/problems/0518.零钱兑换II.md index e29e2107..2fc807cd 100644 --- a/problems/0518.零钱兑换II.md +++ b/problems/0518.零钱兑换II.md @@ -3,9 +3,10 @@

参与本项目,贡献其他语言版本的代码,拥抱开源,让更多学习算法的小伙伴们收益!

-# 动态规划:给你一些零钱,你要怎么凑? -## 518. 零钱兑换 II + + +# 518. 零钱兑换 II [力扣题目链接](https://leetcode.cn/problems/coin-change-ii/) @@ -15,22 +16,25 @@ 示例 1: -输入: amount = 5, coins = [1, 2, 5] -输出: 4 +* 输入: amount = 5, coins = [1, 2, 5] +* 输出: 4 + 解释: 有四种方式可以凑成总金额: -5=5 -5=2+2+1 -5=2+1+1+1 -5=1+1+1+1+1 + +* 5=5 +* 5=2+2+1 +* 5=2+1+1+1 +* 5=1+1+1+1+1 示例 2: -输入: amount = 3, coins = [2] -输出: 0 -解释: 只用面额2的硬币不能凑成总金额3。 + +* 输入: amount = 3, coins = [2] +* 输出: 0 +* 解释: 只用面额2的硬币不能凑成总金额3。 示例 3: -输入: amount = 10, coins = [10] -输出: 1 +* 输入: amount = 10, coins = [10] +* 输出: 1 注意,你可以假设: @@ -47,7 +51,7 @@ 对完全背包还不了解的同学,可以看这篇:[动态规划:关于完全背包,你该了解这些!](https://programmercarl.com/背包问题理论基础完全背包.html) -但本题和纯完全背包不一样,**纯完全背包是能否凑成总金额,而本题是要求凑成总金额的个数!** +但本题和纯完全背包不一样,**纯完全背包是凑成背包最大价值是多少,而本题是要求凑成总金额的物品组合个数!** 注意题目描述中是凑成总金额的硬币组合数,为什么强调是组合数呢? @@ -73,17 +77,21 @@ dp[j]:凑成总金额j的货币组合数为dp[j] 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]]; -**这个递推公式大家应该不陌生了,我在讲解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数组如何初始化 -首先dp[0]一定要为1,dp[0] = 1是 递归公式的基础。 +首先dp[0]一定要为1,dp[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] @@ -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交换顺序,代码如下: -``` +```CPP for (int j = 0; j <= amount; j++) { // 遍历背包容量 for (int i = 0; i < coins.size(); 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: - - ## 其他语言版本 diff --git a/problems/0541.反转字符串II.md b/problems/0541.反转字符串II.md index 59891365..516f78da 100644 --- a/problems/0541.反转字符串II.md +++ b/problems/0541.反转字符串II.md @@ -294,6 +294,8 @@ func reverseStr(s string, k int) string { ss := []byte(s) length := len(s) for i := 0; i < length; i += 2 * k { + // 1. 每隔 2k 个字符的前 k 个字符进行反转 + // 2. 剩余字符小于 2k 但大于或等于 k 个,则反转前 k 个字符 if i + k <= length { reverse(ss[i:i+k]) } else { @@ -326,7 +328,7 @@ javaScript: var reverseStr = function(s, k) { const len = s.length; 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; while(++l < --r) [resArr[l], resArr[r]] = [resArr[r], resArr[l]]; } diff --git a/problems/1047.删除字符串中的所有相邻重复项.md b/problems/1047.删除字符串中的所有相邻重复项.md index ad729298..417c81d7 100644 --- a/problems/1047.删除字符串中的所有相邻重复项.md +++ b/problems/1047.删除字符串中的所有相邻重复项.md @@ -50,7 +50,7 @@ ![1047.删除字符串中的所有相邻重复项](https://code-thinking.cdn.bcebos.com/gifs/1047.删除字符串中的所有相邻重复项.gif) -从栈中弹出剩余元素,此时是字符串ac,因为从栈里弹出的元素是倒序的,所以在对字符串进行反转一下,就得到了最终的结果。 +从栈中弹出剩余元素,此时是字符串ac,因为从栈里弹出的元素是倒序的,所以再对字符串进行反转一下,就得到了最终的结果。 C++代码 : @@ -102,9 +102,9 @@ public: ## 题外话 -这道题目就像是我们玩过的游戏对对碰,如果相同的元素放在挨在一起就要消除。 +这道题目就像是我们玩过的游戏对对碰,如果相同的元素挨在一起就要消除。 -可能我们在玩游戏的时候感觉理所当然应该消除,但程序又怎么知道该如果消除呢,特别是消除之后又有新的元素可能挨在一起。 +可能我们在玩游戏的时候感觉理所当然应该消除,但程序又怎么知道该如何消除呢,特别是消除之后又有新的元素可能挨在一起。 此时游戏的后端逻辑就可以用一个栈来实现(我没有实际考察对对碰或者爱消除游戏的代码实现,仅从原理上进行推断)。 diff --git a/problems/1049.最后一块石头的重量II.md b/problems/1049.最后一块石头的重量II.md index 5412c5b5..bf4e708a 100644 --- a/problems/1049.最后一块石头的重量II.md +++ b/problems/1049.最后一块石头的重量II.md @@ -15,17 +15,20 @@ 每一回合,从中选出任意两块石头,然后将它们一起粉碎。假设石头的重量分别为 x 和 y,且 x <= y。那么粉碎的可能结果如下: 如果 x == y,那么两块石头都会被完全粉碎; + 如果 x != y,那么重量为 x 的石头将会完全粉碎,而重量为 y 的石头新重量为 y-x。 + 最后,最多只会剩下一块石头。返回此石头最小的可能重量。如果没有石头剩下,就返回 0。 示例: -输入:[2,7,4,1,8,1] -输出:1 +* 输入:[2,7,4,1,8,1] +* 输出:1 + 解释: -组合 2 和 4,得到 2,所以数组转化为 [2,7,1,8,1], -组合 7 和 8,得到 1,所以数组转化为 [2,1,1,1], -组合 2 和 1,得到 1,所以数组转化为 [1,1,1], -组合 1 和 1,得到 0,所以数组转化为 [1],这就是最优值。 +* 组合 2 和 4,得到 2,所以数组转化为 [2,7,1,8,1], +* 组合 7 和 8,得到 1,所以数组转化为 [2,1,1,1], +* 组合 2 和 1,得到 1,所以数组转化为 [1,1,1], +* 组合 1 和 1,得到 0,所以数组转化为 [1],这就是最优值。 提示: @@ -51,7 +54,11 @@ 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. 确定递推公式 @@ -61,7 +68,7 @@ 一些同学可能看到这dp[j - stones[i]] + stones[i]中 又有- stones[i] 又有+stones[i],看着有点晕乎。 -还是要牢记dp[j]的含义,要知道dp[j - stones[i]]为 容量为j - stones[i]的背包最大所背重量。 +大家可以再去看 dp[j]的含义。 3. dp数组如何初始化 diff --git a/problems/二叉树理论基础.md b/problems/二叉树理论基础.md index 03422960..21e2039f 100644 --- a/problems/二叉树理论基础.md +++ b/problems/二叉树理论基础.md @@ -14,7 +14,7 @@ 二叉树大纲 -说道二叉树,大家对于二叉树其实都很熟悉了,本文呢我也不想教科书式的把二叉树的基础内容在啰嗦一遍,所以一下我讲的都是一些比较重点的内容。 +说到二叉树,大家对于二叉树其实都很熟悉了,本文呢我也不想教科书式的把二叉树的基础内容再啰嗦一遍,所以以下我讲的都是一些比较重点的内容。 相信只要耐心看完,都会有所收获。 @@ -83,7 +83,7 @@ 那么链式存储方式就用指针, 顺序存储的方式就是用数组。 -顾名思义就是顺序存储的元素在内存是连续分布的,而链式存储则是通过指针把分布在散落在各个地址的节点串联一起。 +顾名思义就是顺序存储的元素在内存是连续分布的,而链式存储则是通过指针把分布在各个地址的节点串联一起。 链式存储如图: @@ -143,7 +143,7 @@ 最后再说一说二叉树中深度优先和广度优先遍历实现方式,我们做二叉树相关题目,经常会使用递归的方式来实现深度优先遍历,也就是实现前中后序遍历,使用递归是比较方便的。 -**之前我们讲栈与队列的时候,就说过栈其实就是递归的一种是实现结构**,也就说前中后序遍历的逻辑其实都是可以借助栈使用非递归的方式来实现的。 +**之前我们讲栈与队列的时候,就说过栈其实就是递归的一种实现结构**,也就说前中后序遍历的逻辑其实都是可以借助栈使用非递归的方式来实现的。 而广度优先遍历的实现一般使用队列来实现,这也是队列先进先出的特点所决定的,因为需要先进先出的结构,才能一层一层的来遍历二叉树。 diff --git a/problems/二叉树的递归遍历.md b/problems/二叉树的递归遍历.md index 78861040..5a9a670a 100644 --- a/problems/二叉树的递归遍历.md +++ b/problems/二叉树的递归遍历.md @@ -36,13 +36,13 @@ **以下以前序遍历为例:** -1. **确定递归函数的参数和返回值**:因为要打印出前序遍历节点的数值,所以参数里需要传入vector在放节点的数值,除了这一点就不需要在处理什么数据了也不需要有返回值,所以递归函数返回类型就是void,代码如下: +1. **确定递归函数的参数和返回值**:因为要打印出前序遍历节点的数值,所以参数里需要传入vector来放节点的数值,除了这一点就不需要再处理什么数据了也不需要有返回值,所以递归函数返回类型就是void,代码如下: ```cpp void traversal(TreeNode* cur, vector& vec) ``` -2. **确定终止条件**:在递归的过程中,如何算是递归结束了呢,当然是当前遍历的节点是空了,那么本层递归就要要结束了,所以如果当前遍历的这个节点是空,就直接return,代码如下: +2. **确定终止条件**:在递归的过程中,如何算是递归结束了呢,当然是当前遍历的节点是空了,那么本层递归就要结束了,所以如果当前遍历的这个节点是空,就直接return,代码如下: ```cpp if (cur == NULL) return; diff --git a/problems/剑指Offer05.替换空格.md b/problems/剑指Offer05.替换空格.md index 53f0315f..ab6eedf7 100644 --- a/problems/剑指Offer05.替换空格.md +++ b/problems/剑指Offer05.替换空格.md @@ -36,7 +36,7 @@ i指向新长度的末尾,j指向旧长度的末尾。 这么做有两个好处: 1. 不用申请新数组。 -2. 从后向前填充元素,避免了从前先后填充元素要来的 每次添加元素都要将添加元素之后的所有元素向后移动。 +2. 从后向前填充元素,避免了从前向后填充元素时,每次添加元素都要将添加元素之后的所有元素向后移动的问题。 时间复杂度,空间复杂度均超过100%的用户。 diff --git a/problems/剑指Offer58-II.左旋转字符串.md b/problems/剑指Offer58-II.左旋转字符串.md index c34b2a71..1b619ffb 100644 --- a/problems/剑指Offer58-II.左旋转字符串.md +++ b/problems/剑指Offer58-II.左旋转字符串.md @@ -31,7 +31,7 @@ 不能使用额外空间的话,模拟在本串操作要实现左旋转字符串的功能还是有点困难的。 -那么我们可以想一下上一题目[字符串:花式反转还不够!](https://programmercarl.com/0151.翻转字符串里的单词.html)中讲过,使用整体反转+局部反转就可以实现,反转单词顺序的目的。 +那么我们可以想一下上一题目[字符串:花式反转还不够!](https://programmercarl.com/0151.翻转字符串里的单词.html)中讲过,使用整体反转+局部反转就可以实现反转单词顺序的目的。 这道题目也非常类似,依然可以通过局部反转+整体反转 达到左旋转的目的。 @@ -41,7 +41,7 @@ 2. 反转区间为n到末尾的子串 3. 反转整个字符串 -最后就可以得到左旋n的目的,而不用定义新的字符串,完全在本串上操作。 +最后就可以达到左旋n的目的,而不用定义新的字符串,完全在本串上操作。 例如 :示例1中 输入:字符串abcdefg,n=2 @@ -75,7 +75,7 @@ public: 在这篇文章[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)中,要对一句话里的单词顺序进行反转,发现先整体反转再局部反转 是一个很妙的思路。 diff --git a/problems/哈希表理论基础.md b/problems/哈希表理论基础.md index 9366ca5c..41647d48 100644 --- a/problems/哈希表理论基础.md +++ b/problems/哈希表理论基础.md @@ -108,7 +108,7 @@ std::unordered_map 底层实现为哈希表,std::map 和std::multimap 的底 其他语言例如: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_set,unordered_map又有什么关系呢? diff --git a/problems/图论深搜理论基础.md b/problems/图论深搜理论基础.md index cd66b819..9f965abb 100644 --- a/problems/图论深搜理论基础.md +++ b/problems/图论深搜理论基础.md @@ -6,10 +6,18 @@ # 深度优先搜索理论基础 -提到深度优先搜索(dfs),就不得不说和广度优先有什么区别(bfs) +录友们期待图论内容已久了,为什么鸽了这么久,主要是最近半年开始更新[代码随想录算法公开课](https://mp.weixin.qq.com/s/xncn6IHJGs45sJOChN6V_g),是开源在B站的算法视频,已经帮助非常多基础不好的录友学习算法。 + +录视频其实是非常累的,也要花很多时间,所以图论这边就没抽出时间来。 + +后面计划先给大家讲图论里大家特别需要的深搜和广搜。 + +以下,开始讲解深度优先搜索理论基础: ## dfs 与 bfs 区别 +提到深度优先搜索(dfs),就不得不说和广度优先有什么区别(bfs) + 先来了解dfs的过程,很多录友可能对dfs(深度优先搜索),bfs(广度优先搜索)分不清。 先给大家说一下两者大概的区别: @@ -35,7 +43,7 @@ ![图三](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。 @@ -55,7 +63,6 @@ * 搜索方向,是认准一个方向搜,直到碰壁之后在换方向 * 换方向是撤销原路径,改为节点链接的下一个路径,回溯的过程。 - ## 代码框架 正式因为dfs搜索可一个方向,并需要回溯,所以用递归的方式来实现是最方便的。 @@ -65,6 +72,7 @@ 有递归的地方就有回溯,那么回溯在哪里呢? 就地递归函数的下面,例如如下代码: + ``` void dfs(参数) { 处理节点 @@ -160,8 +168,6 @@ if (终止条件) { 终止添加不仅是结束本层递归,同时也是我们收获结果的时候。 另外,其实很多dfs写法,没有写终止条件,其实终止条件写在了, 下面dfs递归的逻辑里了,也就是不符合条件,直接不会向下递归。这里如果大家不理解的话,没关系,后面会有具体题目来讲解。 -* 841.钥匙和房间 -* 200. 岛屿数量 3. 处理目前搜索节点出发的路径 @@ -190,6 +196,9 @@ for (选择:本节点所连接的其他节点) { 以上如果大家都能理解了,其实搜索的代码就很好写,具体题目套用具体场景就可以了。 +后面我也会给大家安排具体练习的题目,依旧是代码随想录的风格,循序渐进由浅入深! + +

diff --git a/problems/栈与队列理论基础.md b/problems/栈与队列理论基础.md index 66b86919..ee4506f4 100644 --- a/problems/栈与队列理论基础.md +++ b/problems/栈与队列理论基础.md @@ -12,7 +12,7 @@ ![栈与队列理论1](https://img-blog.csdnimg.cn/20210104235346563.png) -那么我这里在列出四个关于栈的问题,大家可以思考一下。以下是以C++为例,相信使用其他编程语言的同学也对应思考一下,自己使用的编程语言里栈和队列是什么样的。 +那么我这里再列出四个关于栈的问题,大家可以思考一下。以下是以C++为例,使用其他编程语言的同学也对应思考一下,自己使用的编程语言里栈和队列是什么样的。 1. C++中stack 是容器么? 2. 我们使用的stack是属于哪个版本的STL? @@ -23,7 +23,7 @@ 有的同学可能仅仅知道有栈和队列这么个数据结构,却不知道底层实现,也不清楚所使用栈和队列和STL是什么关系。 -所以这里我在给大家扫一遍基础知识, +所以这里我再给大家扫一遍基础知识, 首先大家要知道 栈和队列是STL(C++标准库)里面的两个数据结构。 @@ -83,7 +83,7 @@ std::queue> third; // 定义以list为底层容器的队列 所以STL 队列也不被归类为容器,而被归类为container adapter( 容器适配器)。 -我这里讲的都是C++ 语言中情况, 使用其他语言的同学也要思考栈与队列的底层实现问题, 不要对数据结构的使用浅尝辄止,而要深挖起内部原理,才能夯实基础。 +我这里讲的都是C++ 语言中的情况, 使用其他语言的同学也要思考栈与队列的底层实现问题, 不要对数据结构的使用浅尝辄止,而要深挖其内部原理,才能夯实基础。 diff --git a/problems/背包问题理论基础完全背包.md b/problems/背包问题理论基础完全背包.md index 06bda147..0f721bf9 100644 --- a/problems/背包问题理论基础完全背包.md +++ b/problems/背包问题理论基础完全背包.md @@ -82,7 +82,7 @@ dp状态图如下: 就知道了,01背包中二维dp数组的两个for遍历的先后循序是可以颠倒了,一维dp数组的两个for循环先后循序一定是先遍历物品,再遍历背包容量。 -**在完全背包中,对于一维dp数组来说,其实两个for循环嵌套顺序同样无所谓!** +**在完全背包中,对于一维dp数组来说,其实两个for循环嵌套顺序是无所谓的!** 因为dp[j] 是根据 下标j之前所对应的dp[j]计算出来的。 只要保证下标j之前的dp[j]都是经过计算的就可以了。