樹的常見遍歷方式有:前序遍歷,中序遍歷,後序遍歷,層序遍歷,關於這些遍歷方式的遞歸和非遞歸實現:https://blog.csdn.net/Applying/article/details/84982712
現在,如果給定前中後序遍歷方式中的兩種,如何重建二叉樹呢?
1. 從前序和中序遍歷構建二叉樹(LeetCode 105)
這個是三種情況中,最常被問到的題目。LeetCode上也有對應的題目:(LeetCode 105)
思路:對於前序遍歷,其順序爲 (根值)(先序遍歷左子樹)(先序遍歷右子樹) 對於中序遍歷,其順序爲 (中序遍歷左子樹)(根值)(中序遍歷右子樹) 由於題目中默認,節點值是不會重複的,所以我們可以通過按順序遍歷中序遍歷的結果,找到根植對應的下標,將中序遍歷成功分成三個部分。然後,根據中序遍歷切出來的第一部分的左子樹遍歷結果的長度,來切先序遍歷中左子樹的部分。
實現代碼:
private static TreeNode buildTree(int[] preorder, int[] inorder) {
// 判斷輸入是否爲空,是否合法
if (preorder == null || inorder == null || preorder.length != inorder.length) return null;
return buildTree(preorder, 0, preorder.length - 1, inorder, 0, inorder.length - 1);
}
// 注意,這裏都是用下標表示
private static TreeNode buildTree(int[] preorder, int preStart, int preEnd, int[] inorder, int inStart, int inEnd) {
if (preEnd < preStart || inEnd < inStart) return null;
// 從先序遍歷的數組中,獲取根植
TreeNode root = new TreeNode(preorder[preStart]);
// 從中序數組中尋找根植對應的下標
int i = inStart;
for (; i <= inEnd; i++) {
if (inorder[i] == root.val)
break;
}
// 利用中序遍歷的特點,將左子樹跟右子樹分開
root.left = buildTree(preorder, preStart + 1, preStart + (i - inStart), inorder, inStart, i - 1);
root.right = buildTree(preorder, preStart + (i - inStart) + 1, preEnd, inorder, i + 1, inEnd);
return root;
}
這個解法還可以進一步利用map的數據結構進行優化:
private static TreeNode buildTree_(int[] preorder, int[] inorder) {
if (preorder == null || inorder == null || preorder.length != inorder.length) return null;
// 將中序遍歷的結果放置到map結構中,方便通過根植找下標
Map<Integer, Integer> map = new HashMap<>();
for (int i = 0; i < inorder.length; i++) {
map.put(inorder[i], i);
}
return buildTree(preorder, 0, preorder.length - 1, inorder, 0, inorder.length - 1, map);
}
private static TreeNode buildTree(int[] preorder, int preStart, int preEnd, int[] inorder, int inStart, int inEnd, Map<Integer, Integer> map) {
if (preEnd < preStart || inEnd < inStart) return null;
TreeNode root = new TreeNode(preorder[preStart]);
int i = map.get(root.val);
root.left = buildTree(preorder, preStart + 1, preStart + (i - inStart), inorder, inStart, i - 1, map);
root.right = buildTree(preorder, preStart + (i - inStart) + 1, preEnd, inorder, i + 1, inEnd, map);
return root;
}
這裏最重點就是如何將先序遍歷preorder切出其左右子樹部分,和中序遍歷inorder如何切出左右子樹部分,我是這麼考慮的:
中序遍歷比較容易進行切割,我們通過找根植下標 i,因此,中序遍歷 inorder 中,左子樹的範圍[inStart, i - 1],右子樹的範圍[i + 1, inEnd]。接下來,看先序遍歷,根據先序遍歷(根值)(先序遍歷左子樹)(先序遍歷右子樹),以及我們可以根據已經切分出來的中序遍歷中的左子樹的範圍來推導先序遍歷中左子樹的範圍,因此,先序遍歷 preorder 中,左子樹的範圍[preStart + 1, preStart + (i - inStart)],其中,i - inStart 爲中序遍歷中左子樹的長度範圍。右子樹的範圍:[preStart + (i - inStart) + 1, preEnd]。
2. 從後序和中序遍歷構建二叉樹(LeetCode 106)
這個問題對應 LeetCode 106,題目如下:
思路:這種情況跟第一種類似,同樣利用中序,對後序進行切分,實現代碼:
private static TreeNode buildTree(int[] inorder, int[] postorder) {
if (inorder == null || postorder == null || inorder.length != postorder.length) return null;
Map<Integer, Integer> map = new HashMap<>();
for (int i = 0; i < inorder.length; i++) {
map.put(inorder[i], i);
}
return buildTree(0, inorder.length - 1, 0, postorder.length - 1, inorder, postorder, map);
}
private static TreeNode buildTree(int inStart, int inEnd, int postStart, int postEnd,
int[] inorder, int[] postorder, Map<Integer, Integer> map) {
if (inEnd < inStart || postEnd < postStart) return null;
TreeNode root = new TreeNode(postorder[postEnd]);
int i = map.get(root.val);
root.left = buildTree(inStart, i - 1, postStart, postStart + (i - inStart) - 1, inorder, postorder, map);
root.right = buildTree(i + 1, inEnd, postStart + (i - inStart), postEnd - 1, inorder, postorder, map);
return root;
}
中序遍歷 (中序遍歷左子樹)(根值)(中序遍歷右子樹) ,而後序遍歷 (後序遍歷左子樹)(後序遍歷右子樹)(根值) ,類比第一種情況,進行切分即可
3. 從前序和後序遍歷構建二叉樹(LeetCode 889)
該情況對應LeetCode 889,題目如下:
這種情況,因爲沒有中序遍歷,對左右子樹進行切分,因此出現了可能存在多個符合答案的解。
這裏是找其中一個解即可。因此,我的思路是:根據先序 (根值)(先序遍歷左子樹)(先序遍歷右子樹) ,後序 (後序遍歷左子樹)(後序遍歷右子樹)(根值), 我假設左子樹存在,則先序中 preorder[preStart] 爲左子樹的根植,而在後序遍歷中,左子樹的根植,將在最後被遍歷到,也就是,它所在的位置是(後序遍歷左子樹)部分中的最後一個元素,因此,可以切割左子樹的範圍,對應右子樹的範圍也就出來了。
private static TreeNode constructFromPrePost(int[] pre, int[] post) {
if (pre == null || post == null || post.length != pre.length) return null;
return constructTree(0, pre.length - 1, 0, post.length - 1, pre, post);
}
private static TreeNode constructTree(int preStart, int preEnd, int postStart, int postEnd,
int[] pre, int[] post) {
if (preEnd < preStart || postEnd < postStart) return null;
TreeNode root = new TreeNode(pre[preStart]);
if (preEnd == preStart) return root;
int i = postStart;
// 找到後序遍歷中,左子樹的根植的位置
for (; i <= postEnd; i++) {
if (post[i] == pre[preStart + 1])
break;
}
root.left = constructTree(preStart + 1, preStart + 1 + (i - postStart), postStart, i, pre, post);
root.right = constructTree(preStart + 1 + (i - postStart) + 1, preEnd, i + 1, postEnd - 1, pre, post);
return root;
}
但,可能有人會說,可能會存在沒有左子樹的情況,例如:
確實,但按照我們上面的算法,[3, 8, 9] 這一部分,將被劃分成左子樹的部分,而後子樹爲 null:
對於這兩種情況,其前序,後序遍歷是完全相同的,即,對於求存在的其中一個符合條件的解,這種思路是可行的。