羊羊刷题笔记Day22/60 | 第六章 二叉树P8 | 235. 二叉搜索树的最近共同祖先、701. 二叉搜索树中的插入操作、450. 删除二叉搜素树中的节点

138 阅读6分钟

235 二叉搜索树的最近共同祖先

236 二叉树的最近公共祖先后的题,加了二叉搜索树的条件

思路

对比236 二叉树的最近公共祖先,是利用回溯从底向上搜索,大致思路就是遇到一个节点的左子树里有p,右子树里有q,那么当前节点就是最近公共祖先。
那么本题添加了条件:二叉搜索树。二叉搜索树是有序的,那得好好利用一下这个特点。
在有序树里,如果判断一个节点的左子树里有p,右子树里有q呢?
根据二叉搜索树的性质,假设一个节点已经是p与q的共同祖先,他会有什么特点。
如果该节点向右遍历,右节点将会大于原左子树所有值,同理向左遍历也是。因此,当q p在根节点两边时,此时根节点就是共同祖先
反过来讲,如果p q同时大于或小于根节点的话就要向下遍历。

如图,我们从根节点搜索,10都大于p q,因此向左遍历,即 节点5。此时q小于5而p又大于5,可以说明 p 和 q 一定分别存在于 节点 5的左子树,和右子树中。
image.png
此时,5就是最近共同祖先了~
理解这一点,本题就很好解了。
递归遍历顺序,本题没有中的处理逻辑(判断root与p q大小的目的是为了左右遍历),因此只需左右遍历即可。
如图所示:p为节点6,q为节点9

递归法

递归三部曲如下:

  • 确定递归函数返回值以及参数

参数就是当前节点,以及两个结点 p、q
返回值是要返回最近公共祖先,所以是TreeNode
代码如下:

public TreeNode lowestCommonAncestor(TreeNode root, TreeNode p, TreeNode q) {}
  • 确定终止条件

遇到空返回就可以了,代码如下:

if (cur == NULL) return cur;

其实都不需要这个终止条件,因为题目中说了p、q 为不同节点且均存在于给定的二叉搜索树中。也就是说一定会找到公共祖先的,所以并不存在遇到空的情况。

  • 确定单层递归的逻辑

在遍历二叉搜索树的时候就是寻找q p一大于一小于根节点情况
如果根节点(cur)都大于q p,那么就应该向左遍历(说明目标区间在左子树上),小于同理。
需要注意的是此时不知道p和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);

这种写法在236 二叉树的最近公共祖先中碰到过。如果递归函数有返回值,如何区分要搜索一条边,还是搜索整个树
搜索一条边的写法:

if (递归函数(root.left)) return ;
if (递归函数(root.right)) return ;

搜索整个树写法:

left = 递归函数(root.left);
right = 递归函数(root.right);
left与right的逻辑处理;

本题就是标准的搜索一条边的写法,遇到递归函数的返回值,如果不为空,立刻返回。

剩下的情况,就是根节点(cur)与p q一个大于一个小于,那么此时cur就是最近公共祖先了,直接返回cur。
代码如下:

return cur;

那么整体递归代码如下:

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;

}

迭代法

没有终止条件,迭代法很简单,使用while模拟递归,root标志节点即可。

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 。
使用原方法即可,代码如下:

public 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将其接住。
整体代码如下:

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;
}

450 删除二叉搜索树中的节点

递归

递归三部曲:

  • 确定递归函数参数以及返回值

说到递归函数的返回值,在上一题 701 二叉搜索树中的插入操作 中通过递归返回值来加入新节点, 这里也可以通过递归返回值删除节点。
代码如下:

public TreeNode deleteNode(TreeNode root, int key) {}
  • 确定终止条件

节点可能遇到空返回,其实这也说明没找到删除的节点,遍历到空节点直接返回了。节点也可能找到了节点,他的左右孩子了如下情况:

  • 第一种情况:没找到删除的节点,遍历到空节点直接返回了
  • 找到删除的节点 (注:x为不空 o为空)
    • 第二种情况**(o o)**:左右孩子都为空(叶子节点),直接删除节点, 返回NULL为根节点
    • 第三种情况**(o x)**:删除节点的左孩子为空,右孩子不为空,删除节点,右孩子补位,返回右孩子为根节点
    • 第四种情况**(x o)**:删除节点的右孩子为空,左孩子不为空,删除节点,左孩子补位,返回左孩子为根节点
    • 第五种情况**(x x)**:左右孩子节点都不为空,**把左孩子放到右孩子的最左边,返回删除节点右孩子为新的根节点。**如下图所示_(此处默认右孩子继承,左孩子继承也可以也是同理)_


代码如下:
// 终止条件
// 1. 空节点 - 没找到
if (root == null) return null;
// 找到了:
if (root.val == key) {
    // 2. (空 空)
    if (root.left == null && root.right == null) return null;
    // 3. (x 空)
    if (root.left != null && root.right == null) return root.left;
    // 4. (空 x)
    if (root.left == null && root.right != null) return root.right;
    // 5. (x x) - 换树逻辑
    else {
        // 默认是右子树继承
        TreeNode cur = root.right;
        // 原左子树应该嫁接在右子树的最左节点
        while (cur.left != null) cur = cur.left;
        cur.left = root.left;
        // 返回右节点
        return root.right;
    }
}
  • 确定单层递归的逻辑

这里寻找删除节点(root),然后把新的节点返回给上一层,上一层就要用 root.left 或者 root.right接住,代码如下:

// 单层递归逻辑 - 寻找root,返回值是下一个节点用left与right接收
if (key > root.val) root.right = deleteNode(root.right,key);
if (key < root.val) root.left = deleteNode(root.left,key);

return root;

拓展:普通二叉树删除节点 和 迭代法 可以看这里👈

总结

对比今天的插入与删除,二叉搜索树删除节点比增加节点复杂的多。
因为二叉搜索树添加节点只需要在叶子上添加就可以的,不涉及到结构的调整,而删除节点操作涉及到结构的调整。我们依然使用递归函数的返回值来完成把节点从二叉树中移除的操作。
这里最关键的逻辑就是第五种情况(左右都不为空),这种情况一定要想清楚。非常考验思维逻辑,以及代码能力

学习资料

235 二叉搜索树的最近共同祖先

701 二叉搜索树中的插入操作

450 删除二叉搜素树中的节点