海口网站建设托管,虾皮跨境电商注册多少钱,广州哪个大学做网站制作好些的,金沙集团186cc成色算法训练DAY22|二叉树8
235. 二叉搜索树的最近公共祖先
力扣题目链接(opens new window)
给定一个二叉搜索树, 找到该树中两个指定节点的最近公共祖先。
百度百科中最近公共祖先的定义为#xff1a;“对于有根树 T 的两个结点 p、q#xff0c;最近公共祖先表示为一个结点…算法训练DAY22|二叉树8
235. 二叉搜索树的最近公共祖先
力扣题目链接(opens new window)
给定一个二叉搜索树, 找到该树中两个指定节点的最近公共祖先。
百度百科中最近公共祖先的定义为“对于有根树 T 的两个结点 p、q最近公共祖先表示为一个结点 x满足 x 是 p、q 的祖先且 x 的深度尽可能大一个节点也可以是它自己的祖先。”
例如给定如下二叉搜索树: root [6,2,8,0,4,7,9,null,null,3,5] 示例 1: 输入: root [6,2,8,0,4,7,9,null,null,3,5], p 2, q 8 输出: 6 解释: 节点 2 和节点 8 的最近公共祖先是 6。
示例 2: 输入: root [6,2,8,0,4,7,9,null,null,3,5], p 2, q 4 输出: 2 解释: 节点 2 和节点 4 的最近公共祖先是 2, 因为根据定义最近公共祖先节点可以为节点本身。
说明: 所有节点的值都是唯一的。 p、q 为不同节点且均存在于给定的二叉搜索树中。
思路
做过二叉树公共祖先问题 (opens new window)题目的同学应该知道利用回溯从底向上搜索遇到一个节点的左子树里有p右子树里有q那么当前节点就是最近公共祖先。
那么本题是二叉搜索树二叉搜索树是有序的那得好好利用一下这个特点。
在有序树里如果判断一个节点的左子树里有p右子树里有q呢
因为是有序树所有 如果 中间节点是 q 和 p 的公共祖先那么 中节点的数组 一定是在 [p, q]区间的。即 中节点 p 中节点 q 或者 中节点 q 中节点 p。
那么只要从上到下去遍历遇到 cur节点是数值在[p, q]区间中则一定可以说明该节点cur就是p 和 q的公共祖先。 那问题来了一定是最近公共祖先吗
如图我们从根节点搜索第一次遇到 cur节点是数值在[q, p]区间中即 节点5此时可以说明 q 和 p 一定分别存在于 节点 5的左子树和右子树中。 此时节点5是不是最近公共祖先 如果 从节点5继续向左遍历那么将错过成为p的祖先 如果从节点5继续向右遍历则错过成为q的祖先。
所以当我们从上向下去递归遍历第一次遇到 cur节点是数值在[q, p]区间中那么cur就是 q和p的最近公共祖先。
理解这一点本题就很好解了。
而递归遍历顺序本题就不涉及到 前中后序了这里没有中节点的处理逻辑遍历顺序无所谓了。
如图所示p为节点6q为节点9 可以看出直接按照指定的方向就可以找到节点8为最近公共祖先而且不需要遍历整棵树找到结果直接返回
#递归法
递归三部曲如下 确定递归函数返回值以及参数
参数就是当前节点以及两个结点 p、q。
返回值是要返回最近公共祖先所以是TreeNode * 。
代码如下
TreeNode* traversal(TreeNode* cur, TreeNode* p, TreeNode* q) 确定终止条件
遇到空返回就可以了代码如下
if (cur NULL) return cur;
其实都不需要这个终止条件因为题目中说了p、q 为不同节点且均存在于给定的二叉搜索树中。也就是说一定会找到公共祖先的所以并不存在遇到空的情况。 确定单层递归的逻辑
在遍历二叉搜索树的时候就是寻找区间[p-val, q-val]注意这里是左闭又闭
那么如果 cur-val 大于 p-val同时 cur-val 大于q-val那么就应该向左遍历说明目标区间在左子树上。
需要注意的是此时不知道p和q谁大所以两个都要判断
代码如下
if (cur-val p-val cur-val q-val) {TreeNode* left traversal(cur-left, p, q);if (left ! NULL) {return left;}
}
细心的同学会发现在这里调用递归函数的地方把递归函数的返回值left直接return。
在二叉树公共祖先问题 (opens new window)中如果递归函数有返回值如何区分要搜索一条边还是搜索整个树。
搜索一条边的写法
if (递归函数(root-left)) return ;
if (递归函数(root-right)) return ;
搜索整个树写法
left 递归函数(root-left);
right 递归函数(root-right);
left与right的逻辑处理;
本题就是标准的搜索一条边的写法遇到递归函数的返回值如果不为空立刻返回。
如果 cur-val 小于 p-val同时 cur-val 小于 q-val那么就应该向右遍历目标区间在右子树。
if (cur-val p-val cur-val q-val) {TreeNode* right traversal(cur-right, p, q);if (right ! NULL) {return right;}
}
剩下的情况就是cur节点在区间p-val cur-val cur-val q-val或者 q-val cur-val cur-val p-val中那么cur就是最近公共祖先了直接返回cur。
代码如下
return cur;
那么整体递归代码如下:
class Solution {
private:TreeNode* traversal(TreeNode* cur, TreeNode* p, TreeNode* q) {if (cur NULL) return cur;// 中if (cur-val p-val cur-val q-val) { // 左TreeNode* left traversal(cur-left, p, q);if (left ! NULL) {return left;}}
if (cur-val p-val cur-val q-val) { // 右TreeNode* right traversal(cur-right, p, q);if (right ! NULL) {return right;}}return cur;}
public:TreeNode* lowestCommonAncestor(TreeNode* root, TreeNode* p, TreeNode* q) {return traversal(root, p, q);}
};
精简后代码如下
class Solution {
public:TreeNode* lowestCommonAncestor(TreeNode* root, TreeNode* p, TreeNode* q) {if (root-val p-val root-val q-val) {return lowestCommonAncestor(root-left, p, q);} else if (root-val p-val root-val q-val) {return lowestCommonAncestor(root-right, p, q);} else return root;}
};
#迭代法
对于二叉搜索树的迭代法大家应该在二叉树二叉搜索树登场 (opens new window)就了解了。
利用其有序性迭代的方式还是比较简单的解题思路在递归中已经分析了。
迭代代码如下
class Solution {
public:TreeNode* lowestCommonAncestor(TreeNode* root, TreeNode* p, TreeNode* q) {while(root) {if (root-val p-val root-val q-val) {root root-left;} else if (root-val p-val root-val q-val) {root root-right;} else return root;}return NULL;}
};
灵魂拷问是不是又被简单的迭代法感动到痛哭流涕
#总结
对于二叉搜索树的最近祖先问题其实要比普通二叉树公共简单的多。
不用使用回溯二叉搜索树自带方向性可以方便的从上向下查找目标区间遇到目标区间内的节点直接返回。
最后给出了对应的迭代法二叉搜索树的迭代法甚至比递归更容易理解也是因为其有序性自带方向性按照目标区间找就行了。
701.二叉搜索树中的插入操作
力扣题目链接
给定二叉搜索树BST的根节点和要插入树中的值将值插入二叉搜索树。 返回插入后二叉搜索树的根节点。 输入数据保证新值和原始二叉搜索树中的任意节点值都不同。
注意可能存在多种有效的插入方式只要树在插入后仍保持为二叉搜索树即可。 你可以返回任意有效的结果。 提示 给定的树上的节点数介于 0 和 10^4 之间 每个节点都有一个唯一整数值取值范围从 0 到 10^8 -10^8 val 10^8 新值和原始二叉搜索树中的任意节点值都不同
思路
这道题目其实是一道简单题目但是题目中的提示有多种有效的插入方式还可以重构二叉搜索树一下子吓退了不少人瞬间感觉题目复杂了很多。
其实可以不考虑题目中提示所说的改变树的结构的插入方式。
如下演示视频中可以看出只要按照二叉搜索树的规则去遍历遇到空节点就插入节点就可以了。 例如插入元素10 需要找到末尾节点插入便可一样的道理来插入元素15插入元素0插入元素6需要调整二叉树的结构么 并不需要。。
只要遍历二叉搜索树找到空节点 插入元素就可以了那么这道题其实就简单了。
接下来就是遍历二叉搜索树的过程了。
#递归
递归三部曲 确定递归函数参数以及返回值
参数就是根节点指针以及要插入元素这里递归函数要不要有返回值呢
可以有也可以没有但递归函数如果没有返回值的话实现是比较麻烦的下面也会给出其具体实现代码。
有返回值的话可以利用返回值完成新加入的节点与其父节点的赋值操作。下面会进一步解释
递归函数的返回类型为节点类型TreeNode * 。
代码如下
TreeNode* insertIntoBST(TreeNode* root, int val) 确定终止条件
终止条件就是找到遍历的节点为null的时候就是要插入节点的位置了并把插入的节点返回。
代码如下
if (root NULL) {TreeNode* node new TreeNode(val);return node;
}
这里把添加的节点返回给上一层就完成了父子节点的赋值操作了详细再往下看。 确定单层递归的逻辑
此时要明确需要遍历整棵树么
别忘了这是搜索树遍历整棵搜索树简直是对搜索树的侮辱。
搜索树是有方向了可以根据插入元素的数值决定递归方向。
代码如下
if (root-val val) root-left insertIntoBST(root-left, val);
if (root-val val) root-right insertIntoBST(root-right, val);
return root;
到这里大家应该能感受到如何通过递归函数返回值完成了新加入节点的父子关系赋值操作了下一层将加入节点返回本层用root-left或者root-right将其接住。
整体代码如下
class Solution {
public:TreeNode* insertIntoBST(TreeNode* root, int val) {if (root NULL) {TreeNode* node new TreeNode(val);return node;}if (root-val val) root-left insertIntoBST(root-left, val);if (root-val val) root-right insertIntoBST(root-right, val);return root;}
};
可以看出代码并不复杂。
刚刚说了递归函数不用返回值也可以找到插入的节点位置直接让其父节点指向插入节点结束递归也是可以的。
那么递归函数定义如下
TreeNode* parent; // 记录遍历节点的父节点
void traversal(TreeNode* cur, int val)
没有返回值需要记录上一个节点parent遇到空节点了就让parent左孩子或者右孩子指向新插入的节点。然后结束递归。
代码如下
class Solution {
private:TreeNode* parent;void traversal(TreeNode* cur, int val) {if (cur NULL) {TreeNode* node new TreeNode(val);if (val parent-val) parent-right node;else parent-left node;return;}parent cur;if (cur-val val) traversal(cur-left, val);if (cur-val val) traversal(cur-right, val);return;}
public:TreeNode* insertIntoBST(TreeNode* root, int val) {parent new TreeNode(0);if (root NULL) {root new TreeNode(val);}traversal(root, val);return root;}
};
可以看出还是麻烦一些的。
我之所以举这个例子是想说明通过递归函数的返回值完成父子节点的赋值是可以带来便利的。
网上千篇一律的代码可能会误导大家认为通过递归函数返回节点 这样的写法是天经地义其实这里是有优化的
#迭代
跳过
450.删除二叉搜索树中的节点
力扣题目链接
给定一个二叉搜索树的根节点 root 和一个值 key删除二叉搜索树中的 key 对应的节点并保证二叉搜索树的性质不变。返回二叉搜索树有可能被更新的根节点的引用。
一般来说删除节点可分为两个步骤
首先找到需要删除的节点 如果找到了删除它。 说明 要求算法时间复杂度为 $O(h)$h 为树的高度。
示例: 思路
搜索树的节点删除要比节点增加复杂的多有很多情况需要考虑做好心理准备。
#递归
递归三部曲 确定递归函数参数以及返回值
说到递归函数的返回值在二叉树搜索树中的插入操作 (opens new window)中通过递归返回值来加入新节点 这里也可以通过递归返回值删除节点。
代码如下
TreeNode* deleteNode(TreeNode* root, int key)
1 确定终止条件
遇到空返回其实这也说明没找到删除的节点遍历到空节点直接返回了
if (root nullptr) return root;
1 确定单层递归的逻辑
这里就把二叉搜索树中删除节点遇到的情况都搞清楚。
有以下五种情况 第一种情况没找到删除的节点遍历到空节点直接返回了 找到删除的节点 第二种情况左右孩子都为空叶子节点直接删除节点 返回NULL为根节点 第三种情况删除节点的左孩子为空右孩子不为空删除节点右孩子补位返回右孩子为根节点 第四种情况删除节点的右孩子为空左孩子不为空删除节点左孩子补位返回左孩子为根节点 第五种情况左右孩子节点都不为空则将删除节点的左子树头结点左孩子放到删除节点的右子树的最左面节点的左孩子上返回删除节点右孩子为新的根节点。
第五种情况有点难以理解看下面动画 动画中的二叉搜索树中删除元素7 那么删除节点元素7的左孩子就是5删除节点元素7的右子树的最左面节点是元素8。
将删除节点元素7的左孩子放到删除节点元素7的右子树的最左面节点元素8的左孩子上就是把5为根节点的子树移到了8的左孩子的位置。
要删除的节点元素7的右孩子元素9为新的根节点。.
这样就完成删除元素7的逻辑最好动手画一个图尝试删除一个节点试试。
代码如下
if (root-val key) {// 第二种情况左右孩子都为空叶子节点直接删除节点 返回NULL为根节点// 第三种情况其左孩子为空右孩子不为空删除节点右孩子补位 返回右孩子为根节点if (root-left nullptr) return root-right;// 第四种情况其右孩子为空左孩子不为空删除节点左孩子补位返回左孩子为根节点else if (root-right nullptr) return root-left;// 第五种情况左右孩子节点都不为空则将删除节点的左子树放到删除节点的右子树的最左面节点的左孩子的位置// 并返回删除节点右孩子为新的根节点。else {TreeNode* cur root-right; // 找右子树最左面的节点while(cur-left ! nullptr) {cur cur-left;}cur-left root-left; // 把要删除的节点root左子树放在cur的左孩子的位置TreeNode* tmp root; // 把root节点保存一下下面来删除root root-right; // 返回旧root的右孩子作为新rootdelete tmp; // 释放节点内存这里不写也可以但C最好手动释放一下吧return root;}
}
这里相当于把新的节点返回给上一层上一层就要用 root-left 或者 root-right接住代码如下
if (root-val key) root-left deleteNode(root-left, key);
if (root-val key) root-right deleteNode(root-right, key);
return root;
整体代码如下注释中情况12345和上面分析严格对应
class Solution {
public:TreeNode* deleteNode(TreeNode* root, int key) {if (root nullptr) return root; // 第一种情况没找到删除的节点遍历到空节点直接返回了if (root-val key) {// 第二种情况左右孩子都为空叶子节点直接删除节点 返回NULL为根节点if (root-left nullptr root-right nullptr) {///! 内存释放delete root;return nullptr;}// 第三种情况其左孩子为空右孩子不为空删除节点右孩子补位 返回右孩子为根节点else if (root-left nullptr) {auto retNode root-right;///! 内存释放delete root;return retNode;}// 第四种情况其右孩子为空左孩子不为空删除节点左孩子补位返回左孩子为根节点else if (root-right nullptr) {auto retNode root-left;///! 内存释放delete root;return retNode;}// 第五种情况左右孩子节点都不为空则将删除节点的左子树放到删除节点的右子树的最左面节点的左孩子的位置// 并返回删除节点右孩子为新的根节点。else {TreeNode* cur root-right; // 找右子树最左面的节点while(cur-left ! nullptr) {cur cur-left;}cur-left root-left; // 把要删除的节点root左子树放在cur的左孩子的位置TreeNode* tmp root; // 把root节点保存一下下面来删除root root-right; // 返回旧root的右孩子作为新rootdelete tmp; // 释放节点内存这里不写也可以但C最好手动释放一下吧return root;}}if (root-val key) root-left deleteNode(root-left, key);if (root-val key) root-right deleteNode(root-right, key);return root;}
};
普通二叉树的删除方式
这里我在介绍一种通用的删除普通二叉树的删除方式没有使用搜索树的特性遍历整棵树用交换值的操作来删除目标节点。
代码中目标节点要删除的节点被操作了两次 第一次是和目标节点的右子树最左面节点交换。 第二次直接被NULL覆盖了。
思路有点绕感兴趣的同学可以画图自己理解一下。
代码如下关键部分已经注释
class Solution {
public:TreeNode* deleteNode(TreeNode* root, int key) {if (root nullptr) return root;if (root-val key) {if (root-right nullptr) { // 这里第二次操作目标值最终删除的作用return root-left;}TreeNode *cur root-right;while (cur-left) {cur cur-left;}swap(root-val, cur-val); // 这里第一次操作目标值交换目标值其右子树最左面节点。}root-left deleteNode(root-left, key);root-right deleteNode(root-right, key);return root;}
};
这个代码是简短一些思路也巧妙但是不太好想实操性不强推荐第一种写法