最近筆試做了這麼一道題,想和大家分享一下我的做法
目錄:1.題目
2.題目分析
3.功能與模組實現
4.完整程式碼
5.總結
一、題目
二叉樹路徑查詢
給定一棵二叉樹(結構如下),其中每個節點值為整數。給定一個值K,求所有滿足如下條件的路徑並將路徑上節點的值列印出來:
1、路徑方向必須向下,即只能從父節點指向子節點
2、路徑並不是必須從根節點開始或在葉節點結束。即樹上任意節點開始向下走到任意節點的路徑都允許。
3、路徑上的節點得分之和等於給定值K。節點得分=節點值+節點所在層(根節點為0,之後每層+1)。
l 示例:給定二叉樹[5,3,7,9,null,11,2,4,-1, null,null,2,-2],K=22
輸出:
5 3 9 -1
5 7 2 2
3 9 4
解釋:如第一個路徑5 3 9 -1,路徑上節點得分分別為5+0,3+1,9+2,-1+3,和為22
l 輸入格式: 第一行為一個整數K,第二行為一個二叉樹的層次遍歷序列,其中空子樹用 null 表示,每兩個數字或者null之間用空格分隔,例如:
22
5 3 7 9 null 11 2 4 -1 null null 2 -2
需要注意的是,null節點的子節點不會顯式的寫出來,如上例中第二行值為3的節點的右子樹為空,則該右空子樹的左右子樹不會再以null表示。
l 輸出格式: 分為多行,每行為一個滿足條件的路徑上節點的值的序列,例如:
5 3 9 -1
5 7 2 2
3 9 4
現有如下輸入:
35
5 4 8 11 null 13 4 7 2 null null 5 1 8 null 7 10 6 null null null
請用程式將正確結果輸出
二、題目分析
1.題目表面上想要查詢出所有符合條件的路徑,其實更深層次考察的是,如何構造二叉樹
2.如題目所述,二叉樹的層次遍歷序列是直接從中控臺輸入的,僅僅依靠此序列來層次構造二叉樹。這與我們以往的說法不同,通常需要兩條序列(比如前序和中序)來構造二叉樹,或者構造完全二叉樹的時候,可以直接使用前序遍歷序列。
3.題目還有一個要求,null節點的子節點不會顯式的寫出來,如上例中第二行值為3的節點的右子樹為空,則該右空子樹的左右子樹不會再以null表示。
4.層次構造二叉樹過程:
第五步和第六步中間紅色欄位很重要,這是子節點找父節點的思路
5.上面的分析是從上到下,按層次分析,通過父節點找其子節點,很好理解。但是程式碼實現無法做到這一點,我們只能由子節點找其父節點,怎麼說呢?
原因是,父節點找其子節點,我們通常會想到用遞迴,非常簡單,直接套用公式(n-NULLSUM)*2-x,這裡n表示父節點下標,x表示左右(左為一,右為二),一直找下去。但是仔細會發現,這裡的NULLSUM值可能不正確,
在遞迴構造二叉樹的過程中,無法發做到層次構造,它更像前序遍歷構造形式。比如上圖,從根節點的右節點7開始,讀到11,此時11節點的下標是5,此時(5-NULLSUM)*2+1=11得到左子節點2的下標,這裡NULLSUM為什麼是0呢?11節點的前面有null節點啊,NULLSUM不應該是1嗎?我們在看一下NULLSUM的定義:統計當前節點前面出現null值的節點。嗯我們理解的定義沒有問題,問題出現在遞迴呼叫上,它不層次演算法,而是一頭扎到底再回頭的那種,這就導致讀到11這個節點的時候,跳過了下標為4的null節點,故NULLSUM值還是為0。
6.基於以上問題,如何做到層次構造二叉樹呢?
這裡我想到了一個辦法,那就是讓子節點找父節點,這樣做的好處是,一對一思想,找到父節點,就可以跳到下一個節點繼續尋找其父節點。那麼還有個問題,怎麼知道當前節點是父節點的左節點還是右節點?其實很簡單,可以在第四點的層次構造二叉樹過程圖中可以發現,每個節點的左節點的下標一定是奇數,右節點的下標一定是偶數,那麼可以根據當前節點的下標奇偶性判斷其是左節點還是右節點。
子節點找父節點,可以通過(n-NULLSUM)*2-x這個公式的逆運算算出父節點的下標n,在利用樹的遍歷查詢,即可找到父節點
7.不知道大家發現第六點又產生了個問題,那就是利用樹的遍歷查詢,因為從中控臺輸入一連串序列,這個序列中的數可以不唯一,可重複,導致構造的樹每個節點的值不唯一,那麼樹的遍歷就不好使了。如何解決這個問題呢?其實很簡單,既然節點的值不唯一,那我們可以在樹的資料結構裡,給節點增加一個下標變數用來標識該節點,比如:
1 public class TreeNode { 2 public int val; 3 /* 4 * 由於題目給的二叉樹中節點值不唯一, 5 * 增加treeIndex做唯一標識 6 */ 7 public int treeIndex;//對應陣列下標 8 public TreeNode left; 9 public TreeNode right; 10 //由於陣列為String型別,需要轉型為整型,方便後面運算 11 public TreeNode(String x) { 12 val = Integer.parseInt(x); 13 } 14 15 public TreeNode() { 16 17 } 18 19 @Override 20 public String toString() { 21 return "TreeNode [val=" + val + "]"; 22 } 23 24 25 }
三、功能和模組實現
1、建立二叉樹
1.1、尋找父節點
1 public TreeNode searchNode(TreeNode root,int index) {//廣度優先搜尋,查詢父節點 2 if(root==null||index<0)return null; 3 LinkedList<TreeNode> list = new LinkedList<>();//連結串列,這裡我將其作為佇列 4 list.add(root);//把資料加入到佇列尾部 5 while(!list.isEmpty()) { 6 TreeNode node = list.poll(); 7 if(node.treeIndex==index) 8 return node; 9 if(node.left!=null) 10 list.add(node.left); 11 if(node.right!=null) 12 list.add(node.right); 13 } 14 15 return null; 16 }
1.2、處理傳入的序列
1 public TreeNode create(String[] levelOrder) {//考慮到給的陣列有null值,故用String型別 2 if(levelOrder.length==0) 3 return null; 4 TreeNode root = new TreeNode(levelOrder[0]);//根節點 5 LinkedList<Integer> list = new LinkedList<>();//連結串列,這裡我將其作為佇列 6 for(int i=1;i<levelOrder.length;i++) { 7 if(levelOrder[i]==null||"null".equals(levelOrder[i])) { 8 list.add(i); 9 continue; 10 } 11 TreeNode node = new TreeNode(levelOrder[i]); 12 node.treeIndex = i; 13 14 LinkedList<Integer> newList = new LinkedList(); 15 for (Iterator iterator = list.iterator(); iterator.hasNext();) { 16 newList.add((Integer) iterator.next()); 17 18 } 19 buildTree(root,node,i,newList,levelOrder); 20 21 } 22 return root; 23 }
1.3、建立二叉樹
1 //建立樹 2 public TreeNode buildTree(TreeNode root,TreeNode node,int i,LinkedList<Integer> list,String[] levelOrder) { 3 int NULLSUM = compareIndex(list,levelOrder,i); 4 /* 5 * 如題目給的示例:給定二叉樹[5,3,7,9,null,11,2,4,-1, null,null,2 ,-2] 6 * index: 0,1,2,3, 4 ,5 ,6,7, 8, 9 , 10 ,11,12 7 * 8 * 5 9 * / \ 10 * 3 7 11 * / / \ 12 * 9 11 2 13 * / \ / \ 14 * 4 -1 2 -2 15 * 思路:1.定義NULLSUM變數記錄null節點個數 16 * 2.通過compareIndex函式計算該節點的父節點層及以上出現null節點個數 17 * 3.(i-2)/2+NULLSUM可以計算出該節點的父節點 18 */ 19 if(i%2==0) { 20 TreeNode parent = searchNode(root,(i-2)/2+NULLSUM); 21 while(parent==null) { 22 NULLSUM++; 23 parent = searchNode(root,(i-2)/2+NULLSUM); 24 } 25 parent.right = node; 26 }else { 27 TreeNode parent = searchNode(root,(i-1)/2+NULLSUM); 28 while(parent==null) { 29 NULLSUM++; 30 parent = searchNode(root,(i-1)/2+NULLSUM); 31 } 32 parent.left = node; 33 } 34 35 return root; 36 }
為什麼在第21和28行設立while迴圈判斷找到的父節點是否為null?還記得下面這張圖嗎,父節點為null時,就會跳到下一個不為null的節點來代替指向原本父節點的子節點。逆過來,子節點找父節點,如果父節點時null,那可以跳到下一個直至不為null的父節點
2、二叉樹路徑查詢
2.1、路徑查詢入口
1 /* 2 * 傳入的引數分別為根節點root、定值K和當前節點所在層數dept(這個非常好用,因為傳入的根節點有可能只是樹中某節點) 3 */ 4 public List<List<Integer>> pathSumEntry(TreeNode root,int K,int dept){ 5 List<List<Integer>> result = new LinkedList<List<Integer>>();//用於儲存所有匹配路徑 6 List<Integer> currentResult = new LinkedList<Integer>();//用於儲存找到的當前匹配的路徑 7 pathSum(root,K,currentResult,result,dept); 8 return result; 9 }
2.2、路徑查詢主函式
1 /* 2 * 這裡主要使用遞迴加回溯的思想 3 */ 4 public void pathSum(TreeNode root,int K,List<Integer>currentResult,List<List<Integer>>result,int dept) { 5 if(root==null)return; 6 currentResult.add(new Integer(root.val)); 7 if(root.left==null&&root.right==null&&K==root.val+dept) { 8 result.add(new LinkedList(currentResult)); 9 }else { 10 pathSum(root.left,K-root.val-dept,currentResult,result,dept+1); 11 pathSum(root.right,K-root.val-dept,currentResult,result,dept+1); 12 } 13 currentResult.remove(currentResult.size()-1); 14 }
四、完整程式碼
1.樹的資料結構
1 public class TreeNode { 2 public int val; 3 /* 4 * 由於題目給的二叉樹中節點值不唯一, 5 * 增加treeIndex做唯一標識 6 */ 7 public int treeIndex;//對應陣列下標 8 public TreeNode left; 9 public TreeNode right; 10 //由於陣列為String型別,需要轉型為整型,方便後面運算 11 public TreeNode(String x) { 12 val = Integer.parseInt(x); 13 } 14 15 public TreeNode() { 16 17 } 18 19 @Override 20 public String toString() { 21 return "TreeNode [val=" + val + "]"; 22 } 23 24 25 }
2.構造二叉樹類
1 public class BuildTree { 2 3 public TreeNode searchNode(TreeNode root,int index) {//廣度優先搜尋,查詢父節點 4 if(root==null||index<0)return null; 5 LinkedList<TreeNode> list = new LinkedList<>();//連結串列,這裡我將其作為佇列 6 list.add(root);//把資料加入到佇列尾部 7 while(!list.isEmpty()) { 8 TreeNode node = list.poll(); 9 if(node.treeIndex==index) 10 return node; 11 if(node.left!=null) 12 list.add(node.left); 13 if(node.right!=null) 14 list.add(node.right); 15 } 16 17 return null; 18 } 19 20 21 public TreeNode create(String[] levelOrder) {//考慮到給的陣列有null值,故用String型別 22 if(levelOrder.length==0) 23 return null; 24 TreeNode root = new TreeNode(levelOrder[0]);//根節點 25 LinkedList<Integer> list = new LinkedList<>();//連結串列,這裡我將其作為佇列 26 for(int i=1;i<levelOrder.length;i++) { 27 if(levelOrder[i]==null||"null".equals(levelOrder[i])) { 28 list.add(i); 29 continue; 30 } 31 TreeNode node = new TreeNode(levelOrder[i]); 32 node.treeIndex = i; 33 34 LinkedList<Integer> newList = new LinkedList(); 35 for (Iterator iterator = list.iterator(); iterator.hasNext();) { 36 newList.add((Integer) iterator.next()); 37 38 } 39 buildTree(root,node,i,newList,levelOrder); 40 41 } 42 return root; 43 } 44 45 46 //建立樹 47 public TreeNode buildTree(TreeNode root,TreeNode node,int i,LinkedList<Integer> list,String[] levelOrder) { 48 int NULLSUM = compareIndex(list,levelOrder,i); 49 /* 50 * 如題目給的示例:給定二叉樹[5,3,7,9,null,11,2,4,-1, null,null,2 ,-2] 51 * index: 0,1,2,3, 4 ,5 ,6,7, 8, 9 , 10 ,11,12 52 * 53 * 5 54 * / \ 55 * 3 7 56 * / / \ 57 * 9 11 2 58 * / \ / \ 59 * 4 -1 2 -2 60 * 思路:1.定義NULLSUM變數記錄null節點個數 61 * 2.通過compareIndex函式計算該節點的父節點層及以上出現null節點個數 62 * 3.(i-2)/2+NULLSUM可以計算出該節點的父節點 63 */ 64 if(i%2==0) { 65 TreeNode parent = searchNode(root,(i-2)/2+NULLSUM); 66 while(parent==null) { 67 NULLSUM++; 68 parent = searchNode(root,(i-2)/2+NULLSUM); 69 } 70 parent.right = node; 71 }else { 72 TreeNode parent = searchNode(root,(i-1)/2+NULLSUM); 73 while(parent==null) { 74 NULLSUM++; 75 parent = searchNode(root,(i-1)/2+NULLSUM); 76 } 77 parent.left = node; 78 } 79 80 return root; 81 } 82 83 /* 84 * 比較下標所指向的值,判斷當前節點的父節點下標所在陣列位置的值是否等於null 85 * list為儲存空值的下標佇列,從頭到尾取值,並計算比較當前節點下標比棧值的子節點大 86 * 若大於,這NULLSUM++,否則停止,返回NULLSUM值 87 */ 88 public int compareIndex(LinkedList<Integer> list,String[] order,int i) { 89 int sum,NULLSUM = 0;//記錄陣列中null的數量 90 if(list==null&&list.size()==0) { 91 return 0; 92 } 93 while(!list.isEmpty()&&i>list.poll()*2) { 94 NULLSUM++; 95 } 96 return NULLSUM; 97 } 98 99 100 }
3.路徑查詢類
1 public class PathSum { 2 3 /* 4 * 傳入的引數分別為根節點root、定值K和當前節點所在層數dept(這個非常好用,因為傳入的根節點有可能只是樹中某節點) 5 */ 6 public List<List<Integer>> pathSumEntry(TreeNode root,int K,int dept){ 7 List<List<Integer>> result = new LinkedList<List<Integer>>();//用於儲存所有匹配路徑 8 List<Integer> currentResult = new LinkedList<Integer>();//用於儲存找到的當前匹配的路徑 9 pathSum(root,K,currentResult,result,dept); 10 return result; 11 } 12 /* 13 * 這裡主要使用遞迴加回溯的思想 14 */ 15 public void pathSum(TreeNode root,int K,List<Integer>currentResult,List<List<Integer>>result,int dept) { 16 if(root==null)return; 17 currentResult.add(new Integer(root.val)); 18 if(root.left==null&&root.right==null&&K==root.val+dept) { 19 result.add(new LinkedList(currentResult)); 20 }else { 21 pathSum(root.left,K-root.val-dept,currentResult,result,dept+1); 22 pathSum(root.right,K-root.val-dept,currentResult,result,dept+1); 23 } 24 currentResult.remove(currentResult.size()-1); 25 } 26 }
4.主函式
1 public class Main { 2 3 public static void main(String[] args) { 4 Scanner input = new Scanner(System.in); 5 System.out.print("輸入K:"); 6 int K = input.nextInt(); 7 System.out.println("輸入陣列(以'#'結束,例如:5 3 7 9 null 11 2 4 -1 null null 2 -2 #)"); 8 List<String> list = new ArrayList<String>();//集合接收輸入串 9 String cin = null; 10 while(!"#".equals((cin=input.next()))) { 11 list.add(cin); 12 13 } 14 //將集合轉成字串陣列 15 String[] levelOrder = new String[list.size()]; 16 for(int i=0;i<=list.size()-1;i++) { 17 levelOrder[i] = list.get(i); 18 } 19 20 //從左到右構造二叉樹,並尋找路徑和等於K的路徑 21 BuildTree tree = new BuildTree(); 22 TreeNode root = tree.create(levelOrder); 23 printTree(root,K,0); 24 } 25 26 public static void printTree(TreeNode root,int K,int dept) { 27 if(root==null)return; 28 PathSum pathSum = new PathSum(); 29 30 List<List<Integer>> result = pathSum.pathSumEntry(root, K,dept); 31 for (List resultList : result) { 32 System.out.println(resultList); 33 } 34 35 printTree(root.left,K,dept+1); 36 printTree(root.right,K,dept+1); 37 } 38 }
五、總結
一開始我還以為這道題就是個簡單的路徑查詢算題,採用遞迴加回溯演算法分分鐘鍾就可以解決這道題,當我再仔細看題的時候,才知道這不是一道簡簡單單的回溯演算法題。還有二叉樹如何構建問題,一看到構建二叉樹題,還是覺得簡單,結果自己還是太嫩了!沒錯,這是筆者第一次做分層構造二叉樹題,筆者的思考可以參考題目分析部分,這裡有筆者的思考的部分過程。第一次做分層構造二叉樹,所以花費了不少時間,考慮不周還望各位老師同學指點,如果大家有更好的分層構造二叉樹想法,可以分享連結到評論區,比如可以直接父節點找子節點。
每日一囧,微笑面對生活,我是懂先森