diff --git a/README.md b/README.md index 4609858a..a8427da5 100644 --- a/README.md +++ b/README.md @@ -67,6 +67,8 @@ * [字符串:前缀表不右移,难道就写不出KMP了?](https://mp.weixin.qq.com/s/p3hXynQM2RRROK5c6X7xfw) * [字符串:总结篇!](https://mp.weixin.qq.com/s/gtycjyDtblmytvBRFlCZJg) +* [双指针法:总结篇!](https://mp.weixin.qq.com/s/_p7grwjISfMh0U65uOyCjA) + * 栈与队列 * [栈与队列:来看看栈和队列不为人知的一面](https://mp.weixin.qq.com/s/VZRjOccyE09aE-MgLbCMjQ) * [栈与队列:我用栈来实现队列怎么样?](https://mp.weixin.qq.com/s/P6tupDwRFi6Ay-L7DT4NVg) @@ -76,6 +78,9 @@ * [栈与队列:有没有想过计算机是如何处理表达式的?](https://mp.weixin.qq.com/s/hneh2nnLT91rR8ms2fm_kw) * [栈与队列:滑动窗口里求最大值引出一个重要数据结构](https://mp.weixin.qq.com/s/8c6l2bO74xyMjph09gQtpA) * [栈与队列:求前 K 个高频元素和队列有啥关系?](https://mp.weixin.qq.com/s/8hMwxoE_BQRbzCc7CA8rng) + * [栈与队列:总结篇!](https://mp.weixin.qq.com/s/xBcHyvHlWq4P13fzxEtkPg) +* 二叉树 + * [关于二叉树,你该了解这些!](https://mp.weixin.qq.com/s/_ymfWYvTNd2GvWvC5HOE4A) (持续更新中....) @@ -242,6 +247,7 @@ |[0459.重复的子字符串](https://github.com/youngyangyang04/leetcode/blob/master/problems/0459.重复的子字符串.md) |字符创 |简单| **KMP**| |[0486.预测赢家](https://github.com/youngyangyang04/leetcode/blob/master/problems/0486.预测赢家.md) |动态规划 |中等| **递归** **记忆递归** **动态规划**| |[0491.递增子序列](https://github.com/youngyangyang04/leetcode/blob/master/problems/0491.递增子序列.md) |深度优先搜索 |中等|**深度优先搜索/回溯算法**| +|[0538.把二叉搜索树转换为累加树](https://github.com/youngyangyang04/leetcode/blob/master/problems/0538.把二叉搜索树转换为累加树.md) |二叉树 |简单|**递归** **迭代**| |[0541.反转字符串II](https://github.com/youngyangyang04/leetcode/blob/master/problems/0541.反转字符串II.md) |字符串 |简单| **模拟**| |[0575.分糖果](https://github.com/youngyangyang04/leetcode/blob/master/problems/0575.分糖果.md) |哈希表 |简单|**哈希**| |[0617.合并二叉树](https://github.com/youngyangyang04/leetcode/blob/master/problems/0617.合并二叉树.md) |树 |简单|**递归** **迭代**| diff --git a/pics/538.把二叉搜索树转换为累加树.png b/pics/538.把二叉搜索树转换为累加树.png new file mode 100644 index 00000000..1417fef9 Binary files /dev/null and b/pics/538.把二叉搜索树转换为累加树.png differ diff --git a/pics/968.监控二叉树1.png b/pics/968.监控二叉树1.png new file mode 100644 index 00000000..7e6a75df Binary files /dev/null and b/pics/968.监控二叉树1.png differ diff --git a/pics/968.监控二叉树2.png b/pics/968.监控二叉树2.png new file mode 100644 index 00000000..664a656e Binary files /dev/null and b/pics/968.监控二叉树2.png differ diff --git a/pics/968.监控二叉树3.png b/pics/968.监控二叉树3.png new file mode 100644 index 00000000..0a3a9ea6 Binary files /dev/null and b/pics/968.监控二叉树3.png differ diff --git a/problems/0102.二叉树的层序遍历.md b/problems/0102.二叉树的层序遍历.md index b5998a45..04520204 100644 --- a/problems/0102.二叉树的层序遍历.md +++ b/problems/0102.二叉树的层序遍历.md @@ -7,6 +7,7 @@ https://leetcode-cn.com/problems/binary-tree-level-order-traversal/ 相似题目: * [0102.二叉树的层序遍历](https://github.com/youngyangyang04/leetcode/blob/master/problems/0102.二叉树的层序遍历.md) +* [0107.二叉树的层次遍历II](https://github.com/youngyangyang04/leetcode/blob/master/problems/0107.二叉树的层次遍历II.md) * [0199.二叉树的右视图](https://github.com/youngyangyang04/leetcode/blob/master/problems/0199.二叉树的右视图.md) * [0637.二叉树的层平均值](https://github.com/youngyangyang04/leetcode/blob/master/problems/0637.二叉树的层平均值.md) diff --git a/problems/0538.把二叉搜索树转换为累加树.md b/problems/0538.把二叉搜索树转换为累加树.md new file mode 100644 index 00000000..f50a03f1 --- /dev/null +++ b/problems/0538.把二叉搜索树转换为累加树.md @@ -0,0 +1,116 @@ + +## 题目地址 + +https://leetcode-cn.com/problems/convert-bst-to-greater-tree/ + +## 思路 + +一看到累加树,相信很多小伙伴一脸懵逼,如何累加,遇到一个节点,然后在遍历其他节点累加?怎么一想这么麻烦呢。 + +然后发现这是一颗二叉搜索树,二叉搜索树啊,这是有序的啊。 + +那么有序的元素如果求累加呢? + +**其实这就是一棵树,大家可能看起来有点别扭,换一个角度来看,这就是一个有序数组[2, 5, 13],求从后到前的累加数组,也就是[20, 18, 13],大家是不是感觉这就是送分题了。** + +为什么变成数组就是送分题了呢,因为数组大家都知道怎么遍历啊,从后向前,挨个累加就完事了,这换成了二叉搜索树,看起来就别扭了一些是不是。 + +那么知道如何遍历这个二叉树,也就迎刃而解了,从树中可以看出累加的顺讯是 右中左,所以我们需要中序遍历反过来遍历这个二叉树,然后顺序累加就可以了。 + +遍历顺序如图所示: + + + + +以下我给出一种递归的写法,两种迭代法的写法,别问我为什么写出了这么多写法,把我写的这个题解[彻底吃透二叉树的前中后序递归法和迭代法!!](https://leetcode-cn.com/problems/binary-tree-inorder-traversal/solution/che-di-chi-tou-er-cha-shu-de-qian-zhong-hou-xu-d-2/)看了,你也能分分钟写出来三种写法![机智] + +## C++递归代码 + +``` +class Solution { +private: + int pre; // 记录前一个节点的数值 + void traversal(TreeNode* cur) { // 右中左遍历 + if (cur == NULL) return; + traversal(cur->right); + cur->val += pre; + pre = cur->val; + traversal(cur->left); + } +public: + TreeNode* convertBST(TreeNode* root) { + pre = 0; + traversal(root); + return root; + } +}; +``` + +## C++迭代法(一)代码 + +``` +class Solution { +private: + int pre; // 记录前一个节点的数值 + void traversal(TreeNode* root) { + stack st; + TreeNode* cur = root; + while (cur != NULL || !st.empty()) { + if (cur != NULL) { + st.push(cur); + cur = cur->right; // 右 + } else { + cur = st.top(); // 中 + st.pop(); + cur->val += pre; + pre = cur->val; + cur = cur->left; // 左 + } + } + } +public: + TreeNode* convertBST(TreeNode* root) { + pre = 0; + traversal(root); + return root; + } +}; +``` + +## C++迭代法(二)代码 + +``` +class Solution { +private: + int pre; // 记录前一个节点的数值 + void traversal(TreeNode* root) { + stack st; + if (root != NULL) st.push(root); + while (!st.empty()) { + TreeNode* node = st.top(); + if (node != NULL) { + st.pop(); + if (node->left) st.push(node->left); // 左 + + st.push(node); // 中 + st.push(NULL); + + if (node->right) st.push(node->right); // 右 + } else { + st.pop(); + node = st.top(); + st.pop(); + node->val += pre; // 处理中间节点 + pre = node->val; + } + } + } + +public: + TreeNode* convertBST(TreeNode* root) { + pre = 0; + traversal(root); + return root; + } +}; +``` diff --git a/problems/0968.监控二叉树.md b/problems/0968.监控二叉树.md new file mode 100644 index 00000000..613a5a5c --- /dev/null +++ b/problems/0968.监控二叉树.md @@ -0,0 +1,217 @@ +# 题目地址 +https://leetcode-cn.com/problems/binary-tree-cameras/ + +## 思路 + +这道题目其实不是那么好理解的,题目举的示例不是很典型,会误以为摄像头必须要放在中间,其实放哪里都可以只要覆盖了就行。 + +这道题目难在两点: + +1. 需要确定遍历方式 +2. 需要状态转移的方程 + +我们之前做动态规划的时候,只要最难的地方在于确定状态转移方程,至于遍历方式无非就是在数组或者二维数组上。 + +**而本题,不仅要确定状态转移方式,而且要在树上进行推导,所以难度就上来了,一些同学知道这道题目难,但其实说不上难点究竟在哪。** + +1. 需要确定遍历方式 + +首先先确定遍历方式,才能确定转移方程,那么该如何遍历呢? + +在安排选择摄像头的位置的时候,**我们要从底向上进行推导,因为尽量让叶子节点的父节点安装摄像头,这样摄像头的数量才是最少的** + +如何从低向上推导呢? + +就是后序遍历也就是左右中的顺序,这样就可以从下到上进行推导了。 + +后序遍历代码如下: + +``` + int traversal(TreeNode* cur) { + + // 空节点,该节点有覆盖 + if (终止条件) return ; + + int left = traversal(cur->left); // 左 + int right = traversal(cur->right); // 右 + + 逻辑处理 // 中 + + return ; + } +``` + +**注意在以上代码中我们取了左孩子的返回值,右孩子的返回值,即left 和 right, 以后推导中间节点的状态** + +2. 需要状态转移的方程 + +确定了遍历顺序,再看看这个状态应该如何转移,先来看看每个节点可能有几种状态: + +可以说有如下三种: + +* 该节点无覆盖 +* 本节点有摄像头 +* 本节点有覆盖 + +我们分别有三个数字来表示: + +* 0:该节点无覆盖 +* 1:本节点有摄像头 +* 2:本节点有覆盖 + +大家应该找不出第四个节点的状态了。 + +那么问题来了,空节点究竟是哪一种状态呢? 空节点表示无覆盖? 表示有摄像头?还是有覆盖呢? + +回归本质,为了让摄像头数量最少,我们要尽量让叶子节点的父节点安装摄像头,这样才能摄像头的数量最少。 + +那么空节点不能是无覆盖的状态,这样叶子节点就可以放摄像头了,空节点也不能是有摄像头的状态,这样叶子节点的父节点就没有必要放摄像头了,而是可以把摄像头放在叶子节点的爷爷节点上。 + +**所以空节点的状态只能是有覆盖,这样就可以在叶子节点的父节点放摄像头了** + +接下来就是递推关系。 + +那么递归的终止条件应该是遇到了空节点,此时应该返回2(有覆盖),原因上面已经解释过了。 + +代码如下: + +``` + // 空节点,该节点有覆盖 + if (cur == NULL) return 2; +``` + +递归的函数,以及终止条件已经确定了,再来看单层逻辑处理。 + +主要有如下四类情况: + +1. 情况1:左右节点都有覆盖 + +左孩子有覆盖,右孩子有覆盖,那么此时中间节点应该就是无覆盖的状态了。 + +如图: + + + +代码如下: + +``` + // 左右节点都有覆盖 + if (left == 2 && right == 2) return 0; +``` + +2. 情况2:左右节点至少有一个无覆盖的情况 + +如果是以下情况,则中间节点(父节点)应该放摄像头: + +left == 0 && right == 0 左右节点无覆盖 +left == 1 && right == 0 左节点有摄像头,右节点无覆盖 +left == 0 && right == 1 左节点有无覆盖,右节点摄像头 +left == 0 && right == 2 左节点无覆盖,右节点覆盖 +left == 2 && right == 0 左节点覆盖,右节点无覆盖 + +这个不难理解,毕竟有一个孩子没有覆盖,父节点就应该放摄像头。 + +此时摄像头的数量要加一,并且return 1,代表中间节点放摄像头。 + +代码如下: +``` + if (left == 0 || right == 0) { + result++; + return 1; + } +``` + +3. 情况3:左右节点至少有一个有摄像头 + +如果是以下情况,其实就是 左右孩子节点有一个有摄像头了,那么其父节点就应该是2(覆盖的状态) + +left == 1 && right == 2 左节点有摄像头,右节点有覆盖 +left == 2 && right == 1 左节点有覆盖,右节点有摄像头 +left == 1 && right == 1 左右节点都有摄像头 + +代码如下: + +``` + if (left == 1 || right == 1) return 2; +``` + +**从这个代码中,可以看出,如果left == 1, right == 0 怎么办?其实这种条件在情况2中已经判断过了**,如图: + + + +这种情况也是大多数同学容易迷惑的情况。 + +4. 情况4 + +以上都处理完了,递归结束之后,可能头结点 还有一个无覆盖的情况,如图: + + + +所以递归结束之后,还要判断根节点,如果没有覆盖,result++,代码如下: + +``` + int minCameraCover(TreeNode* root) { + result = 0; + if (traversal(root) == 0) { // root 无覆盖 + result++; + } + return result; + } +``` + +以上四种情况我们分析完了,代码也差不多了,整体代码如下: + +(**以下我的代码是可以精简的,但是我是为了把情况说清楚,特别把每种情况列出来,因为精简之后的代码读者不好理解。**) + +## C++代码 + +``` +class Solution { +private: + int result; + int traversal(TreeNode* cur) { + + // 空节点,该节点有覆盖 + if (cur == NULL) return 2; + + int left = traversal(cur->left); // 左 + int right = traversal(cur->right); // 右 + + // 情况1 + // 左右节点都有覆盖 + if (left == 2 && right == 2) return 0; + + // 情况2 + // left == 0 && right == 0 左右节点无覆盖 + // left == 1 && right == 0 左节点有摄像头,右节点无覆盖 + // left == 0 && right == 1 左节点有无覆盖,右节点摄像头 + // left == 0 && right == 2 左节点无覆盖,右节点覆盖 + // left == 2 && right == 0 左节点覆盖,右节点无覆盖 + if (left == 0 || right == 0) { + result++; + return 1; + } + + // 情况3 + // left == 1 && right == 2 左节点有摄像头,右节点有覆盖 + // left == 2 && right == 1 左节点有覆盖,右节点有摄像头 + // left == 1 && right == 1 左右节点都有摄像头 + // 其他情况前段代码均已覆盖 + if (left == 1 || right == 1) return 2; + + // 以上代码我没有使用else,主要是为了把各个分支条件展现出来,这样代码有助于读者理解 + // 这个 return -1 逻辑不会走到这里。 + return -1; + } + +public: + int minCameraCover(TreeNode* root) { + result = 0; + // 情况4 + if (traversal(root) == 0) { // root 无覆盖 + result++; + } + return result; + } +}; +```