diff --git a/README.md b/README.md index 0eae3978..55434203 100644 --- a/README.md +++ b/README.md @@ -113,6 +113,7 @@ * [本周小结!(二叉树系列三)](https://mp.weixin.qq.com/s/JLLpx3a_8jurXcz6ovgxtg) * [二叉树:合并两个二叉树](https://mp.weixin.qq.com/s/3f5fbjOFaOX_4MXzZ97LsQ) * [二叉树:二叉搜索树登场!](https://mp.weixin.qq.com/s/vsKrWRlETxCVsiRr8v_hHg) + * [二叉树:搜索树的最小绝对差](https://mp.weixin.qq.com/s/Hwzml6698uP3qQCC1ctUQQ) @@ -268,8 +269,8 @@ |[0110.平衡二叉树](https://github.com/youngyangyang04/leetcode/blob/master/problems/0110.平衡二叉树.md) |树 |简单|**递归**| |[0111.二叉树的最小深度](https://github.com/youngyangyang04/leetcode/blob/master/problems/0111.二叉树的最小深度.md) |树 |简单|**递归** **队列/BFS**| |[0112.路径总和](https://github.com/youngyangyang04/leetcode/blob/master/problems/0112.路径总和.md) |树 |简单|**深度优先搜索/递归** **回溯** **栈**| -|[0116.填充每个节点的下一个右侧节点指针](https://github.com/youngyangyang04/leetcode/blob/master/problems/0116.填充每个节点的下一个右侧节点指针.md) |二叉树 |中等|**广度优先搜索**| -|[0117.填充每个节点的下一个右侧节点指针II](https://github.com/youngyangyang04/leetcode/blob/master/problems/0117.填充每个节点的下一个右侧节点指针II.md) |二叉树 |中等|**广度优先搜索**| +|[0116.填充每个节点的下一个右侧节点指针](https://github.com/youngyangyang04/leetcode/blob/master/problems/0116.填充每个节点的下一个右侧节点指针.md) |二叉树 |中等|**递归** **迭代/广度优先搜索**| +|[0117.填充每个节点的下一个右侧节点指针II](https://github.com/youngyangyang04/leetcode/blob/master/problems/0117.填充每个节点的下一个右侧节点指针II.md) |二叉树 |中等|**递归** **迭代/广度优先搜索**| |[0131.分割回文串](https://github.com/youngyangyang04/leetcode/blob/master/problems/0131.分割回文串.md) |回溯 |中等|**回溯**| |[0141.环形链表](https://github.com/youngyangyang04/leetcode/blob/master/problems/0141.环形链表.md) |链表 |简单|**快慢指针/双指针**| |[0142.环形链表II](https://github.com/youngyangyang04/leetcode/blob/master/problems/0142.环形链表II.md) |链表 |中等|**快慢指针/双指针**| @@ -325,6 +326,7 @@ |[0637.二叉树的层平均值](https://github.com/youngyangyang04/leetcode/blob/master/problems/0637.二叉树的层平均值.md) |树 |简单|**广度优先搜索/队列**| |[0654.最大二叉树](https://github.com/youngyangyang04/leetcode/blob/master/problems/0654.最大二叉树.md) |树 |中等|**递归**| |[0685.冗余连接II](https://github.com/youngyangyang04/leetcode/blob/master/problems/0685.冗余连接II.md) | 并查集/树/图 |困难|**并查集**| +|[0669.修剪二叉搜索树](https://github.com/youngyangyang04/leetcode/blob/master/problems/0669.修剪二叉搜索树.md) | 二叉搜索树/二叉树 |简单|**递归** **迭代**| |[0700.二叉搜索树中的搜索](https://github.com/youngyangyang04/leetcode/blob/master/problems/0700.二叉搜索树中的搜索.md) |树 |简单|**递归** **迭代**| |[0701.二叉搜索树中的插入操作](https://github.com/youngyangyang04/leetcode/blob/master/problems/0701.二叉搜索树中的插入操作.md) |树 |简单|**递归** **迭代**| |[0705.设计哈希集合](https://github.com/youngyangyang04/leetcode/blob/master/problems/0705.设计哈希集合.md) |哈希表 |简单|**模拟**| diff --git a/pics/116.填充每个节点的下一个右侧节点指针.png b/pics/116.填充每个节点的下一个右侧节点指针.png index 0078731b..bec25c0a 100644 Binary files a/pics/116.填充每个节点的下一个右侧节点指针.png and b/pics/116.填充每个节点的下一个右侧节点指针.png differ diff --git a/pics/669.修剪二叉搜索树.png b/pics/669.修剪二叉搜索树.png new file mode 100644 index 00000000..89fe4104 Binary files /dev/null and b/pics/669.修剪二叉搜索树.png differ diff --git a/pics/669.修剪二叉搜索树1.png b/pics/669.修剪二叉搜索树1.png new file mode 100644 index 00000000..1b46e8d0 Binary files /dev/null and b/pics/669.修剪二叉搜索树1.png differ diff --git a/problems/0116.填充每个节点的下一个右侧节点指针.md b/problems/0116.填充每个节点的下一个右侧节点指针.md index 8dfa00e8..def34c1d 100644 --- a/problems/0116.填充每个节点的下一个右侧节点指针.md +++ b/problems/0116.填充每个节点的下一个右侧节点指针.md @@ -4,9 +4,68 @@ https://leetcode-cn.com/problems/populating-next-right-pointers-in-each-node/ ## 思路 -同 117题目,代码都是一样的 -## C++代码 + +注意题目提示内容,: +* 你只能使用常量级额外空间。 +* 使用递归解题也符合要求,本题中递归程序占用的栈空间不算做额外的空间复杂度。 + +基本上就是要求使用递归了,迭代的方式一定会用到栈或者队列。 + +### 递归 + +一想用递归怎么做呢,虽然层序遍历是最直观的,但是递归的方式确实不好想。 + +如图,假如当前操作的节点是cur: + + + +最关键的点是可以通过上一层递归 搭出来的线,进行本次搭线。 + +图中cur节点为元素4,那么搭线的逻辑代码:(**注意注释中操作1和操作2和图中的对应关系**) + +``` +if (cur->left) cur->left->next = cur->right; // 操作1 +if (cur->right) { + if (cur->next) cur->right->next = cur->next->left; // 操作2 + else cur->right->next = NULL; +} +``` + +理解到这里,使用前序遍历,那么不难写出如下代码: + +如果对二叉树的前中后序不了解看这篇:[二叉树:一入递归深似海,从此offer是路人](https://mp.weixin.qq.com/s/PwVIfxDlT3kRgMASWAMGhA) + + +``` +class Solution { +private: + void traversal(Node* cur) { + if (cur == NULL) return; + // 中 + if (cur->left) cur->left->next = cur->right; // 操作1 + if (cur->right) { + if (cur->next) cur->right->next = cur->next->left; // 操作2 + else cur->right->next = NULL; + } + traversal(cur->left); // 左 + traversal(cur->right); // 右 + } +public: + Node* connect(Node* root) { + traversal(root); + return root; + } +}; +``` + +### 迭代(层序遍历) + +本题使用层序遍历是最为直观的,如果对层序遍历不了解,看这篇:[二叉树:层序遍历登场!](https://mp.weixin.qq.com/s/Gb3BjakIKGNpup2jYtTzog)。 + +层序遍历本来就是一层一层的去遍历,记录一层的头结点(nodePre),然后让nodePre指向当前遍历的节点就可以了。 + +代码如下: ``` @@ -20,9 +79,9 @@ public: vector vec; Node* nodePre; Node* node; - for (int i = 0; i < size; i++) { + for (int i = 0; i < size; i++) { // 开始每一层的遍历 if (i == 0) { - nodePre = que.front(); // 取出一层的头结点 + nodePre = que.front(); // 记录一层的头结点 que.pop(); node = nodePre; } else { diff --git a/problems/0117.填充每个节点的下一个右侧节点指针II.md b/problems/0117.填充每个节点的下一个右侧节点指针II.md index 41d52566..43b56b91 100644 --- a/problems/0117.填充每个节点的下一个右侧节点指针II.md +++ b/problems/0117.填充每个节点的下一个右侧节点指针II.md @@ -4,6 +4,8 @@ https://leetcode-cn.com/problems/populating-next-right-pointers-in-each-node-ii/ ## 思路 +这道题目使用递归还是有难度的,不是完美二叉树,应该怎么办。 + ## C++代码 ``` diff --git a/problems/0236.二叉树的最近公共祖先.md b/problems/0236.二叉树的最近公共祖先.md index e222a34a..bcfb336f 100644 --- a/problems/0236.二叉树的最近公共祖先.md +++ b/problems/0236.二叉树的最近公共祖先.md @@ -21,7 +21,6 @@ public: TreeNode* left = lowestCommonAncestor(root->left, p, q); TreeNode* right = lowestCommonAncestor(root->right, p, q); if (left != NULL && right != NULL) return root; - if (left == NULL) return right; return left; } diff --git a/problems/0501.二叉搜索树中的众数.md b/problems/0501.二叉搜索树中的众数.md index 2781d5c2..5e727433 100644 --- a/problems/0501.二叉搜索树中的众数.md +++ b/problems/0501.二叉搜索树中的众数.md @@ -2,20 +2,96 @@ https://leetcode-cn.com/problems/find-mode-in-binary-search-tree/solution/ +> 二叉树上应该怎么求,二叉搜索树上有应该怎么求 + +# 501.二叉搜索树中的众数 + +给定一个有相同值的二叉搜索树(BST),找出 BST 中的所有众数(出现频率最高的元素)。 + +假定 BST 有如下定义: + +* 结点左子树中所含结点的值小于等于当前结点的值 +* 结点右子树中所含结点的值大于等于当前结点的值 +* 左子树和右子树都是二叉搜索树 + +例如: + +给定 BST [1,null,2,2], + +![501. 二叉搜索树中的众数](https://img-blog.csdnimg.cn/20201014221532206.png) + +返回[2]. + +提示:如果众数超过1个,不需考虑输出顺序 + +进阶:你可以不使用额外的空间吗?(假设由递归产生的隐式调用栈的开销不被计算在内) + # 思路 +这道题目呢,递归法我从两个维度来讲。 -## 暴力统计 +首先如果不是二叉搜索树的话,应该怎么解题,是二叉搜索树,又应该如何解题,两种方式做一个比较,可以加深大家对二叉树的理解。 -这看到这道题目,最直观的方法一定是把这个树都遍历了,用map统计频率,用vector排个序,最后出去前面高频的元素。 +## 递归法 -其实这是可以的,也是有效的,面试中时间紧张,可能快速的把这个方法实现出来,后面在考虑满满优化。 +### 如果不是二叉搜索树 + +如果不是二叉搜索树,最直观的方法一定是把这个树都遍历了,用map统计频率,把频率排个序,最后取前面高频的元素的集合。 + +具体步骤如下: + +1. 这个树都遍历了,用map统计频率 + +至于用前中后序那种遍历也不重要,因为就是要全遍历一遍,怎么个遍历法都行,层序遍历都没毛病! + +这里采用前序遍历,代码如下: + +``` +// map key:元素,value:出现频率 +void searchBST(TreeNode* cur, unordered_map& map) { // 前序遍历 + if (cur == NULL) return ; + map[cur->val]++; // 统计元素频率 + searchBST(cur->left, map); + searchBST(cur->right, map); + return ; +} +``` + +2. 把统计的出来的出现频率(即map中的value)排个序 + +有的同学可能可以想直接对map中的value排序,还真做不到,C++中如果使用std::map或者std::multimap可以对key排序,但不能对value排序。 + +所以要把map转化数组即vector,再进行排序,当然vector里面放的也是`pair`类型的数据,第一个int为元素,第二个int为出现频率。 + +代码如下: + +``` +bool static cmp (const pair& a, const pair& b) { + return a.second > b.second; // 按照频率从大到小排序 +} + +vector> vec(map.begin(), map.end()); +sort(vec.begin(), vec.end(), cmp); // 给频率排个序 +``` + +3. 取前面高频的元素 + +此时数组vector中已经是存放着按照频率排好序的pair,那么把前面高频的元素取出来就可以了。 + +代码如下: + +``` +result.push_back(vec[0].first); +for (int i = 1; i < vec.size(); i++) { + // 取最高的放到result数组中 + if (vec[i].second == vec[0].second) result.push_back(vec[i].first); + else break; +} +return result; +``` -至于用前中后序那种遍历也不重要,因为就是要全遍历一遍,怎么个遍历法都行,层序遍历都没毛病! - -这种方法C++代码如下: - +整体C++代码如下: ``` class Solution { @@ -33,14 +109,15 @@ bool static cmp (const pair& a, const pair& b) { } public: vector findMode(TreeNode* root) { - unordered_map map; + unordered_map map; // key:元素,value:出现频率 vector result; if (root == NULL) return result; searchBST(root, map); vector> vec(map.begin(), map.end()); sort(vec.begin(), vec.end(), cmp); // 给频率排个序 result.push_back(vec[0].first); - for (int i = 1; i < vec.size(); i++) { + for (int i = 1; i < vec.size(); i++) { + // 取最高的放到result数组中 if (vec[i].second == vec[0].second) result.push_back(vec[i].first); else break; } @@ -49,15 +126,11 @@ public: }; ``` -**这种方法的缺点是没有利用上二叉搜索树这一特性**,如果用这种方法,这道题就可以是普通的二叉树就行了,反正都要全撸一遍统计频率。 +**所以如果本题没有说是二叉搜索树的话,那么就按照上面的思路写!** -## 中序遍历 +### 是二叉搜索树 -既然是搜索树,它就是有序的,那么如何有序呢? - -**搜索树在中序遍历的过程中,就是有序序列,所以此时的问题相当于 给出如果给出一个有序数组,求最大频率的元素集合。** - -**所以我们要采用中序遍历!** +**既然是搜索树,它中序遍历就是有序的**。 如图: @@ -66,69 +139,82 @@ public: 中序遍历代码如下: ``` - void searchBST(TreeNode* cur) { - if (cur == NULL) return ; - searchBST(cur->left); // 左 - (处理节点) // 中 - searchBST(cur->right); // 右 - return ; - } +void searchBST(TreeNode* cur) { + if (cur == NULL) return ; + searchBST(cur->left); // 左 + (处理节点) // 中 + searchBST(cur->right); // 右 + return ; +} ``` -遍历有序数组的元素出现频率,从头遍历,那么一定是相邻两个元素作比较,要是数组的话,好搞,在树上怎么搞呢? +遍历有序数组的元素出现频率,从头遍历,那么一定是相邻两个元素作比较,然后就把出现频率最高的元素输出就可以了。 -需要弄一个指针指向前一个节点,这样每次cur(当前节点)才能和pre(前一个节点)作比较。 +关键是在有序数组上的话,好搞,在树上怎么搞呢? -而且初始化的时候pre = NULL,这样当pre为NULL时候,我们就知道这是比较的第一个元素,然后再给pre赋值即pre = cur; +这就考察对树的操作了。 + +在[二叉树:搜索树的最小绝对差](https://mp.weixin.qq.com/s/Hwzml6698uP3qQCC1ctUQQ)中我们就使用了pre指针和cur指针的技巧,这次又用上了。 + +弄一个指针指向前一个节点,这样每次cur(当前节点)才能和pre(前一个节点)作比较。 + +而且初始化的时候pre = NULL,这样当pre为NULL时候,我们就知道这是比较的第一个元素。 代码如下: ``` - if (pre == NULL) { // 第一个节点 - count = 1; - } else if (pre->val == cur->val) { // 与前一个节点数值相同 - count++; - } else { // 与前一个节点数值不同 - count = 1; - } - pre = cur; // 更新上一个节点 +if (pre == NULL) { // 第一个节点 + count = 1; // 频率为1 +} else if (pre->val == cur->val) { // 与前一个节点数值相同 + count++; +} else { // 与前一个节点数值不同 + count = 1; +} +pre = cur; // 更新上一个节点 ``` -此时又有问题了,因为要求最大频率的元素集合,直观想的想法是要先遍历一遍找出频率最大的次数maxCount,然后在重新遍历一遍再把出现频率为maxCount的元素放进集合。 +此时又有问题了,因为要求最大频率的元素集合(注意是集合,不是一个元素,可以有多个众数),如果是数组上大家一般怎么办? +应该是先遍历一遍数组,找出最大频率(maxCount),然后再重新遍历一遍数组把出现频率为maxCount的元素放进集合。(因为众数有多个) + +这种方式遍历了两遍数组。 + +那么我们遍历两遍二叉搜索树,把众数集合算出来也是可以的。 + +但这里其实只需要遍历一次就可以找到所有的众数。 那么如何只遍历一遍呢? -如果 频率count 等于 maxCount,当然要把这个元素加入到结果集中(以下代码为result数组),代码如下: +如果 频率count 等于 maxCount(最大频率),当然要把这个元素加入到结果集中(以下代码为result数组),代码如下: ``` - if (count == maxCount) { // 如果和最大值相同,放进result中 - result.push_back(cur->val); - } +if (count == maxCount) { // 如果和最大值相同,放进result中 + result.push_back(cur->val); +} ``` -当时感觉这里有问题,result怎么能轻易就把元素放进去了呢,万一,这个maxCount此时还不是真正最大值呢。 +是不是感觉这里有问题,result怎么能轻易就把元素放进去了呢,万一,这个maxCount此时还不是真正最大频率呢。 所以下面要做如下操作: 频率count 大于 maxCount的时候,不仅要更新maxCount,而且要清空结果集(以下代码为result数组),因为结果集之前的元素都失效了。 ``` - if (count > maxCount) { // 如果计数大于最大值 - maxCount = count; - result.clear(); // 很关键的一步,不要忘记清空result,之前result里的元素都失效了 - result.push_back(cur->val); - } +if (count > maxCount) { // 如果计数大于最大值 + maxCount = count; // 更新最大频率 + result.clear(); // 很关键的一步,不要忘记清空result,之前result里的元素都失效了 + result.push_back(cur->val); +} ``` -关键代码都讲完了,完整代码如下: +关键代码都讲完了,完整代码如下:(**只需要遍历一遍二叉搜索树,就求出了众数的集合**) ``` class Solution { private: - int count; - int maxCount; + int maxCount; // 最大频率 + int count; // 统计频率 TreeNode* pre; vector result; void searchBST(TreeNode* cur) { @@ -149,8 +235,8 @@ private: result.push_back(cur->val); } - if (count > maxCount) { // 如果计数大于最大值 - maxCount = count; + if (count > maxCount) { // 如果计数大于最大值频率 + maxCount = count; // 更新最大频率 result.clear(); // 很关键的一步,不要忘记清空result,之前result里的元素都失效了 result.push_back(cur->val); } @@ -161,8 +247,8 @@ private: public: vector findMode(TreeNode* root) { - int count = 0; // 记录元素出现次数 - int maxCount = 0; + count = 0; + maxCount = 0; TreeNode* pre = NULL; // 记录前一个节点 result.clear(); @@ -172,8 +258,79 @@ public: }; ``` -此时的运行效率: - +## 迭代法 + +只要把中序遍历转成迭代,中间节点的处理逻辑完全一样。 + +二叉树前中后序转迭代,传送门: + +* [二叉树:前中后序迭代法](https://mp.weixin.qq.com/s/c_zCrGHIVlBjUH_hJtghCg) +* [二叉树:前中后序统一风格的迭代方式](https://mp.weixin.qq.com/s/WKg0Ty1_3SZkztpHubZPRg) + +下面我给出其中的一种中序遍历的迭代法,其中间处理逻辑一点都没有变(我从递归法直接粘过来的代码,连注释都没改,哈哈) + +代码如下: + +``` +class Solution { +public: + vector findMode(TreeNode* root) { + stack st; + TreeNode* cur = root; + TreeNode* pre = NULL; + int maxCount = 0; // 最大频率 + int count = 0; // 统计频率 + vector result; + while (cur != NULL || !st.empty()) { + if (cur != NULL) { // 指针来访问节点,访问到最底层 + st.push(cur); // 将访问的节点放进栈 + cur = cur->left; // 左 + } else { + cur = st.top(); + st.pop(); // 中 + if (pre == NULL) { // 第一个节点 + count = 1; + } else if (pre->val == cur->val) { // 与前一个节点数值相同 + count++; + } else { // 与前一个节点数值不同 + count = 1; + } + if (count == maxCount) { // 如果和最大值相同,放进result中 + result.push_back(cur->val); + } + + if (count > maxCount) { // 如果计数大于最大值频率 + maxCount = count; // 更新最大频率 + result.clear(); // 很关键的一步,不要忘记清空result,之前result里的元素都失效了 + result.push_back(cur->val); + } + pre = cur; + cur = cur->right; // 右 + } + } + return result; + } +}; +``` + +# 总结 + +本题在递归法中,我给出了如果是普通二叉树,应该怎么求众数。 + +知道了普通二叉树的做法时候,我再进一步给出二叉搜索树又应该怎么求众数,这样鲜明的对比,相信会对二叉树又有更深层次的理解了。 + +在递归遍历二叉搜索树的过程中,我还介绍了一个统计最高出现频率元素集合的技巧, 要不然就要遍历两次二叉搜索树才能把这个最高出现频率元素的集合求出来。 + + +**为什么没有这个技巧一定要遍历两次呢? 因为要求的是集合,会有多个众数,如果规定只有一个众数,那么就遍历一次稳稳的了。** + +最后我依然给出对应的迭代法,其实就是迭代法中序遍历的模板加上递归法中中间节点的处理逻辑,分分钟就可以写出来,中间逻辑的代码我都是从递归法中直接粘过来的。 + +**求二叉搜索树中的众数其实是一道简单题,但大家可以发现我写了这么一大篇幅的文章来讲解,主要是为了尽量从各个角度对本题进剖析,帮助大家更快更深入理解二叉树**。 + +**就酱,如果学到了的话,就转发给身边需要的同学吧,可能他们也需要!** + + **需要强调的是 leetcode上的耗时统计是非常不准确的,看个大概就行,一样的代码耗时可以差百分之50以上**,所以leetcode的耗时统计别太当回事,知道理论上的效率优劣就行了。 diff --git a/problems/0530.二叉搜索树的最小绝对差.md b/problems/0530.二叉搜索树的最小绝对差.md index 2cd19592..6a7f8e8e 100644 --- a/problems/0530.二叉搜索树的最小绝对差.md +++ b/problems/0530.二叉搜索树的最小绝对差.md @@ -3,21 +3,33 @@ https://leetcode-cn.com/problems/minimum-absolute-difference-in-bst/ -## 思路 +> 利用二叉搜索树的特性搞起! + +# 530.二叉搜索树的最小绝对差 + +给你一棵所有节点为非负值的二叉搜索树,请你计算树中任意两节点的差的绝对值的最小值。 + +示例: + +![530二叉搜索树的最小绝对差](https://img-blog.csdnimg.cn/20201014223400123.png) + +提示:树中至少有 2 个节点。 + +# 思路 题目中要求在二叉搜索树上任意两节点的差的绝对值的最小值。 -**注意是二叉搜索树,**二叉搜索树可是有序的。 +**注意是二叉搜索树**,二叉搜索树可是有序的。 遇到在二叉搜索树上求什么最值啊,差值之类的,就把它想成在一个有序数组上求最值,求差值,这样就简单多了。 -### 递归 +## 递归 -那么二叉搜索树如果采用中序遍历,其实就是一个有序数组。 +那么二叉搜索树采用中序遍历,其实就是一个有序数组。 **在一个有序数组上求两个数最小差值,这是不是就是一道送分题了。** -最直观的想法,就是把二叉搜索树转换成有序数组,然后遍历一遍数组,就统计出来最小差值了 +最直观的想法,就是把二叉搜索树转换成有序数组,然后遍历一遍数组,就统计出来最小差值了。 代码如下: @@ -47,12 +59,14 @@ public: 以上代码是把二叉搜索树转化为有序数组了,其实在二叉搜素树中序遍历的过程中,我们就可以直接计算了。 -需要用一个pre节点记录一下,当前节点的前一个节点。 +需要用一个pre节点记录一下cur节点的前一个节点。 如图: +一些同学不知道在递归中如何记录前一个节点的指针,其实实现起来是很简单的,大家只要看过一次,写过一次,就掌握了。 + 代码如下: ``` @@ -62,12 +76,12 @@ int result = INT_MAX; TreeNode* pre; void traversal(TreeNode* cur) { if (cur == NULL) return; - traversal(cur->left); - if (pre != NULL){ + traversal(cur->left); // 左 + if (pre != NULL){ // 中 result = min(result, cur->val - pre->val); } pre = cur; // 记录前一个 - traversal(cur->right); + traversal(cur->right); // 右 } public: int getMinimumDifference(TreeNode* root) { @@ -77,11 +91,13 @@ public: }; ``` -### 迭代 +是不是看上去也并不复杂! + +## 迭代 看过这两篇[二叉树:听说递归能做的,栈也能做!](https://mp.weixin.qq.com/s/c_zCrGHIVlBjUH_hJtghCg),[二叉树:前中后序迭代方式的写法就不能统一一下么?](https://mp.weixin.qq.com/s/WKg0Ty1_3SZkztpHubZPRg)文章之后,不难写出两种中序遍历的迭代法。 -下面我给出其中的一种,代码如下: +下面我给出其中的一种中序遍历的迭代法,代码如下: ``` class Solution { @@ -98,8 +114,8 @@ public: } else { cur = st.top(); st.pop(); - if (pre != NULL) { - result = min(result, cur->val - pre->val); // 中 + if (pre != NULL) { // 中 + result = min(result, cur->val - pre->val); } pre = cur; cur = cur->right; // 右 @@ -109,3 +125,17 @@ public: } }; ``` + +# 总结 + +**遇到在二叉搜索树上求什么最值,求差值之类的,都要思考一下二叉搜索树可是有序的,要利用好这一特点。** + +同时要学会在递归遍历的过程中如何记录前后两个指针,这也是一个小技巧,学会了还是很受用的。 + +后面我将继续介绍一系列利用二叉搜索树特性的题目。 + +**就酱,感觉学到了,就转发给身边需要的同学吧** + + + + diff --git a/problems/0669.修剪二叉搜索树.md b/problems/0669.修剪二叉搜索树.md new file mode 100644 index 00000000..8978424c --- /dev/null +++ b/problems/0669.修剪二叉搜索树.md @@ -0,0 +1,217 @@ + +> 如果不对递归有深刻的理解,本题有点难 + +# 669. 修剪二叉搜索树 + +给定一个二叉搜索树,同时给定最小边界L 和最大边界 R。通过修剪二叉搜索树,使得所有节点的值在[L, R]中 (R>=L) 。你可能需要改变树的根节点,所以结果应当返回修剪好的二叉搜索树的新的根节点。 + +![669.修剪二叉搜索树](https://img-blog.csdnimg.cn/20201014173115788.png) + +![669.修剪二叉搜索树1](https://img-blog.csdnimg.cn/20201014173219142.png) + +# 思路 + +相信看到这道题目大家都感觉是一道简单题(事实上leetcode上也表明是简单)。 + +## 递归法 + +直接想法就是:递归处理,然后遇到 `root->val < low || root->val > high` 的时候直接return NULL。一波修建,赶紧利落。 + +不难写出如下代码: + +``` +class Solution { +public: + TreeNode* trimBST(TreeNode* root, int low, int high) { + if (root == nullptr || root->val < low || root->val > high) return nullptr; + root->left = trimBST(root->left, low, high); + root->right = trimBST(root->right, low, high); + return root; + } +}; +``` + +**然而[1, 3]区间在二叉搜索树的中可不是单纯的节点3和左孩子节点0就决定的,还要考虑节点0的右子树。** + +我们在重新关注一下第二个示例,如图: + + + +**所以以上的代码是不可行的!** + +从图中可以看出需要重构二叉树,想想是不是本题就有点复杂了。 + +其实不用重构那么复杂。 + +在上图中我们发现节点0并不符合区间要求,那么将节点0的右孩子 节点2 直接赋给 节点3的左孩子就可以了(就是把元素0减掉),如图: + + + + +理解了最关键部分了我们在递归三部曲: + +* 确定递归函数的参数以及返回值 + +首先**我们要返回函数参数节点(root)为根节点的树 修剪完之后的 新的根节点**,这里我们为什么需要返回值呢? + + +因为是要遍历整棵树,做修改,其实不需要返回值也可以,我们也可以完成修剪(其实就是要删除节点)的操作。 + +但是有返回值,更方便,可以通过 这样的赋值语句:`root->left = trimBST(root->left, int low, int high)` 直接删除root的左孩子(如果root的左孩子不在范围内的话)。 + +**因为`trimBST(root->left, int low, int high)` 将返回当前root左子树(以root->left为根节点的二叉树)修剪完之后的新的根节点。** + +代码如下: + +``` +TreeNode* trimBST(TreeNode* root, int low, int high) +``` + +* 确定终止条件 + +修剪的操作并不是在终止条件上进行的,所以就是遇到空节点返回就可以了。 + +``` +if (root == nullptr ) return nullptr; +``` + +* 确定单层递归的逻辑 + +如果root(当前节点)的元素小于low的数值,那么应该递归右子树,并返回右子树的头结点。 + +代码如下: + +``` +if (root->val < low) { + TreeNode* right = trimBST(root->right, low, high); // 寻找符合区间[low, high]的节点 + return right; +} +``` + +如果root(当前节点)的元素小于high的,那么应该递归左子树,并返回左子树的头结点。 + +代码如下: + +``` +if (root->val > high) { + TreeNode* left = trimBST(root->left, low, high); // 寻找符合区间[low, high]的节点 + return left; +} +``` + +接下来要将处理完左子树的结果赋给root->left,处理完右子树的结果赋给root->right。 + +最后返回root节点,代码如下: + + +``` +root->left = trimBST(root->left, low, high); // root->left接入符合条件的左孩子 +root->right = trimBST(root->right, low, high); // root->right接入符合条件的右孩子 +return root; +``` + +此时大家是不是还没发现这个 多余的节点究竟是如何删除的? + +其实就是通过,这两个语句删除的。 +``` +root->left = trimBST(root->left, low, high); +root->right = trimBST(root->right, low, high); +``` + +root重新规划root的左右孩子究竟是谁,拿图中示例为例,此时root为节点3,那么trimBST(root->left, low, high)返回的就是节点2,`root->left = trimBST(root->left, low, high);` 相当于把节点0删除了。 + +最后整体代码如下: + +``` +class Solution { +public: + // 注意这里是返回该树的头结点 + TreeNode* trimBST(TreeNode* root, int low, int high) { + if (root == nullptr ) return nullptr; + if (root->val < low) { + TreeNode* right = trimBST(root->right, low, high); // 寻找符合区间[low, high]的节点 + return right; + } + if (root->val > high) { + TreeNode* left = trimBST(root->left, low, high); // 寻找符合区间[low, high]的节点 + return left; + } + root->left = trimBST(root->left, low, high); // root->left接入符合条件的左孩子 + root->right = trimBST(root->right, low, high); // root->right接入符合条件的右孩子 + return root; + } +}; +``` + +精简之后代码如下: + +``` +class Solution { +public: + TreeNode* trimBST(TreeNode* root, int low, int high) { + if (root == nullptr) return nullptr; + if (root->val < low) return trimBST(root->right, low, high); + if (root->val > high) return trimBST(root->left, low, high); + root->left = trimBST(root->left, low, high); + root->right = trimBST(root->right, low, high); + return root; + } +}; +``` + +## 迭代法 + +因为二叉搜索树的有序性,不需要使用栈模拟递归的过程。 + +在剪枝的时候,可以分为三步: + +* 将root移动到[L, R] 范围内,注意是左闭右闭区间 +* 剪枝左子树 +* 剪枝右子树 + +代码如下: + +``` +class Solution { +public: + TreeNode* trimBST(TreeNode* root, int L, int R) { + if (!root) return nullptr; + + // 处理头结点,让root移动到[L, R] 范围内,注意是左闭右闭 + while (root->val < L || root->val > R) { + if (root->val < L) root = root->right; // 小于L往右走 + else root = root->left; // 大于R往左走 + } + TreeNode *cur = root; + // 此时root已经在[L, R] 范围内,处理左孩子元素小于L的情况 + while (cur != nullptr) { + while (cur->left && cur->left->val < L) { + cur->left = cur->left->right; + } + cur = cur->left; + } + cur = root; + + // 此时root已经在[L, R] 范围内,处理右孩子大于R的情况 + while (cur != nullptr) { + while (cur->right && cur->right->val > R) { + cur->right = cur->right->left; + } + cur = cur->right; + } + return root; + } +}; +``` + +# 总结 + +修剪二叉搜索树其实并不难,但在递归法中大家可看出我费了很大的功夫来讲解如何删除节点的,这个思路其实是比较绕的。 + +最终的代码倒是很简洁。 + +**如果不对递归有深刻的理解,这道题目还是有难度的!** + +本题我依然给出递归法和迭代法,大家只要掌握递归其实就可以了,如果想进一步学习,就把迭代法也写一写。 + +**就酱,如果学到了,就转发给身边需要的同学吧!**