problem
Given a root node reference of a BST and a key, delete the node with
the given key in the BST. Return the root node reference (possibly
updated) of the BST.Basically, the deletion can be divided into two stages:
Search for a node to remove.
If the node is found, delete the node.
solution
这个问题如果分成两步的话,查找节点没什么难度,删除的话就是把目标节点的两棵子树合到一起形成一个BST,问题就是如果该节点的左右子树都是空树该如何处理,在Python中我们无法手动销毁一个对象,所以无法让该节点的父节点指向None。
所以这里我们应该把两步合到一起,做一个整体的递归程序,
public TreeNode deleteNode(TreeNode root, int key) {
//基础问题
if(root == null){
return null;
}
//利用子问题的返回值构成原问题的解
if(key < root.val){
root.left = deleteNode(root.left, key);#关键点
}else if(key > root.val){
root.right = deleteNode(root.right, key);
}else{
if(root.left == null){
return root.right;
}else if(root.right == null){
return root.left;
}
TreeNode minNode = findMin(root.right);
root.val = minNode.val;
root.right = deleteNode(root.right, root.val);
}
//返回正确的解
return root;
}
private TreeNode findMin(TreeNode node){
while(node.left != null){
node = node.left;
}
return node;
}
分析递归程序最关键的就是找出所有的return语句、调用递归程序点和使用递归程序赋值语句,这样我们就可以知道每个子程序之间是如何调用的,以及如何返回值、赋值。
例如在上面的这个程序中,通过这样几个return :
我们知道了返回值的意义为:对给定root和val,删除值为val的节点后的树的根节点,我们再动手写递归程序时也要牢记这一点,这对赋值以及递归调用都很有用。
//如果传入的树是空树,返回
if(root == null){
return null;
}
//如果root.val=val,且左右子树有空树的情况
if(root.left == null){
return root.right;
}else if(root.right == null){
return root.left;
}
//root.val = val且左右子树非空
TreeNode minNode = findMin(root.right);
root.val = minNode.val;
root.right = deleteNode(root.right, root.val);
//如果root.val != key
return root;
调用及赋值语句:
if(key < root.val){
root.left = deleteNode(root.left, key);//关键点
}else if(key > root.val){
root.right = deleteNode(root.right, key);
这个代码块中重要的就是这个赋值语句,其实在有些递归中不必进行赋值,如那些在递归中直接在内存上修改的程序,但是这个程序不同,因为它需要处理我们最初提到的那个问题:如果该节点的左右子树都是空树该如何处理。即我们还需要修改父节点的指向。
因此对所有的路过的节点都进行修改,对那些root.val != key的节点直接返回root,而对需要修改的则按对应情况处理子树、返回相应的根节点。
总结
处理这种需要修改之前节点的递归中重要的就是每次都要赋值,如果不修改则返回原来的,否则返回相应的。
例如在链表中删除所有与给定的值相等的节点,当然我们可以用while循环,但是使用递归代码更加简洁。
def findIneqVal(head, val):
if not head:
return None
if head.val != val:
return head
else:
return findIneqVal(head.next, val)
class Solution(object):
def removeElements(self, head, val):
if not head:
return None
if head.val != val:
head.next = self.removeElements(head.next, val)
else:
notEq = findIneqVal(head, val)
if not notEq:
return None
else:
head.val = notEq.val
head.next = self.removeElements(notEq.next, val)
return head
在discussion里面还看到一个更加简单的解法:
class Solution(object):
def removeElements(self, head, val):
if not head:
return None
head.next = self.removeElements(head.next, val)
return head if head.val != val else head.next
分析递归问题还有一个方法就是从后往前分析,例如[6, 1, 2, 6, 6, 3, 6], val = 6,上面的例子中包括开头等于val, 结尾等于val,两个val连着的情况。
结尾的None 调用fun,返回值为None
最后的6返回值为6.next
3的返回值为3
倒数第二个6的返回值为3
总结
分析递归程序的方法,
先判断这个函数的广义的功能和返回值,例如删除二叉树的节点的功能为删除指定数值的节点然后返回它的根,删除链表中等于某个值的所有节点,它的功能是“删除链表中等于某个值的所有节点,然后返回它的头结点”
明确了这功能和返回值后我们就可以在函数体内部对数据结构进行操作,并使用这些返回值,例如在最后的程序中令head.next = self.removeElements(head.next, val)
递归程序三要素:
对基础问题能正确返回,例如if not head: return None
对大问题转化为小问题的解+处理,例如head.next = self.removeElements(head.next, val)
正确返回大问题的解