第二十天|二叉搜索树的公共祖先,修改与构造| 235. 二叉搜索树的最近公共祖先, 701. 二叉搜索树中的插入操作,450. 删除二叉搜索树中的节点

关于二叉搜索树的题目,貌似普遍用迭代法比递归法简单。目前做到的除了98验证二叉搜索树都是如此。

701其实很简单,只是之前自己想不到直接添加到叶子节点这个方法。

注意一个问题:判断需要返回 root 还是 newRoot

  • 返回 root:当操作不改变树的根节点时,返回 root。例如,在插入或删除时,根节点没有被替换,树的结构依然连贯。
  • 返回 newRoot:当操作导致树的根节点发生变化时,返回 newRoot。这通常发生在根节点为空(即空树)或删除操作导致根节点被替换的情况下。

判断是否需要返回 root 还是 newRoot 的关键是看操作是否会改变树的根节点结构。如果根节点没有发生变化,通常返回 root 即可;否则,返回新的根节点。

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

因为刚做了236. 二叉树的最近公共祖先,一看到这个题目的时候思考方式还是从底向上遍历,又想利用二叉搜索树的性质,毫无头绪,没有想法。看了题解才觉得这道题的想法这么妙,就很简单。

总结

  • 对于二叉搜索树的最近祖先问题,其实要比普通二叉树的最近公共祖先问题简单的多。
  • 不用使用回溯,二叉搜索树自带方向性,可以方便的从上向下查找目标区间,遇到目标区间内的节点,直接返回。
  • 最后给出了对应的迭代法,二叉搜索树的迭代法甚至比递归更容易理解,也是因为其有序性(自带方向性),按照目标区间找就行了。

思路:

  • 不需要遍历整棵树,找到结果直接返回!
  • 因为二叉搜索树是有序树,所以 如果 中间节点是 q 和 p 的公共祖先,那么 中节点的数组 一定是在 [p, q]区间的。

注意:如何保证该节点就是最近公共祖先呢?

        从根节点搜索,第一次遇到 cur节点是数值在[q, p]区间中,即 节点5,此时可以说明 q 和 p 一定分别存在于 节点 5的左子树,和右子树中。

        此时节点5是不是最近公共祖先? 如果 从节点5继续向左遍历,那么将错过成为p的祖先, 如果从节点5继续向右遍历则错过成为q的祖先。

        所以当我们从上向下去递归遍历,第一次遇到 cur节点是数值在[q, p]区间中,那么cur就是 q和p的最近公共祖先。

递归法

递归遍历顺序,本题就不涉及到 前中后序了(这里没有中节点的处理逻辑,遍历顺序无所谓了)。

class Solution {
    public TreeNode lowestCommonAncestor(TreeNode root, TreeNode p, TreeNode q) {
        // 递归法
        // 如果 cur->val 大于 p->val,同时 cur->val 大于 q->val,那么就应该向左遍历(目标区间在左子树)。
        if (root.val > p.val && root.val > q.val)
            return lowestCommonAncestor(root.left,p,q);
        // 如果 cur->val 小于 p->val,同时 cur->val 小于 q->val,那么就应该向右遍历(目标区间在右子树)。
        if (root.val < p.val && root.val < q.val)
            return lowestCommonAncestor(root.right,p,q);
        // 剩下的情况,就是cur节点在区间[p,q]或者[q,p]
        // 那么root就是最近公共祖先了,直接返回root
        return root;
    }
}

235和236递归函数返回值的区别

如果递归函数有返回值,如何区分要搜索一条边(235),还是搜索整个树(236)。

搜索一条边的写法:

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

搜索整个树写法:

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

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

迭代法(简单)

利用其有序性,迭代的方式还是比较简单的,解题思路在递归中已经分析了。

    class Solution {
        public TreeNode lowestCommonAncestor(TreeNode root, TreeNode p, TreeNode q) {
            // 迭代法
            while (root != null) {
                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. 二叉搜索树中的插入操作

本题一开始看到没有思路。当不考虑题目中提示所说的改变树的结构的插入方式,直接插入到叶子节点就简单了。在二叉搜索树中的插入操作,其实根本不用重构搜索树。

只要按照二叉搜索树的规则去遍历,遇到空节点(末尾节点)就插入节点就可以了。

递归法(看这个)

搜索树是有方向的,可以根据插入元素的数值,决定递归方向。

遍历整棵搜索树简直是对搜索树的侮辱。

不需遍历整棵树!

   class Solution {
        public TreeNode insertIntoBST(TreeNode root, int val) {
            // 递归法
            // 如果当前节点为空,也就意味着val找到了合适的位置,此时创建节点直接返回。
            if (root == null)
                return new TreeNode(val);
            if (root.val < val) {
                root.right = insertIntoBST(root.right, val);// 递归创建右子树
            } else if (root.val > val) {
                root.left = insertIntoBST(root.left, val);// 递归创建左子树
            }
            return root;
        }
    }

迭代法(供参考)

在迭代法遍历的过程中,需要记录一下当前遍历的节点的父节点,这样才能做插入节点的操作。

用记录pre和cur两个指针的技巧。

  • 代码的整体思路是使用一个指针root遍历树,寻找合适的插入位置。遍历过程中通过pre记录父节点,最终在树的叶节点处插入新的值。
  • 最后返回的是最初保存的根节点newRoot,因为二叉搜索树的插入操作不会改变根节点的位置。
    class Solution {
        public TreeNode insertIntoBST(TreeNode root, int val) {
            // 迭代法
            if (root == null) return new TreeNode(val);
            TreeNode newRoot = root; // 保存初始根节点
            TreeNode pre = root; // pre用于保存root的前一个节点,即插入过程中最后一次遍历到的父节点。因为当root遍历到null时,需要通过pre来插入新节点到合适的位置。
            while (root != null) {
                pre = root; // 每次移动时,将pre更新为当前节点
                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. 删除二叉搜索树中的节点

递归法1(看这个)

二叉搜索树中删除节点有以下五种情况:

  • 第一种情况:没找到删除的节点,遍历到空节点直接返回了
  • 找到删除的节点
    • 第二种情况:左右孩子都为空(叶子节点),直接删除节点, 返回NULL为根节点
    • 第三种情况:删除节点的左孩子为空,右孩子不为空,删除节点,右孩子补位,返回右孩子为根节点
    • 第四种情况:删除节点的右孩子为空,左孩子不为空,删除节点,左孩子补位,返回左孩子为根节点
    • 第五种情况:左右孩子节点都不为空,则将删除节点的左子树头结点(左孩子)放到删除节点的右子树的最左面节点的左孩子上,返回删除节点右孩子为新的根节点(此时相当于左空右不空,情况三)

这里用父节点直接接收返回值。很妙,不需要单独定义一个父节点。(把新的节点返回给上一层,上一层就要用 root->left 或者 root->right接住)具体见下面代码。

if (root->val > key) root->left = deleteNode(root->left, key);
if (root->val < key) root->right = deleteNode(root->right, key);

整体代码如下:

    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的左子树放到cur的左子树下
                    // 此时要删除的节点相当于左空右不空,第三种情况
                    return root.right;
                }
            }
            if (root.val > key) root.left = deleteNode(root.left, key);
            if (root.val < key) root.right = deleteNode(root.right, key);
            return root;
        }
    }

普通二叉树的删除方式

普通二叉树的删除方式(没有使用搜索树的特性,遍历整棵树),用交换值的操作来删除目标节点。

代码中目标节点(要删除的节点)被操作了两次:

  • 第一次是和目标节点的右子树最左面节点交换。
  • 第二次直接被NULL覆盖了。

(我没理解,如果是普通二叉树,为什么要用右子树最左面节点交换,普通二叉树没有排序呀)

递归法2:

对每个节点进行递归处理,寻找需要删除的节点,然后根据节点的不同情况(无子节点、一个子节点、两个子节点)执行相应的删除操作。

   class Solution {
        public TreeNode deleteNode(TreeNode root, int key) {
            root = delete(root, key);
            return root;
        }

        private TreeNode delete(TreeNode root, int key) {
            if (root == null) return null;  // 没有找到要删除的节点
            if (root.val > key) {
                root.left = delete(root.left, key);  // 递归左右子树
            } else if (root.val < key) {
                root.right = delete(root.right, key);  // 递归左右子树
            } else {
                if (root.left == null) return root.right;
                if (root.right == null) return root.left;
                // 处理有两个子节点的情况
                // 用右子树中最小的节点(即右子树的最左边的节点)替换当前节点的值。
                TreeNode tmp = root.right;
                while (tmp.left != null)
                    tmp = tmp.left;
                root.val = tmp.val;
                root.right = delete(root.right, tmp.val); // 再递归地删除右子树中的这个最小节点
            }
            return root;
        }
    }

迭代法

迭代法,就是模拟递归法中的逻辑来删除节点,但需要一个pre记录cur的父节点,方便做删除操作。(迭代法较难)麻烦

    class Solution {
        public TreeNode deleteNode(TreeNode root, int key) {
            // 迭代法,需要pre记录父节点,麻烦
            if (root == null) return null;
            //寻找对应的对应的前面的节点,以及他的前一个节点
            //查找待删除节点
            TreeNode cur = root;
            TreeNode pre = null;
            while (cur != null) {
                if (cur.val < key) {
                    pre = cur;
                    cur = cur.right;
                } else if (cur.val > key) {
                    pre = cur;
                    cur = cur.left;
                } else {
                    break;
                }
            }
            // 特殊情况处理:根节点就是目标节点
            if (pre == null)
                return deleteOneNode(cur);
            // 处理目标节点是父节点的左子节点或右子节点
            if (pre.left != null && pre.left.val == key)
                pre.left = deleteOneNode(cur); // 删除节点,并更新父节点的 left
            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;  // 一旦找到这个最小节点,把node的左子树连接到 这个右子树最小的左子树节点上
            return node.right; // 返回右子树作为删除节点后的新的根节点。
        }
    }

第二十天的总算是结束了,直冲Day21!

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值