轉載本文章請標明作者和出處
本文出自《Darwin的程序空間》
本文題目和部分解題思路來源自《劍指offer》第二版
題目
輸入某二叉樹的前序遍歷和中序遍歷的結果,請重建出該二叉樹。假設輸入的前序遍歷和中序遍歷的結果中都不含重複的數字。例如輸入前序遍歷序列{1,2,4,7,3,5,6,8}和中序遍歷序列{4,7,2,1,5,3,8,6},則重建二叉樹並返回。
重建後的二叉樹如下圖所示:
二叉樹的定義如下:
public class TreeNode {
int val;
TreeNode left;
TreeNode right;
TreeNode(int x) { val = x; }
}
解題分析
首先,想解這道題,我認爲首先要確認的是,你知道二叉樹的前、中、後序遍歷麼?
所謂二叉樹的前中後序鎖參照的是,什麼時候輸出當前節點,就比如說前序遍歷,他是先輸出當前節點,然後在依次輸出左子樹和右子樹;中序遍歷就是輸出左子樹,然後輸出當前節點,最後輸出右子樹;當然,後續遍歷就是先輸出左右子樹,再打印當前節點;
以前序遍歷爲例來說一下題目所示的二叉樹爲什麼前序遍歷的結果爲{1,2,4,7,3,5,6,8},因爲,依據前序遍歷的規定,我們先拿到根節點1,就要先打印出來,所以,先輸出1,然後遞歸到它的左節點2,直接輸出,然後再遞歸到2的左節點輸出4,節點4沒有左節點,然後遞歸節點4的右節點,輸出節點7,然後返回到節點2的右節點,節點2沒有右節點,於是返回到節點1的右節點3,輸出3,節點3有左節點,輸出5,然後是節點3的右節點6輸出,節點6有左節點8,輸出8;所以打印的結果就是{1,2,4,7,3,5,6,8}
- 前序遍歷代碼:
public static void prePrint(TreeNode root) {
if (Objects.nonNull(root)) {
System.out.println(root.val);
midPrint(root.left);
midPrint(root.right);
}
}
- 中序遍歷代碼:
public static void midPrint(TreeNode root) {
if (Objects.nonNull(root)) {
midPrint(root.left);
System.out.println(root.val);
midPrint(root.right);
}
}
- 後序遍歷代碼:
public static void afterPrint(TreeNode root) {
if (Objects.nonNull(root)) {
midPrint(root.left);
midPrint(root.right);
System.out.println(root.val);
}
}
我們可以看到,所謂前中後序遍歷二叉樹,就是當前節點的打印時間;
請您一定要理解前、中、後序遍歷,這是二叉樹的基礎;如果不明白,建議debug跟斷點,主要是理解遞歸的流程;
明確了前序遍歷和中序遍歷之後,我們想一下,前序遍歷中的第一個輸出的節點,是什麼節點,沒錯,它就是整個二叉樹的根節點,因爲,前序遍歷的過程中,根節點首先被輸出,再依次輸出它的左子樹和右子樹;
那麼根據中序遍歷的特性,根節點會在什麼位置輸出呢?答案是中間的某個位置,因爲中序遍歷會先輸出根節點的左子樹,在輸出根節點,再輸出根節點的右子樹;
所以我們這件事請可以明確,前序遍歷就是先是當前節點,接着是這個節點的左子樹的數組,然後是右子樹的數組;就好比,示例,1是根節點{2,4,7}就是左子樹{3,5,6,8}就是右子樹;中序遍歷的先是左子樹,再是當前節點,再是右子樹,示例中的中序遍歷結果就是{4,7,2}是左子樹,1是當前根節點,{5,3,8,6}是右子樹;
換句話說,給你一個前序遍歷,你馬上就能找到第一個節點1是根節點,然後拿着根節點1可以在中序遍歷中找這個節點1的位置,比如實例中,節點1在的位置是3,那麼我能得到的信息是,根節點1的左子樹有三個元素{4,7,2},右子樹有四個元素(因爲我們知道中序遍歷的數組總共多長){5,3,8,6},那麼前序遍歷中節點1的後三個元素就是左子樹{2,4,7},再後四個就是右子樹{3,5,6,8};這個就註定重建二叉樹,元素不能重複,因爲重複了你就不能根據前序遍歷的第一個節點取確認它在中序遍歷中的位置;
然後我們拿着節點1的左子樹遞歸進行如上操作,並把它賦予給節點1的左子樹引用,右子樹同理;因爲上面我們已經確定了,中序遍歷和前序遍歷的左右子樹在整個遍歷結果中的區間,就是索引區間值,每次遞歸的時候都指定中序遍歷和前序遍歷的當前樹的區間就好;
爲了避免每次我們都需要根據前序遍歷的第一個元素去找中序遍歷中該元素的位置,我們可以先把整個中序遍歷的結果存到哈希表中,這樣每次直接get即可;
當你第一次寫完代碼的時候,十有八九遞歸的四個索引值會有錯的,沒有關係,debug去調試,就好了,每次給它當前節點的左子樹和右子樹的索引區間就好了;
代碼(JAVA實現)
ps:這裏筆者使用的jdk爲1.8版本
public class Offer07_BuildTree {
public static void main(String[] args) {
TreeNode t = buildTree(new int[]{1, 2}, new int[]{2, 1});
System.out.println();
}
public static TreeNode buildTree(int[] preorder, int[] inorder) {
if (Objects.isNull(preorder) || preorder.length == 0) {
return null;
}
// 這步是藉助哈希表存儲中序遍歷的結果,方便每次遞歸的時候以O(1)的時間複雜度找到當前根節點在中序遍歷數組中的位置
Map<Integer, Integer> index = new HashMap<>();
for (int i = 0; i < inorder.length; i++) {
index.put(inorder[i], i);
}
return buildTree(preorder, inorder, 0, preorder.length - 1, 0, inorder.length - 1, index);
}
public static TreeNode buildTree(int[] preorder, int[] inorder, int preorderStart, int preorderEnd
, int inorderStart, int inorderEnd, Map<Integer, Integer> index) {
// 這步是防止沒有左節點和右節點導致的數組越界異常
if (preorderStart > preorderEnd) {
return null;
}
int root = preorder[preorderStart];
TreeNode treeNode = new TreeNode(root);
// 說明此時左子樹或者右子樹只有一個節點了,直接把這個節點返回,然後掛到父節點的左指針或者右指針上即可
if (preorderStart == preorderEnd) {
return treeNode;
} else {
int i = index.get(root);
int leftLength = i - inorderStart;
// 這步是關鍵,需要制定此次遍歷的子樹的左子樹在中序和前序遍歷中在各自數組中的前後座標
treeNode.left = buildTree(preorder, inorder, preorderStart + 1, preorderStart + leftLength
, inorderStart, i - 1, index);
treeNode.right = buildTree(preorder, inorder, preorderStart + leftLength + 1, preorderEnd
, i + 1, inorderEnd, index);
return treeNode;
}
}
}