今日任务
力扣 236. 二叉树的最近公共祖先,235. 二叉搜索树的最近公共祖先,701. 二叉搜索树中的插入操作, 450. 删除二叉搜索树中的节点
题目 :236. 二叉树的最近公共祖先
思路
情况1:
如果找到一个节点,发现左子树出现结点p,右子树出现节点q,或者 左子树出现结点q,右子树出现节点p,那么该节点就是节点p和q的最近公共祖先。
情况2:
二叉树节点数值是不重复的,而且一定存在 q 和 p 。但是很多人容易忽略一个情况,就是节点本身p(q),它拥有一个子孙节点q(p)。
其实情况一 和 情况二 代码实现过程都是一样的,也可以说,实现情况一的逻辑,顺便包含了情况二。
因为遇到 q 或者 p 就返回,这样也包含了 q 或者 p 本身就是 公共祖先的情况。
递归三部曲:
- 确定递归函数返回值以及参数
需要递归函数返回值,来告诉我们是否找到节点q或者p,那么返回值为bool类型就可以了。
但我们还要返回最近公共节点,可以利用上题目中返回值是TreeNode * ,那么如果遇到p或者q,就把q或者p返回,返回值不为空,就说明找到了q或者p。
- 确定终止条件
遇到空的话,因为树都是空了,所以返回空。
那么我们来说一说,如果 root == q,或者 root == p,说明找到 q p ,则将其返回,这个返回值,后面在中节点的处理过程中会用到,那么中节点的处理逻辑,下面讲解。
- 确定单层递归逻辑
值得注意的是 本题函数有返回值,是因为回溯的过程需要递归函数的返回值做判断,但本题我们依然要遍历树的所有节点。
拓展
如果递归函数有返回值,如何区分要搜索一条边,还是搜索整个树呢?
搜索一条边的写法:
if (递归函数(root->left)) return ;
if (递归函数(root->right)) return ;
搜索整个树写法:
left = 递归函数(root->left); // 左
right = 递归函数(root->right); // 右
left与right的逻辑处理; // 中
看出区别了没?
在递归函数有返回值的情况下:如果要搜索一条边,递归函数返回值不为空的时候,立刻返回,如果搜索整个树,直接用一个变量left、right接住返回值,这个left、right后序还有逻辑处理的需要,也就是后序遍历中处理中间节点的逻辑(也是回溯)。
关于回溯
那么我给大家归纳如下三点:
-
求最小公共祖先,需要从底向上遍历,那么二叉树,只能通过后序遍历(即:回溯)实现从底向上的遍历方式。
-
在回溯的过程中,必然要遍历整棵二叉树,即使已经找到结果了,依然要把其他节点遍历完,因为要使用递归函数的返回值(也就是代码中的left和right)做逻辑判断。
-
要理解如果返回值left为空,right不为空为什么要返回right,为什么可以用返回right传给上一层结果。
题解
java
class Solution {
public TreeNode lowestCommonAncestor(TreeNode root, TreeNode p, TreeNode q) {
if (root == null || root == p || root == q) return root;
TreeNode left = lowestCommonAncestor(root.left, p, q);
TreeNode right = lowestCommonAncestor(root.right, p, q);
if (left == null && right == null)
{
return null;
} else if (left == null && right != null)
{
return right;
} else if (left != null && right == null)
{
return left;
} else
{
return root;
}
}
}
题目 :235. 二叉搜索树的最近公共祖先
思路
本题是二叉搜索树,二叉搜索树是有序的,要好好利用这一点(区间思想),卡哥的代码太妙了!
只要从上到下去遍历,遇到 cur节点是数值在[p, q]区间中则一定可以说明该节点cur就是p 和 q的公共祖先。 那问题来了,一定是最近公共祖先吗?
第一次遇到 cur节点是数值在[q, p]区间中,那么cur就是 q和p的最近公共祖先。
因为,第一次遇到 cur节点是数值在[q, p]区间中,即 节点5,此时可以说明 q 和 p 一定分别存在于 节点 5的左子树,和右子树中。
此时节点5是不是最近公共祖先? 如果 从节点5继续向左遍历,那么将错过成为p的祖先, 如果从节点5继续向右遍历则错过成为q的祖先。
递归三部曲如下:
- 确定递归函数返回值以及参数
参数就是当前节点,以及两个结点 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谁大,所以两个都要判断
题解
java
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);
if (root.val < p.val && root.val < q.val) return lowestCommonAncestor(root.right, p, q);
return root;
}
}
java
class Solution {
public TreeNode lowestCommonAncestor(TreeNode root, TreeNode p, TreeNode q) {
while (true)
{
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
{
break;
}
}
return root;
}
}
题目 :701. 二叉搜索树中的插入操作
思路
递归三部曲:
- 确定递归函数参数以及返回值
参数就是根节点指针,以及要插入元素,这里递归函数要不要有返回值呢?
可以有,也可以没有,但递归函数如果没有返回值的话,实现是比较麻烦的。
有返回值的话,可以利用返回值完成新加入的节点与其父节点的赋值操作。
递归函数的返回类型为节点类型TreeNode * 。
- 确定终止条件
终止条件就是找到遍历的节点为null的时候,就是要插入节点的位置了,并把插入的节点返回。
这里把添加的节点返回给上一层,就完成了父子节点的赋值操作了,详细再往下看。
- 确定单层递归的逻辑
此时要明确,需要遍历整棵树么?
别忘了这是搜索树,遍历整棵搜索树简直是对搜索树的侮辱。
搜索树是有方向了,可以根据插入元素的数值,决定递归方向。
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将其接住。
题解
递归
java
class Solution {
public TreeNode insertIntoBST(TreeNode root, int val) {
if (root == null) return new TreeNode(val);
if (root.val > val)
{
root.left = insertIntoBST(root.left, val);
} else if (root.val < val) {
root.right = insertIntoBST(root.right, val);
}
return root;
}
}
迭代
java
class Solution {
public TreeNode insertIntoBST(TreeNode root, int val) {
if (root == null) return new TreeNode(val);
TreeNode pre = root;
TreeNode newRoot = root;
while (root != null)
{
pre = root;
if (root.val > val)
{
root = root.left;
} else if (root.val < val)
{
root = root.right;
}
}
if (pre.val > val)
{
pre.left = new TreeNode(val);
} else
{
pre.right = new TreeNode(val);
}
return newRoot;
}
}
题目 :450. 删除二叉搜索树中的节点
思路
删除的话,特别需要注意删除的节点有左右子孩子
递归三部曲:
- 确定递归函数参数以及返回值
说到递归函数的返回值,这里也可以通过递归返回值删除节点。
- 确定终止条件
遇到空返回,其实这也说明没找到删除的节点,遍历到空节点直接返回了
- 确定单层递归的逻辑
有以下五种情况:
- 第一种情况:没找到删除的节点,遍历到空节点直接返回了
- 找到删除的节点
- 第二种情况:左右孩子都为空(叶子节点),直接删除节点, 返回NULL为根节点
- 第三种情况:删除节点的左孩子为空,右孩子不为空,删除节点,右孩子补位,返回右孩子为根节点
- 第四种情况:删除节点的右孩子为空,左孩子不为空,删除节点,左孩子补位,返回左孩子为根节点
- 第五种情况:左右孩子节点都不为空,则将删除节点的左子树头结点(左孩子)放到删除节点的右子树的最左面节点的左孩子上,返回删除节点右孩子为新的根节点。
题解
递归
java
class Solution {
public TreeNode deleteNode(TreeNode root, int key) {
if (root == null) return root;
if (root.val == key)
{
if (root.left == null)
{
return root.right;
} else if (root.right == null)
{
return root.left;
} else
{
TreeNode cur = root.right;
while (cur.left != null) {
cur = cur.left;
}
cur.left = root.left;
root = root.right;
return root;
}
}
if (root.val > key) root.left = deleteNode(root.left, key);
if (root.val < key) root.right = deleteNode(root.right, key);
return root;
}
}
迭代
java
class Solution {
public TreeNode deleteNode(TreeNode root, int key) {
if (root == null)
{
return null;
}
TreeNode cur = root;
TreeNode pre = null;
while (cur != null)
{
if (cur.val > key)
{
pre = cur;
cur = cur.left;
} else if (cur.val < key)
{
pre = cur;
cur = cur.right;
}else
{
break;
}
}
if (pre == null)
{
return deleteOneNode(cur);
}
if (pre.left != null && pre.left.val == key)
{
pre.left = deleteOneNode(cur);
}
if (pre.right != null && pre.right.val == key)
{
pre.right = deleteOneNode(cur);
}
return root;
}
public TreeNode deleteOneNode(TreeNode node) {
if (node == null) return null;
if (node.right == null)
{
return node.left;
}
TreeNode cur = node.right;
while (cur.left != null)
{
cur = cur.left;
}
cur.left = node.left;
return node.right;
}
}