1. 算法特点
望其名而知意,这种算法特点是递归函数的返回值不是真正的答案,或者说答案不能作为递归函数的返回值,有点绕,下面看几个例题。
2. 实例讲解
-
二叉树的直径
对于二叉树的问题,一般采用递归的方法,下面几题也是一样。题目要求的是二叉树的直径,那么递归函数直接返回直径就行了?看了本文的题目醉翁之意不在酒,就知道了,肯定没这么简单。
那么我们来看一下为什么不能直接返回题目要求的直径。二叉树的递归基本就是分而治之,将大问题化成子问题,那么我们来看看子问题的解和原问题的解是否具有相关性,即能够在分了之后还能治。
请看上面给的例子,对于根节点1,经过它的直径跟它的左子树、右子树的直经有半毛钱关系吗?并没有,经过该结点的直径反倒跟左子树、右子树的深度有关系,树的直径=左子树深度+1+右子树深度+1。这也就是说原问题和子问题不具有相关性,即原问题的解不能由子问题表示。但是我们还是想递归,那么我们换一种方式,让原问题变为求树的深度,树的深度=max(左子树深度,右子树深度)+1,这时根的深度与子树的深度明显是相关的,满足了递归的要求。
然后对于一个结点,我们递归求其左子树深度和右子树深度,即可得到经过该节点的直径。class Solution { int diam = 0; public int recur(TreeNode root){//返回从跟到叶子的最大路径长度 if(root != null){ //遍历每一个结点,将其当成根,则经过它的路径就是左子树最大路径加右子树最大路径加2 //注意这里是经过root结点的直径,不是从该节点到根的路径 int left = recur(root.left); int right = recur(root.right); int path = left + right + 2; diam = Math.max(diam, path); return Math.max(left, right) + 1; }else return -1; } public int diameterOfBinaryTree(TreeNode root) { recur(root); return diam; } }
-
二叉树的坡度
这道题其实说的很直接了,结点的坡度=abs(左子树结点之和-右子树结点之和),也就是结点的坡度跟左右子树的坡度并没有关系。但是我们还是想用递归,那就改变一下思路,返回一棵树的结点之和,此时树的结点之和=左子树的结点之和+右子树的结点之和+1,可以看到原问题的解和子问题的解具有了相关性。class Solution { int tilt = 0; public int sumNode(TreeNode root){ if(root == null) return 0; int left = sumNode(root.left); int right = sumNode(root.right); tilt += Math.abs(left - right); return left + right + root.val; } public int findTilt(TreeNode root) { sumNode(root); return tilt; } }
-
最长同值路径
以下这些等式默认为孩子结点的值与父节点的值相同。根结点的最长同值路径=左子树最长同值路径+右子树最长同值路径+2?显然不等,而且并没有什么关系。根节点的最长同值路径=左子树的最长同值深度+右子树的最长同值深度+2,此处指的是当左子树根节点值和右子树的根结点值与父节点值相同时。同理当我们想要递归时,就要使原问题的解和子问题的解有关系,可以将问题的返回值改为 树的最大同值深度,这时有树的最大同值深度= max(左子树最大同值深度,右子树最大同值深度)+1,当然前提是根结点的值与孩子结点的值相同。/** * Definition for a binary tree node. * public class TreeNode { * int val; * TreeNode left; * TreeNode right; * TreeNode(int x) { val = x; } * } */ class Solution { public int path(TreeNode root){ if(root != null){ int l = 0, r = 0; if(root.left != null && root.left.val == root.val){ l = path(root.left) + 1; } if(root.right != null && root.right.val == root.val){ r = path(root.right) + 1; } return Math.max(l, r); } return -1; } public int longestUnivaluePath(TreeNode root) { int max = 0; Stack<TreeNode> stack = new Stack<>(); TreeNode p = root; while(p != null || !stack.empty()){ while(p != null){ stack.push(p); p = p.left; } p = stack.pop(); int len = 0; if(p.left != null && p.left.val == p.val){ len += path(p.left) + 1; } if(p.right != null && p.right.val == p.val){ len += path(p.right) + 1; } max = Math.max(max, len); p = p.right; } return max; } }
-
二叉树中的最大路径和
二叉树肯定是用递归最好。但是我们要考虑一下子问题是什么然后再递归。如果子问题选的不好,没有递归性就完犊子了。思路: 我们可以计算以某个结点p为根的最大路径和,那么所有的结点遍历一遍就可以了。以p为根的最大路径和 = p的值 + p左子树的最大路径(如果大于0的话) + p右子树的最大路径(如果大于0的话).
定义: 某棵以root为根的树的最大路径为以该树的根root为起点,以树中某个结点为终点的所有路径中节点和的最大值,如果root为空,则其值为0。显然最大路径是一个具有递归性的子问题。
maxPath(root) = root.val + max{maxPath(root.left), maxPath(root.right), 0}
,也就是说,root为根的最大路径等于root的值,加上左子树的最大路径与右子树的最大路径与0三者之间的最大值(当子树的最大路径都是负值时,显然不应该将他们计算在内)。虽然题目问的问题不具有递归性,但是计算他需要的中间结果具有递归性(子问题),所以这又是一道典型的醉翁之意不在酒类型题。实现:
有了思路,实现起来就简单多了。class Solution { private int max = Integer.MIN_VALUE; //返回以结点root为起点的子路径的最大值 public int maxNode(TreeNode root) { if (root == null) return 0; int l = maxNode(root.left); int r = maxNode(root.right); int root_max = root.val + Math.max(0,Math.max(l, r)); int sum = root.val + Math.max(0, l) + Math.max(0, r); max = Math.max(max, sum); return root_max; } public int maxPathSum(TreeNode root) { maxNode(root); return max; } }
3. 总结
在面对这种需要递归的或者用递归比较方便的问题时,一定不要上来就盲目递归,分治。而是要分析原问题和子问题的关系是否满足递归条件,不满足的,需要改变问题的关注点。以上几个例子都是题中要求的结果不满足分治条件,需要改变问题的关注点(返回值),此所谓醉翁之意不在酒也。