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