本文參考自《劍指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則沒有這種問題。說到底其實是存儲的是地址還是值的問題。這篇文章討論了一下。