参数传递和剪枝,从修剪二叉树谈起

669. 修剪二叉搜索树 - 力扣(LeetCode)


一、参数传递

Java中的参数传递方式只有一种,那就是值传递。如果我们传的是基本数据类型,那么函数接收到的就是该数据的副本,如果我们传的是对象,那么函数接收到的就是该对象引用的副本。

对于值传递,由于函数拿到的是该值的副本,显然,对于此副本的任何修改不会对原值造成影响。

这里的关键之处在于:修改引用不会对原引用造成影响(因为引用是副本),修改引用指向的对象会对原引用指向的对象造成影响(因为函数中操作的对象不是原对象的副本)。

!!!想要对原引用造成影响,需要做的,是原引用接受函数改变后的副本的值。


二、例子(我的错误解)

在这个例子中,我犯的错误就是:混淆了引用和对象,认为函数中处理的TreeNode root是对象,其实是原对象引用的副本。

    public TreeNode trimBST1(TreeNode root, int low, int high) {
        trim(root, low, high);
        return root;
    }

    public void trim(TreeNode root, int low, int high) {
        if (root == null) return;

        if (root.val >= low && root.val <= high) {
            trim(root.left, low, high);
            trim(root.right, low, high);
        } else {
            if (root.left == null) {
                root = root.right;
                trim(root, low, high);
                return;
            }
            if (root.right == null) {
                root = root.left;
                trim(root, low, high);
                return;
            }
            TreeNode rightMinNode = root.right;
            while (rightMinNode.left != null) {
                rightMinNode = rightMinNode.left;
            }
            rightMinNode.left = root.left;
            root = root.right;
            trim(root, low, high);
        }
    }

让我截取其中的一段来说明这个问题:

            if (root.left == null) {
                root = root.right;
                return trimBST2(root, low, high);
            }

当root需要被修剪而它的左子树为null时,我的想法就是用它的右子树来替代它,并对替换后的子树做进一步修剪。

这个地方我操作的root,是原树的root节点的引用的副本,所以我这样操作,在这个局部root确实指向了它的右孩子节点,但函数结束之后,我用原引用对树进行遍历,会发现“修剪”根本没有生效,这就是因为我的“修剪”操作——尝试改变引用的方式,并没有影响到实际的引用。

下面这个图也许更加详细地说明了我的误解:

之所以会引起这个误解,是因为我们在写代码时,会很自然地认为我们在操作对象。这个说法当然没错,但需要注意的是,我们是通过引用在操作对象。在做具体的操作时,要注意这个操作是生效在引用上的,还是原来的对象上的。


三、剪枝

我对剪枝的理解就是,在进行业务处理之前通过判断避免不必要的处理以提升程序的效率。

下面是我的第二份代码,它解决了上面提到的问题,即只对引用的副本进行修改。

但是在对左右子树都存在的情况进行处理时,我这里是不管左右子树的大小,都默认进行处理。对于一颗较为复杂的树,这样的操作会引起栈溢出。

事实上,如果root.val已经小于low了,那么它的左子树也必然小于low,这样只需要对它的右子树进行接下来的操作并返回即可了。同样的,如果root.val也已经大于high了,那么只需要对它的左子树进行操作并返回。

    public TreeNode trimBST2(TreeNode root, int low, int high) {
        if (root == null) return null;

        if (root.val >= low && root.val <= high) {
            root.left = trimBST2(root.left, low, high);
            root.right = trimBST2(root.right, low, high);
        } else {
            if (root.left == null) {
                root = root.right;
                return trimBST2(root, low, high);
            }
            if (root.right == null) {
                root = root.left;
                return trimBST2(root, low, high);
            }
            TreeNode rightMinNode = root.right;
            while (rightMinNode.left != null) {
                rightMinNode = rightMinNode.left;
            }
            rightMinNode.left = root.left;
            root = root.right;
            return trimBST2(root, low, high);
        }
        return root;
    }

下面是第三份代码,解决了上面提到的参数传递和剪枝的问题,并把单子树的情况和多子树的情况不加区分地融入到剪枝的方案中,实现了最优解。

    public TreeNode trimBST(TreeNode root, int low, int high) {
        if (root == null) return null;

        // 如果当前节点的值小于最小值,则说明该节点及其左子树都不符合要求,直接返回右子树
        // 注意此处返回的是对右子树修剪的结果,即此时的root是被“修剪”掉了
        if (root.val < low) {
            return trimBST(root.right, low, high);
        }
        // 同理,如果当前节点的值大于最大值,则说明该节点及其右子树都不符合要求,直接返回左子树
        // 通过直接返回对左子树修剪的结果,来实现“修剪”root的效果
        if (root.val > high) {
            return trimBST(root.left, low, high);
        }

        // 如果当前节点的值在[low, high]之间,则递归地对左右子树进行修剪
        root.left = trimBST(root.left, low, high);
        root.right = trimBST(root.right, low, high);

        return root;
    }

这里需要注意的一个细节就是,通过返回对右子树修剪的结果,并把这个结果替换掉原本指向根节点的引用,这个过程就已经抛弃了根节点,即完成了对根节点的修剪!

  • 11
    点赞
  • 14
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值