一、構建二叉樹
我們構建一個如下圖所示的二叉樹:
我們使用下面的數據結構來描繪出這個二叉樹
1 public class Node { 2 private String name = ""; 3 public Node leftChild; 4 public Node rightChild; 5 6 public Node(String name) { 7 this.name = name; 8 } 9 10 public Node() { 11 } 12 13 public void setName(String name) { 14 this.name = name; 15 }
二、二叉樹的遍歷
前序遍歷:
1 /** 2 * 前序遍歷 3 */ 4 public String readPre() { 5 StringBuilder result = new StringBuilder(); 6 result.append(name); //前序遍歷 7 if (leftChild != null) { 8 result.append(leftChild.readPre()); 9 } 10 if (rightChild != null) { 11 result.append(rightChild.readPre()); 12 } 13 return result.toString(); 14 }
中序遍歷:
1 /** 2 * 中序遍歷 3 */ 4 public String readMid() { 5 StringBuilder result = new StringBuilder(); 6 if (leftChild != null) { 7 result.append(leftChild.readMid()); 8 } 9 result.append(name); //中序遍歷 10 if (rightChild != null) { 11 result.append(rightChild.readMid()); 12 } 13 return result.toString(); 14 }
后序遍歷:
1 /** 2 * 后序遍歷 3 */ 4 public String readEnd() { 5 StringBuilder result = new StringBuilder(); 6 if (leftChild != null) { 7 result.append(leftChild.readEnd()); 8 } 9 if (rightChild != null) { 10 result.append(rightChild.readEnd()); 11 } 12 result.append(name); //后序遍歷 13 return result.toString(); 14 }
從上面可以看到,前序、中序、后序遍歷的算法基本上差不多,其主要是在對根節點的訪問順序不同,然后利用遞歸的方式來進行實現。
層序遍歷:
1 /** 2 * 層序遍歷 3 */ 4 public String readLevel() { 5 Queue<Node> queue = new LinkedList<>(); 6 StringBuilder result = new StringBuilder(); 7 queue.offer(this); 8 while (!queue.isEmpty()) { 9 Node curNode = queue.poll(); 10 result.append(curNode.name); 11 if (curNode.leftChild != null) { 12 queue.offer(curNode.leftChild); 13 } 14 if (curNode.rightChild != null) { 15 queue.offer(curNode.rightChild); 16 } 17 } 18 return result.toString(); 19 }
跟其他遍歷不同,層序遍歷需要借助隊列來進行實現。首先將根節點放到隊列中,然后遍歷循環,依次將左孩子和右孩子放置到隊列中。
三、還原二叉樹
在第二章節中,獲得到前序、中序、后序、層序的結果依次如下:
1 String pre = "ABDGHCEIF"; //前序遍歷 2 String mid = "GDHBAEICF"; //中序遍歷 3 String end = "GHDBIEFCA"; //后序遍歷 4 String level = "ABCDEFGHI"; //層序遍歷
那能否通過上面的字符串還原出二叉樹的的形狀呢?這個分情況討論
前序+中序:
思路:通過前序獲得根節點的位置,利用根節點將中序序列分為左子樹和右子樹,然后不斷的遞歸划分即可。
代碼:
1 /** 2 * 根據前序和中序排序表獲取樹 3 */ 4 private static Node buildTreeByPreMid(char[] pre, int preBegin, int preEnd, char[] mid, int midBegin, int midEnd) { 5 Node root = new Node(); 6 root.setName(pre[preBegin] + ""); 7 8 int midRootLoc = 0; 9 for (int i = midBegin; i <= midEnd; i++) { 10 if (mid[i] == pre[preBegin]) { 11 midRootLoc = i; 12 break; 13 } 14 } 15 16 //遞歸得到左子樹 17 if (preBegin + (midRootLoc - midBegin) >= preBegin + 1 && (midRootLoc - 1) >= midBegin) { 18 Node leftChild = buildTreeByPreMid(pre, preBegin + 1, preBegin + (midRootLoc - midBegin), 19 mid, midBegin, midRootLoc - 1); 20 root.leftChild = leftChild; 21 } 22 23 //遞歸得到右子樹 24 if (preEnd >= (preEnd - (midEnd - midRootLoc) + 1) && (midEnd >= midRootLoc + 1)) { 25 Node rightChild = buildTreeByPreMid(pre, preEnd - (midEnd - midRootLoc) + 1, preEnd, 26 mid, midRootLoc + 1, midEnd); 27 root.rightChild = rightChild; 28 } 29 30 return root; 31 }
后序+中序:
思路:通過后序獲取根節點的位置,然后在中序中划分左子樹和右子樹,然后遞歸划分即可。
代碼:
1 /** 2 * 根據后序和中序遍歷還原樹 3 */ 4 private static Node buildTreeByMidEnd(char[] mid, int midBegin, int midEnd, char[] end, int endBegin, int endEnd) { 5 Node root = new Node(); 6 root.setName(end[endEnd] + ""); 7 int midRootLoc = 0; 8 for (int i = midEnd; i >= midBegin; i--) { 9 if (mid[i] == end[endEnd]) { 10 midRootLoc = i; 11 break; 12 } 13 } 14 15 //還原左子樹 16 if (midRootLoc - 1 >= midBegin && (endBegin + (midRootLoc - midBegin) - 1 >= endBegin)) { 17 Node leftChild = buildTreeByMidEnd(mid, midBegin, midRootLoc - 1, end, endBegin, endBegin + (midRootLoc - midBegin) - 1); 18 root.leftChild = leftChild; 19 } 20 21 //還原右子樹 22 if (midEnd >= midRootLoc + 1 && (endEnd - 1 >= endEnd - (midEnd - midRootLoc))) { 23 Node rightChild = buildTreeByMidEnd(mid, midRootLoc + 1, midEnd, end, endEnd - (midEnd - midRootLoc), endEnd - 1); 24 root.rightChild = rightChild; 25 } 26 27 return root; 28 }
層序+中序:
思路:根據層序遍歷獲取根節點的位置,然后將中序划分為左子樹和右子樹,然后根據划分出的左子樹和右子樹分別在層序遍歷中獲取其對應的層序順序,然后遞歸調用划分即可。
代碼如下:
1 /** 2 * 根據層序遍歷和中序遍歷得到結果 3 * @return 4 */ 5 private static Node buildTreeByMidLevel(char[] mid, char[] level, int midBegin, int midEnd) { 6 Node root = new Node(level[0] + ""); 7 8 int midLoc = -1; 9 for (int i = midBegin; i <= midEnd; i++) { 10 if (mid[i] == level[0]) { 11 midLoc = i; 12 break; 13 } 14 } 15 16 if (level.length >= 2) { 17 if (isLeft(mid, level[0], level[1])) { 18 Node left = buildTreeByMidLevel(mid, getLevelStr(mid, midBegin, midLoc - 1, level), midBegin, midLoc - 1); 19 root.leftChild = left; 20 if (level.length >= 3 && !isLeft(mid, level[0], level[2])) { 21 Node right = buildTreeByMidLevel(mid, getLevelStr(mid, midLoc + 1, midEnd, level), midLoc + 1, midEnd); 22 root.rightChild = right; 23 } 24 } else { 25 Node right = buildTreeByMidLevel(mid, getLevelStr(mid, midLoc + 1, midEnd, level), midLoc + 1, midEnd); 26 root.rightChild = right; 27 } 28 } 29 return root; 30 } 31 32 /** 33 * 將中序序列中midBegin與MidEnd的字符依次從level中提取出來,保持level中的字符順序不變 34 */ 35 private static char[] getLevelStr(char[] mid, int midBegin, int midEnd, char[] level) { 36 char[] result = new char[midEnd - midBegin + 1]; 37 int curLoc = 0; 38 for (int i = 0; i < level.length; i++) { 39 if (contains(mid, level[i], midBegin, midEnd)) { 40 result[curLoc++] = level[i]; 41 } 42 } 43 return result; 44 } 45 46 /** 47 * 如果str字符串的begin和end位置之間(包括begin和end)含有字符target,則返回true。 48 */ 49 private static boolean contains(char[] str, char target, int begin, int end) { 50 for (int i = begin; i <= end; i++) { 51 if (str[i] == target) { 52 return true; 53 } 54 } 55 return false; 56 }
其他的遍歷組合均不能還原出二叉樹的形狀,因為無法確認其左右孩子。例如,前序為AB,后序為AB,則無法確認出,B節點是A節點的左孩子還是右孩子,因此無法還原。
完整代碼已經上傳至GitHub: