diff --git a/README.md b/README.md index c241eaef..01e80da8 100644 --- a/README.md +++ b/README.md @@ -297,6 +297,8 @@ |[0349.两个数组的交集](https://github.com/youngyangyang04/leetcode/blob/master/problems/0349.两个数组的交集.md) |哈希表 |简单|**哈希**| |[0350.两个数组的交集II](https://github.com/youngyangyang04/leetcode/blob/master/problems/0350.两个数组的交集II.md) |哈希表 |简单|**哈希**| |[0383.赎金信](https://github.com/youngyangyang04/leetcode/blob/master/problems/0383.赎金信.md) |数组 |简单|**暴力** **字典计数** **哈希**| +|[0404.左叶子之和](https://github.com/youngyangyang04/leetcode/blob/master/problems/0404.左叶子之和.md) |树/二叉树 |简单|**递归** **迭代**| +|[0416.分割等和子集](https://github.com/youngyangyang04/leetcode/blob/master/problems/0416.分割等和子集.md) |动态规划 |中等|**背包问题/01背包**| |[0429.N叉树的层序遍历](https://github.com/youngyangyang04/leetcode/blob/master/problems/0429.N叉树的层序遍历.md) |树 |简单|**队列/广度优先搜索**| |[0434.字符串中的单词数](https://github.com/youngyangyang04/leetcode/blob/master/problems/0434.字符串中的单词数.md) |字符串 |简单|**模拟**| |[0450.删除二叉搜索树中的节点](https://github.com/youngyangyang04/leetcode/blob/master/problems/0450.删除二叉搜索树中的节点.md) |树 |中等|**递归**| diff --git a/pics/416.分割等和子集.png b/pics/416.分割等和子集.png new file mode 100644 index 00000000..8d1b9ef6 Binary files /dev/null and b/pics/416.分割等和子集.png differ diff --git a/pics/416.分割等和子集1.png b/pics/416.分割等和子集1.png new file mode 100644 index 00000000..6be70dc6 Binary files /dev/null and b/pics/416.分割等和子集1.png differ diff --git a/pics/530.二叉搜索树的最小绝对差.png b/pics/530.二叉搜索树的最小绝对差.png new file mode 100644 index 00000000..04ca9a65 Binary files /dev/null and b/pics/530.二叉搜索树的最小绝对差.png differ diff --git a/pics/617.合并二叉树.png b/pics/617.合并二叉树.png new file mode 100644 index 00000000..182f959c Binary files /dev/null and b/pics/617.合并二叉树.png differ diff --git a/problems/0416.分割等和子集.md b/problems/0416.分割等和子集.md new file mode 100644 index 00000000..1dc1b86a --- /dev/null +++ b/problems/0416.分割等和子集.md @@ -0,0 +1,121 @@ + +和Leetcode 473:火柴拼正方形和Leetcode 698:划分为k个相等的子集是 + + +## 思路 + +这道题目是要找是否可以将这个数组分割成两个子集,使得两个子集的元素和相等。 + +那么只要找到集合里能够出现 sum / 2 的集合,就算是可以分割成两个相同元素和子集了。 + +本来是我是想用回溯暴力搜索出所有答案的,各种剪枝,还是超时了,不想在调了,放弃回溯,直接上01背包吧。 + +如下的讲解中,我讲的重点是如何把01背包应用到此题,而不是讲01背包,如果对01背包本身还不理解的同学,需要额外学习一下基础知识,我后面也会在[代码随想录](https://img-blog.csdnimg.cn/20200815195519696.png)里深度讲解背包问题。 + +### 背包问题 + +背包问题,大家都知道,就是书包,书包可以容纳的体积n, 然后有各种商品,每一种商品体积为m,价值为z,问如果把书包塞满(不一定能塞满),书包里的商品最大价值总和是多少。 + +**背包问题有多种背包方式,常见的有:01背包、完全背包、多重背包、分组背包和混合背包等等。** + +要注意背包问题问题中商品是不是可以重复放入。 + +**即一个商品如果可以重复多次放入是完全背包,而只能放入一次是01背包,写法还是不一样的。** + +**要明确本题中我们要使用的是01背包,因为元素我们只能用一次。** + +为了让大家对背包问题有一个整体的了解,可以看如下图: + + + +回归主题:首先,本题要求集合里能否出现总和为 sum / 2 的子集。 + +那么来一一对应一下本题,看看背包问题如果来解决。 + +**只有确定了如下四点,才能把背包问题,套到本题上来。** + +* 背包的体积为sum / 2 +* 背包要放入的商品(集合里的元素)体积为 元素的数值,价值也为元素的数值 +* 背包如何正好装满,说明找到了总和为 sum / 2 的子集。 +* 背包中每一个元素一定是不可重复放入。 + + +定义里数组为dp[],dp[i] 表示 背包中放入体积为i的商品,最大价值为dp[i]。 + +套到本题,dp[i]表示 背包中总和是i,最大可以凑成总和为i的元素总和为dp[i]。 + +dp[i]一定是小于等于i的,因为背包不能装入超过自身体积的商品(这里理解为元素数值)。 + +**如果dp[i] == i 说明,集合中的元素正好可以凑成总和i,理解这一点很重要。** + +## C++代码如下(详细注释 ) +``` +class Solution { +public: + bool canPartition(vector& nums) { + int sum = 0; + + // dp[i]中的i表示背包内总和 + // 题目中说:每个数组中的元素不会超过 100,数组的大小不会超过 200 + // 那么背包内总和不会大于20000,所以定义一个20000大的数组。 + vector dp(20001, 0); + for (int i = 0; i < nums.size(); i++) { + sum += nums[i]; + } + if (sum % 2 == 1) return false; + int target = sum / 2; + + // 开始 01背包 + for(int i = 0; i < nums.size(); i++) { + for(int j = target; j >= nums[i]; j--) { // 每一个元素一定是不可重复放入,所以从大到小遍历 + dp[j] = max(dp[j], dp[j - nums[i]] + nums[i]); + } + } + // 集合中的元素正好可以凑成总和target + if (dp[target] == target) return true; + return false; + } +}; +``` + +### 暴力 + +本来是想用回溯暴力搜索出所有答案的,各种剪枝,还是超时了,不想在调了,放弃回溯,直接上01背包吧。 + +回溯搜索超时的代码如下: + +``` +class Solution { +private: + int target; + bool backtracking(vector& nums, int startIndex, int pathSum, vector& used) { + for (int i = startIndex; i < nums.size(); i++) { + if (pathSum > target) break; // 剪枝 + if (target < nums[i]) break; // 剪枝 + if (i > 0 && nums[i] == nums[i - 1] && used[i - 1] == false) { // 去重 + continue; + } + pathSum += nums[i]; + used[i] = true; + if (pathSum == target) return true; + if (backtracking(nums, i + 1, pathSum, used)) return true; + used[i] = false; + pathSum -= nums[i]; + } + return false; + } + +public: + bool canPartition(vector& nums) { + vector used(nums.size(), false); + sort(nums.begin(), nums.end()); + int sum = 0; + for (int i = 0; i < nums.size(); i++) sum += nums[i]; + if (sum % 2 == 1) return false; + target = sum / 2; + cout << "sum:" << sum << " target:" << target << endl; + return backtracking(nums, 0, 0, used); + } +}; + +``` diff --git a/problems/0530.二叉搜索树的最小绝对差.md b/problems/0530.二叉搜索树的最小绝对差.md new file mode 100644 index 00000000..2cd19592 --- /dev/null +++ b/problems/0530.二叉搜索树的最小绝对差.md @@ -0,0 +1,111 @@ + +## 题目地址 + +https://leetcode-cn.com/problems/minimum-absolute-difference-in-bst/ + +## 思路 + +题目中要求在二叉搜索树上任意两节点的差的绝对值的最小值。 + +**注意是二叉搜索树,**二叉搜索树可是有序的。 + +遇到在二叉搜索树上求什么最值啊,差值之类的,就把它想成在一个有序数组上求最值,求差值,这样就简单多了。 + +### 递归 + +那么二叉搜索树如果采用中序遍历,其实就是一个有序数组。 + +**在一个有序数组上求两个数最小差值,这是不是就是一道送分题了。** + +最直观的想法,就是把二叉搜索树转换成有序数组,然后遍历一遍数组,就统计出来最小差值了 + +代码如下: + +``` +class Solution { +private: +vector vec; +void traversal(TreeNode* root) { + if (root == NULL) return; + traversal(root->left); + vec.push_back(root->val); // 将二叉搜索树转换为有序数组 + traversal(root->right); +} +public: + int getMinimumDifference(TreeNode* root) { + vec.clear(); + traversal(root); + if (vec.size() < 2) return 0; + int result = INT_MAX; + for (int i = 1; i < vec.size(); i++) { // 统计有序数组的最小差值 + result = min(result, vec[i] - vec[i-1]); + } + return result; + } +}; +``` + +以上代码是把二叉搜索树转化为有序数组了,其实在二叉搜素树中序遍历的过程中,我们就可以直接计算了。 + +需要用一个pre节点记录一下,当前节点的前一个节点。 + +如图: + + + +代码如下: + +``` +class Solution { +private: +int result = INT_MAX; +TreeNode* pre; +void traversal(TreeNode* cur) { + if (cur == NULL) return; + traversal(cur->left); + if (pre != NULL){ + result = min(result, cur->val - pre->val); + } + pre = cur; // 记录前一个 + traversal(cur->right); +} +public: + int getMinimumDifference(TreeNode* root) { + traversal(root); + return result; + } +}; +``` + +### 迭代 + +看过这两篇[二叉树:听说递归能做的,栈也能做!](https://mp.weixin.qq.com/s/c_zCrGHIVlBjUH_hJtghCg),[二叉树:前中后序迭代方式的写法就不能统一一下么?](https://mp.weixin.qq.com/s/WKg0Ty1_3SZkztpHubZPRg)文章之后,不难写出两种中序遍历的迭代法。 + +下面我给出其中的一种,代码如下: + +``` +class Solution { +public: + int getMinimumDifference(TreeNode* root) { + stack st; + TreeNode* cur = root; + TreeNode* pre = NULL; + int result = INT_MAX; + while (cur != NULL || !st.empty()) { + if (cur != NULL) { // 指针来访问节点,访问到最底层 + st.push(cur); // 将访问的节点放进栈 + cur = cur->left; // 左 + } else { + cur = st.top(); + st.pop(); + if (pre != NULL) { + result = min(result, cur->val - pre->val); // 中 + } + pre = cur; + cur = cur->right; // 右 + } + } + return result; + } +}; +``` diff --git a/problems/0617.合并二叉树.md b/problems/0617.合并二叉树.md index 92eb7d11..cfecb9a1 100644 --- a/problems/0617.合并二叉树.md +++ b/problems/0617.合并二叉树.md @@ -1,13 +1,29 @@ ## 题目地址 https://leetcode-cn.com/problems/merge-two-binary-trees/ -## 思路 +> 合并一下 + +# 617.合并二叉树 + +给定两个二叉树,想象当你将它们中的一个覆盖到另一个上时,两个二叉树的一些节点便会重叠。 + +你需要将他们合并为一个新的二叉树。合并的规则是如果两个节点重叠,那么将他们的值相加作为节点合并后的新值,否则不为 NULL 的节点将直接作为新二叉树的节点。 + +示例 1: + + + +注意: 合并必须从两个树的根节点开始。 + +# 思路 相信这道题目很多同学疑惑的点是如何同时遍历两个二叉树呢? 其实和遍历一个树逻辑是一样的,只不过传入两个树的节点,同时操作。 -那么前中后序应该使用哪种遍历呢? +## 递归 + +二叉树使用递归,就要想使用前中后哪种遍历方式? **本题使用哪种遍历都是可以的!** @@ -15,12 +31,12 @@ https://leetcode-cn.com/problems/merge-two-binary-trees/ 动画如下: - 那么我们来按照递归三部曲来解决: 1. **确定递归函数的参数和返回值:** + 首先那么要合入两个二叉树,那么参数至少是要传入两个二叉树的根节点,返回值就是合并之后二叉树的根节点。 代码如下: @@ -31,9 +47,9 @@ TreeNode* mergeTrees(TreeNode* t1, TreeNode* t2) { 2. **确定终止条件:** -因为是传入了两个树,那么就有两个树遍历的节点t1 和 t2,如果t1 == NULL 了,两个树合并就应该是 t2 了啊(如果t2也为NULL也无所谓)。 +因为是传入了两个树,那么就有两个树遍历的节点t1 和 t2,如果t1 == NULL 了,两个树合并就应该是 t2 了啊(如果t2也为NULL也无所谓,合并之后就是NULL)。 -反过来如果t2 == NULL,那么两个数合并就是t1(如果t1也为NULL也无所谓)。 +反过来如果t2 == NULL,那么两个数合并就是t1(如果t1也为NULL也无所谓,合并之后就是NULL)。 代码如下: @@ -45,14 +61,18 @@ if (t2 == NULL) return t1; // 如果t2为空,合并之后就应该是t1 3. **确定单层递归的逻辑:** -单层递归的逻辑就比较好些了,这里我们用重复利用一下t1这个树,t1就是合并之后树的根节点(所谓的修改了元数据的结构)。 +单层递归的逻辑就比较好些了,这里我们用重复利用一下t1这个树,t1就是合并之后树的根节点(就是修改了原来树的结构)。 那么单层递归中,就要把两棵树的元素加到一起。 ``` t1->val += t2->val; ``` -那么此时t1 的左子树 应该是 合并 t1左子树 t2左子树之后的左子树,t1 的右子树 应该是 合并 t1右子树 t2右子树之后的右子树。 +接下来t1 的左子树是:合并 t1左子树 t2左子树之后的左子树。 + +t1 的右子树:是 合并 t1右子树 t2右子树之后的右子树。 + +最终t1就是合并之后的根节点。 代码如下: @@ -62,7 +82,7 @@ t1->val += t2->val; return t1; ``` -此时前序遍历,修改原输入树结构的完整代码就写出来了,如下: +此时前序遍历,完整代码就写出来了,如下: ``` class Solution { @@ -79,7 +99,7 @@ public: }; ``` -那么中序遍历可不可以呢,也是可以的,代码如下: +那么中序遍历也是可以的,代码如下: ``` class Solution { @@ -96,7 +116,7 @@ public: }; ``` -后序遍历呢,依然可以,代码如下: +后序遍历依然可以,代码如下: ``` class Solution { @@ -115,38 +135,10 @@ public: **但是前序遍历是最好理解的,我建议大家用前序遍历来做就OK。** -**那么如下还总结了四种方法,递归的方式均使用了前序遍历,此时大家应该知道了,以下每一种递归的方法都可以换成中序和后序遍历,所以本题的解法是很多的。** +如上的方法修改了t1的结构,当然也可以不修改t1和t2的结构,重新定一个树。 -**其实这道题目迭代法实现是比较困难的,大家可以试一试,是一道不错的面试进阶题目。** +不修改输入树的结构,前序遍历,代码如下: -四种写法如下: - -1. 递归修改了输入树的结构 -2. 递归不修改树的结构 -3. 递归,一波指针的操作,自己写的野路子(可以用来深度理解一下C++的指针) -4. 迭代(这应该是最简单直观的迭代法代码了,一看就懂) - -## C++代码 - -### 递归 - -修改了输入树的结构,前序遍历 -``` -class Solution { -public: - TreeNode* mergeTrees(TreeNode* t1, TreeNode* t2) { - if (t1 == NULL) return t2; // 如果t1为空,合并之后就应该是t2 - if (t2 == NULL) return t1; // 如果t2为空,合并之后就应该是t1 - // 修改了t1的数值和结构 - t1->val += t2->val; - t1->left = mergeTrees(t1->left, t2->left); - t1->right = mergeTrees(t1->right, t2->right); - return t1; - } -}; -``` - -不修改输入树的结构,前序遍历 ``` class Solution { public: @@ -163,9 +155,61 @@ public: }; ``` -一波指针的操作,自己写的野路子 -想要更改二叉树的值,应该传入指向指针的指针, 如果process(t1, t2);这么写的话,其实只是传入的一个int型的指针,并没有传入地址,要传入指向指针的指针才能完成对t1的修改。 -(前序遍历) +## 迭代法 + +使用迭代法,如何同时处理两棵树呢? + +思路我们在[二叉树:我对称么?](https://mp.weixin.qq.com/s/Kgf0gjvlDlNDfKIH2b1Oxg)中的迭代法已经讲过一次了,求二叉树对称的时候就是把两个树的节点同时加入队列进行比较。 + +本题我们也使用队列,模拟的层序遍历,代码如下: + +``` +class Solution { +public: + TreeNode* mergeTrees(TreeNode* t1, TreeNode* t2) { + if (t1 == NULL) return t2; + if (t2 == NULL) return t1; + queue que; + que.push(t1); + que.push(t2); + while(!que.empty()) { + TreeNode* node1 = que.front(); que.pop(); + TreeNode* node2 = que.front(); que.pop(); + // 此时两个节点一定不为空,val相加 + node1->val += node2->val; + + // 如果两棵树左节点都不为空,加入队列 + if (node1->left != NULL && node2->left != NULL) { + que.push(node1->left); + que.push(node2->left); + } + // 如果两棵树右节点都不为空,加入队列 + if (node1->right != NULL && node2->right != NULL) { + que.push(node1->right); + que.push(node2->right); + } + + // 当t1的左节点 为空 t2左节点不为空,就赋值过去 + if (node1->left == NULL && node2->left != NULL) { + node1->left = node2->left; + } + // 当t1的右节点 为空 t2右节点不为空,就赋值过去 + if (node1->right == NULL && node2->right != NULL) { + node1->right = node2->right; + } + } + return t1; + } +}; +``` + +# 拓展 + +当然也可以秀一波指针的操作,这是我写的野路子,大家就随便看看就行了,以防带跑遍了。 + +如下代码中,想要更改二叉树的值,应该传入指向指针的指针。 + +代码如下:(前序遍历) ``` class Solution { public: @@ -190,45 +234,17 @@ public: } }; ``` -### 迭代 -这应该是最简单直观的迭代法了,模拟的层序遍历。 -``` -class Solution { -public: - TreeNode* mergeTrees(TreeNode* t1, TreeNode* t2) { - if (t1 == NULL) return t2; - if (t2 == NULL) return t1; - queue que; - que.push(t1); - que.push(t2); - while(!que.empty()) { - TreeNode* node1 = que.front(); que.pop(); - TreeNode* node2 = que.front(); que.pop(); - // 此时两个节点一定不为空,val相加 - node1->val += node2->val; - // 如果左节点都不为空,加入队列 - if (node1->left != NULL && node2->left != NULL) { - que.push(node1->left); - que.push(node2->left); - } - // 如果右节点都不为空,加入队列 - if (node1->right != NULL && node2->right != NULL) { - que.push(node1->right); - que.push(node2->right); - } - // 当t1的左节点 为空 t2左节点不为空,就赋值过去 - if (node1->left == NULL && node2->left != NULL) { - node1->left = node2->left; - } - // 当t1的右节点 为空 t2右节点不为空,就赋值过去 - if (node1->right == NULL && node2->right != NULL) { - node1->right = node2->right; - } - } - return t1; - } -}; -``` +# 总结 + +合并二叉树,也是二叉树操作的经典题目,如果没有接触过的话,其实并不简单,因为我们习惯了操作一个二叉树,一起操作两个二叉树,还会有点懵懵的。 + +这不是我们第一次操作两颗二叉树了,在[二叉树:我对称么?](https://mp.weixin.qq.com/s/Kgf0gjvlDlNDfKIH2b1Oxg)中也一起操作了两棵二叉树。 + +迭代法中,一般一起操作两个树都是使用队列模拟类似层序遍历,同时处理两个树的节点,这种方式最好理解,如果用模拟递归的思路的话,要复杂一些。 + +最后拓展中,我给了一个操作指针的野路子,大家随便看看就行了,如果学习C++的话,可以在去研究研究。 + +就酱,学到了的话,就转发给身边需要的同学吧! > 更多算法干货文章持续更新,可以微信搜索「代码随想录」第一时间围观,关注后,回复「Java」「C++」 「python」「简历模板」「数据结构与算法」等等,就可以获得我多年整理的学习资料。