題目描述
給定一個二叉樹,它的每個結點都存放着一個整數值。
找出路徑和等於給定數值的路徑總數。
路徑不需要從根節點開始,也不需要在葉子節點結束,但是路徑方向必須是向下的(只能從父節點到子節點)。
二叉樹不超過1000個節點,且節點數值範圍是 [-1000000,1000000] 的整數。
思路1:DFS
本題需要去計算路徑和等於給定數值的路徑總數,遵循樹模型的解題思路,按照遞歸的方式去求解(遞歸的一個重要思想就是兩部分:1.找到最簡單的子問題求解,2.其他問題不考慮內在細節,只考慮整體邏輯),那我們現在來設計遞歸關係:
首先,最簡單的子問題是什麼呢?由於這道題是在樹的框架下,我們最容易想到的就是遍歷的終止條件:
if(root == null){
return 0;
}
接下來,我們來考慮再上升的一個層次,題目要求 路徑不需要從根節點開始,也不需要在葉子節點結束,但是路徑方向必須是向下的(只能從父節點到子節點) 。這就要求我們只需要去求三部分即可:
- 以當前節點作爲頭結點的路徑數量
- 以當前節點的左孩子作爲頭結點的路徑數量
- 以當前節點的右孩子作爲頭結點的路徑數量
將這三部分之和作爲最後結果即可。
最後的問題是:我們應該如何去求以當前節點作爲頭結點的路徑的數量?這裏依舊是按照樹的遍歷方式模板,採用前序遍歷,每到一個節點讓sum-root.val,並判斷sum是否爲0,如果爲零的話,則找到滿足條件的一條路徑。這條路徑將與該節點左右子樹的統計結果一起相加,往遞歸的上一層返回。這部分對應解法中的pathSumFrom()方法。
代碼如下,形式上是一個“雙遞歸”。
代碼
public class Solution {
public int pathSum(TreeNode root, int sum) {
if (root == null) return 0;
return pathSumFrom(root, sum) + pathSum(root.left, sum) + pathSum(root.right, sum);
}
private int pathSumFrom(TreeNode node, int sum) {
if (node == null) return 0;
return (node.val == sum ? 1 : 0)
+ pathSumFrom(node.left, sum - node.val) + pathSumFrom(node.right, sum - node.val);
}
}
空間複雜度: O(n) 遞歸調用棧所花費的空間。
時間複雜度:
最差情況 O(n^2) 無分支
最好情況 O(nlogn) 平衡樹的情況
思路2:回溯
上面的解法由於要經過很多重複的分支,因此耗時較長。
如果能用hashMap來記錄遍歷過程中所經過節點的狀態,就能節省很多不必要的重複遍歷。
下面轉載大神的思路和解法:
這道題用到了一個概念,叫前綴和。就是到達當前元素的路徑上,之前所有元素的和。
前綴和怎麼應用呢?
如果兩個數的前綴總和是相同的,那麼這些節點之間的元素總和爲零。進一步擴展相同的想法, 如果前綴總和currSum,在節點A和節點B處相差target,則位於節點A和節點B之間的元素之和是target。
因爲本題中的路徑是一棵樹,從根往任一節點的路徑上(不走回頭路),有且僅有一條路徑,因爲不存在環。(如果存在環,前綴和就不能用了,需要改造算法)
每個節點的前綴和,存儲在hashMap中,key是前綴和, value是大小爲key的前綴和出現的次數。
抵達當前節點(即B節點)後,將前綴和累加,然後查找在前綴和上,有沒有前綴和 currSum-target 的節點(即A節點),存在即表示從A到B有一條路徑之和滿足條件的情況。結果加上滿足前綴和currSum-target的節點的數量。然後遞歸進入左右子樹。
左右子樹遍歷完成之後,回到當前層,需要把當前節點添加的前綴和去除。避免回溯之後影響上一層。因爲思想是前綴和,不屬於前綴的,我們就要去掉它。
時間複雜度:每個節點只遍歷一次,O(N).
空間複雜度:開闢了一個hashMap,O(N).
作者:burning-summer
鏈接:https://leetcode-cn.com/problems/path-sum-iii/solution/qian-zhui-he-di-gui-hui-su-by-shi-huo-de-xia-tian/
來源:力扣(LeetCode)
著作權歸作者所有。商業轉載請聯繫作者獲得授權,非商業轉載請註明出處。
代碼
class Solution {
public int pathSum(TreeNode root, int sum) {
// key是前綴和, value是大小爲key的前綴和出現的次數
Map<Integer, Integer> prefixSumCount = new HashMap<>();
// 前綴和爲0的一條路徑
prefixSumCount.put(0, 1);
// 前綴和的遞歸回溯思路
return recursionPathSum(root, prefixSumCount, sum, 0);
}
/**
* 前綴和的遞歸回溯思路
* 從當前節點反推到根節點(反推比較好理解,正向其實也只有一條),有且僅有一條路徑,因爲這是一棵樹
* 如果此前有和爲currSum-target,而當前的和又爲currSum,兩者的差就肯定爲target了
* 所以前綴和對於當前路徑來說是唯一的,當前記錄的前綴和,在回溯結束,回到本層時去除,保證其不影響其他分支的結果
* @param node 樹節點
* @param prefixSumCount 前綴和Map
* @param target 目標值
* @param currSum 當前路徑和
* @return 滿足題意的解
*/
private int recursionPathSum(TreeNode node, Map<Integer, Integer> prefixSumCount, int target, int currSum) {
// 1.遞歸終止條件
if (node == null) {
return 0;
}
// 2.本層要做的事情
int res = 0;
// 當前路徑上的和
currSum += node.val;
//---核心代碼
// 看看root到當前節點這條路上是否存在節點前綴和加target爲currSum的路徑
// 當前節點->root節點反推,有且僅有一條路徑,如果此前有和爲currSum-target,而當前的和又爲currSum,兩者的差就肯定爲target了
// currSum-target相當於找路徑的起點,起點的sum+target=currSum,當前點到起點的距離就是target
res += prefixSumCount.getOrDefault(currSum - target, 0);
// 更新路徑上當前節點前綴和的個數
prefixSumCount.put(currSum, prefixSumCount.getOrDefault(currSum, 0) + 1);
//---核心代碼
// 3.進入下一層
res += recursionPathSum(node.left, prefixSumCount, target, currSum);
res += recursionPathSum(node.right, prefixSumCount, target, currSum);
// 4.回到本層,恢復狀態,去除當前節點的前綴和數量
prefixSumCount.put(currSum, prefixSumCount.get(currSum) - 1);
return res;
}
}