本文參考自《劍指offer》一書,代碼采用Java語言。
題目
輸入一棵二叉樹和一個整數,打印出二叉樹中結點值的和為輸入整數的所有路徑。從樹的根結點開始往下一直到葉結點所經過的結點形成一條路徑。
思路
1.假設找到了其中一條路徑,達到葉結點后,由於沒有指向父結點的指針,所以必須提前創建一個鏈表存儲前面經過的結點。
2.由於是從根結點出發,所以要想到使用前序遍歷
3.利用鏈表存儲結點,在該結點完成左右子樹的路徑搜索后(即遞歸函數結束,返回到其父結點之前),要刪除該結點,從而記錄別的路徑。
具體實現:通過前序遍歷,從根結點出發,每次在鏈表中存儲遍歷到的結點,若到達葉子結點,則根據所有結點的和是否等於輸入的整數,判斷是否打印輸出。在當前結點訪問結束后,遞歸函數將會返回到它的父結點,所以在函數退出之前,要刪除鏈表中的當前結點,以確保返回父結點時,儲存的路徑剛好是從根結點到父結點。
改進:書中的代碼是根據所有結點的和是否等於輸入的整數,判斷是否打印輸出。其實沒有這個必要,只需要在每次遍歷到一個結點時,令目標整數等於自己減去當前結點的值,若到達根結點時,最終的目標整數等於0就可以打印輸出。(描述得不是很清楚,就是相當於每個結點的目標整數不同,詳見代碼)
測試算例
1.功能測試(一條或者多條對應路徑;無對應路徑;結點值為正負零;)
2.特殊測試(根結點為null)
Java代碼
//題目:輸入一棵二叉樹和一個整數,打印出二叉樹中結點值的和為輸入整數的所
//有路徑。從樹的根結點開始往下一直到葉結點所經過的結點形成一條路徑。
public class PathInTree {
public class TreeNode {
int val = 0;
TreeNode left = null;
TreeNode right = null;
public TreeNode(int val) {
this.val = val;
}
}
public void findPath(TreeNode root,int target) {
if(root==null)
return;
ArrayList<Integer> list= new ArrayList<>();
printPath(root, target,list);
}
private void printPath(TreeNode node,int target,ArrayList<Integer> list) {
if(node==null)
return;
list.add(node.val);
target-=node.val; //每個結點的target不會受到方法的影響而改變
if(target==0 && node.left==null && node.right==null) { //葉子結點
for (Integer integer : list)
System.out.print(integer+" ");
System.out.println();
}else { //中間結點
printPath(node.left, target, list);
printPath(node.right, target, list);
}
list.remove(list.size()-1);
}
牛客網中的題目有多兩點要求:1.返回類型為ArrayList<ArrayList<Integer>>,即返回所有路徑的集合;2.要求返回的集合中,長度大的靠前。下面是實現的代碼:
/*
* 幾個要點:
* 1. 將nodeList和pathList定義成全局變量,避免在方法中的多次傳遞
* 2. 在pathList中添加nodeList時,因為nodeList會不斷變化,所以必須新建一個list存入
* 復制ArrayList的方法:newList=new ArrayList<Integer>(oldList)(復制內容,而不是復制地址,注意與newList=oldList的區分)
* 3. 在當前結點完成左右子樹的路徑搜索后,記得刪除nodeList中的當前結點
* 4. target是基本數據類型int,不會受到方法的影響而改變
*/
private ArrayList<Integer> nodeList= new ArrayList<>();
private ArrayList<ArrayList<Integer>> pathList = new ArrayList<>();
public ArrayList<ArrayList<Integer>> FindPath(TreeNode node,int target) {
if(node==null)
return pathList;
nodeList.add(node.val);
target-=node.val;
if(target==0 && node.left==null && node.right==null) { //葉子結點
//長度大的nodeList插入到pathList的前面
int i=0;
while(i<pathList.size() && nodeList.size()<pathList.get(i).size() ) //記得防止越界
i++;
pathList.add(i, new ArrayList<Integer>(nodeList)); //nodeList會隨方法變化,必須新建一個list存入pathList中!
}else { //不是葉子結點
pathList=FindPath(node.left, target);
pathList=FindPath(node.right, target);
}
nodeList.remove(nodeList.size()-1); //記得刪除當前結點
return pathList;
}
收獲
1.二叉樹的許多題目與遍歷(包括層序遍歷)有關,要深刻理解;根據根結點的位置判斷使用哪一種遍歷。
2.二叉樹遍歷過程沒有父結點指針,要保存路徑的話,必須要創建容器存儲之前的結點。
3.熟悉這道題中在每次遞歸函數結束前刪除當前結點的操作,這可以確保返回到父結點時路徑剛好是從根結點到父結點。
4.target-=node.val這句代碼非常好,多多體會。
5.牛客網的那部分代碼:在鏈表中存儲一個對象時,如果該對象是不斷變化的,則應該創建一個新的對象復制該對象的內容(而不是指向同一個對象),將這個新的對象存儲到鏈表中。。如果直接存儲該對象的話,鏈表中的對象也會不斷變化。基本數據類型和String則沒有這種問題。說到底其實是存儲的是地址還是值的問題。這篇文章討論了一下。
