最简单的思路为双递归,内部递归函数用于计算以 node 节点为头元素的路径和, 外部递归函数用于遍历所有节点。
即遍历以每个节点为头元素的所有符合条件的路径:
/** * @Author Niuxy * @Date 2020/7/15 11:04 下午 * @Description 双递归 */ public int pathSum(TreeNode root, int sum) { if (root == null) { return 0; } return pathSum0(root, sum, 0) + pathSum(root.right, sum) + pathSum(root.left, sum); } public int pathSum0(TreeNode node, int sum, int preSum) { if (node == null) { return 0; } int currentSum = preSum + node.val; int re = pathSum0(node.left, sum, currentSum) + pathSum0(node.right, sum, currentSum); if (sum == currentSum) { re++; } return re; }
计算过程存在大量重复求和计算。比如 [1,2,3,4,5] ,1-5 的路径和为 2-5 的路径和加 1 ,而在计算过程中,2-5 的路径和被计算了两次。
内部函数建立缓存只能由 node,currentSum 两个坐标为依据,但这两个坐标命中的结果,整个计算过程只被使用过一次。也就是说内部函数的定义无法帮我们找出上述的重复计算情况。
参考数组求区间和时,可以使用双指针复用局部的区间和。
也可以在遍历一条路径时,通过区间和的差来复用该路径上的区间和。
从头元素到节点 i 的区间和为 I ,到节点 j 的区间和为 J 。则节点 i 到 j 的区间和为 J-I 。其中 I 与 J 即为节点 I 与 节点 J 的前缀和。
在遍历一条路径时,记录下每一个节点的前缀和,通过前缀和的差值寻找区间和为某值的路径是否存在。
因为要寻找路径的总和是确定的 sum ,某节点的前缀和 currentSum 是确定的。寻找的区间为 currentSum-x = sum , 也就是遍历每个节点时,寻找其前面前缀和为 x = currentSum-sum
的节点的数量。因为 x 是确定的,在一条路径上嗅探时,可以用哈希表存储前面节点的前缀和,以 O(1) 时间复杂度寻找前缀和为 x 的节点。
语义为遍历每个节点时,寻找该路径上,所有以该节点为尾结点的符合题意的区间。
目标区间的尾结点必为该路径上某一节点,该方式可完整遍历解空间,并帮助我们避免重复的求和计算。
/** * @Author Niuxy * @Date 2020/7/15 11:52 下午 * @Description 前缀和解法 */ public final int pathSum1(TreeNode root, int sum) { return pathSum1(root, sum, 0, new HashMap<Integer, Integer>()); } public final int pathSum1(TreeNode root, int sum, int preSum, Map<Integer, Integer> cache) { if (root == null) { return 0; } int currentRe = 0; int currentSum = preSum + root.val; if (currentSum == sum) { currentRe++; } //以该节点为末尾节点,向上寻找满足条件的路径数: currentRe - x = sum -> x = currentSum - sum currentRe += cache.getOrDefault(currentSum - sum, 0); //本节点前缀和 cache.put(currentSum, cache.getOrDefault(currentSum, 0) + 1); currentRe += pathSum1(root.left, sum, currentSum, cache) + pathSum1(root.right, sum, currentSum, cache); cache.put(currentSum, cache.get(currentSum) - 1); return currentRe; }
与数组双指针遍历区间和的思路类似,前缀和也是避免重复计算的一种思路。
同时,在“以某个节点为头节点”进行遍历时无法避免的重复计算,转变为“以某个元素为尾结点”进行遍历便解决了。
因为“以某个节点为头节点”时,已计算的结果不足以支撑计算出当前结果。“以某个元素为尾结点”则是在根节点与尾结点间寻找子路径,以计算出的前缀和结果足以支撑计算出当前结果,以此可以避免重复计算。
在很多情况下,逆向思维是破局的不二法门。但局内逻辑繁多,确定可以在哪个逻辑上尝试逆向,需要大量的练习。