剑指 Offer 07. 重建二叉树【千字分析,三种方法】
立志用最少的代碼做最高效的表達(dá)
輸入某二叉樹的前序遍歷和中序遍歷的結(jié)果,請重建該二叉樹。假設(shè)輸入的前序遍歷和中序遍歷的結(jié)果中都不含重復(fù)的數(shù)字。
例如,給出
前序遍歷 preorder = [3,9,20,15,7]
中序遍歷 inorder = [9,3,15,20,7]
返回如下的二叉樹:
3
/ \
9 20
/ \
15 7
0 <= 節(jié)點個數(shù) <= 5000
文章目錄
- 1. 儲備知識
- 2. 分析
- 3. 解法:遞歸實現(xiàn)
- 4. 一次優(yōu)化:HashMap映射
- 5. 二次優(yōu)化:傳遞位置參數(shù)而非復(fù)制數(shù)組
- 6.測試用例
- 7. 本題考點
1. 儲備知識
樹是一種在實際編程中經(jīng)常遇到的數(shù)據(jù)結(jié)構(gòu)。它的邏輯很簡單:除根節(jié)點之外每個節(jié)點只有一個父節(jié)點,根節(jié)點沒有父節(jié)點;除葉節(jié)點之外所有節(jié)點都有一個或多個子節(jié)點,葉節(jié)點沒有子節(jié)點。父節(jié)點和子節(jié)點之間用指針鏈接。
由于樹的操作會涉及大量的指針,因此與樹有關(guān)的面試題都不太容易。當(dāng)面試官想考查應(yīng)聘者在有復(fù)雜指針操作的情況下寫代碼的能力時,他往往會想到用與樹有關(guān)的面試題。
面試的時候提到的樹,大部分是二叉樹。所謂二叉樹是樹的一種特殊結(jié)構(gòu),在二叉樹中每個節(jié)點最多只能有兩個子節(jié)點。在二叉樹中最重要的操作莫過于遍歷,即按照某一順序訪問樹中的所有節(jié)點。通常樹有如下幾種遍歷方式。
前序遍歷:先訪問根節(jié)點,再訪問左子節(jié)點,最后訪問右子節(jié)點。圖中的二叉樹的前序遍歷的順序是10、6、4、8、14、12、16。
中序遍歷:先訪問左子節(jié)點,再訪問根節(jié)點,最后訪問右子節(jié)點。圖中的二叉樹的中序遍歷的順序是4、6、8、10、12、14、16。
后序遍歷:先訪問左子節(jié)點,再訪問右子節(jié)點,最后訪問根節(jié)點。圖中的二叉樹的后序遍歷的順序是4、8、6、12、16、14、10。
2. 分析
在二叉樹的前序遍歷序列中,第一個數(shù)字總是樹的根節(jié)點的值。但在中序遍歷序列中,根節(jié)點的值在序列的中間,左子樹的節(jié)點的值位于根節(jié)點的值的左邊,而右子樹的節(jié)點的值位于根節(jié)點的值的右邊。因此我們需要掃描中序遍歷序列,才能找到根節(jié)點的值。
如圖2.7所示,前序遍歷序列的第一個數(shù)字1就是根節(jié)點的值。掃描中序遍歷序列,就能確定根節(jié)點的值的位置。根據(jù)中序遍歷的特點,在根節(jié)點的值1前面的3個數(shù)字都是左子樹節(jié)點的值,位于1后面的數(shù)字都是右子樹節(jié)點的值。
由于在中序遍歷序列中,有3個數(shù)字是左子樹節(jié)點的值,因此左子樹共有3個左子節(jié)點。同樣,在前序遍歷序列中,根節(jié)點后面的3個數(shù)字就是3個左子樹節(jié)點的值,再后面的所有數(shù)字都是右子樹節(jié)點的值。這樣我們就在前序遍歷和中序遍歷兩個序列中分別找到了左、右子樹對應(yīng)的子序列。
3. 解法:遞歸實現(xiàn)
class Solution {public TreeNode buildTree(int[] preorder, int[] inorder) {int n = preorder.length;if(n == 0) return null;// 1、建立根節(jié)點TreeNode r = new TreeNode(preorder[0]);// 2、找根節(jié)點的左右節(jié)點,即在in中找pre的值int p = 0;while(inorder[p] != r.val) p++;// 3、分割 + 遞歸r.left = buildTree(Arrays.copyOfRange(preorder, 1, p+1), Arrays.copyOfRange(inorder, 0, p));r.right = buildTree(Arrays.copyOfRange(preorder, p+1, n), Arrays.copyOfRange(inorder, p+1, n));// 4、建樹成功,返回樹return r;}}4. 一次優(yōu)化:HashMap映射
由于解法1中每次查根節(jié)點時,都需要將數(shù)列遍歷一遍,因此考慮:
定義HashMap為成員變量, 并重新構(gòu)造一個方法myBuildTree(),專門提供HashMap鍵值對的生成,而后調(diào)用BuildTree方法,實現(xiàn)遞歸。
通過map映射每個索引所在的位置,將查找索引的級數(shù)降低至O(1)
定義兩個方法的原因是:若將HashMap的映射生成定義在了方法內(nèi),每次遞歸都會重復(fù)進(jìn)行HashMap映射的生成。
class Solution {// 定義為成員變量,這樣類中兩個方法都可以使用它private Map<Integer, Integer>indexMap;public TreeNode myBuildTree(int[] preorder, int[] inorder) {int n = preorder.length;if(n == 0) return null;// 1. 定義根節(jié)點TreeNode r = new TreeNode(preorder[0]);// 2、查找界限值 + 遞歸int p = indexMap.get(r.val);r.left = buildTree(Arrays.copyOfRange(preorder, 1, p+1), Arrays.copyOfRange(inorder, 0, p));r.right = buildTree(Arrays.copyOfRange(preorder, p+1, n), Arrays.copyOfRange(inorder, p+1, n));return r;}public TreeNode buildTree(int[] preorder, int[] inorder) {int n = preorder.length;indexMap = new HashMap<Integer, Integer>();for(int i = 0; i < n; i++) {indexMap.put(inorder[i], i);}return myBuildTree(preorder, inorder);} }5. 二次優(yōu)化:傳遞位置參數(shù)而非復(fù)制數(shù)組
再次提交,發(fā)現(xiàn)耗時依然很高,仔細(xì)思考后發(fā)現(xiàn),在源碼中,每次遞歸都需要進(jìn)行兩次數(shù)組的復(fù)制,而每次復(fù)制的耗時為O(n), 因此考慮:
取消數(shù)組的復(fù)制操作(copyOfRange()方法),改為使用邊界值(left, right)代替,再次降低時間復(fù)雜度。
class Solution3 {private Map<Integer, Integer> indexMap;public TreeNode myBuildTree(int[] preorder, int[] inorder, int preorder_left, int preorder_right, int inorder_left, int inorder_right) {if (preorder_left > preorder_right) {return null;}// 前序遍歷中的第一個節(jié)點就是根節(jié)點int preorder_root = preorder_left;// 在中序遍歷中定位根節(jié)點int inorder_root = indexMap.get(preorder[preorder_root]);// 先把根節(jié)點建立出來TreeNode root = new TreeNode(preorder[preorder_root]);// 得到左子樹中的節(jié)點數(shù)目int size_left_subtree = inorder_root - inorder_left;// 遞歸地構(gòu)造左子樹,并連接到根節(jié)點// 先序遍歷中「從 左邊界+1 開始的 size_left_subtree」個元素就對應(yīng)了中序遍歷中「從 左邊界 開始到 根節(jié)點定位-1」的元素root.left = myBuildTree(preorder, inorder, preorder_left + 1, preorder_left + size_left_subtree, inorder_left, inorder_root - 1);// 遞歸地構(gòu)造右子樹,并連接到根節(jié)點// 先序遍歷中「從 左邊界+1+左子樹節(jié)點數(shù)目 開始到 右邊界」的元素就對應(yīng)了中序遍歷中「從 根節(jié)點定位+1 到 右邊界」的元素root.right = myBuildTree(preorder, inorder, preorder_left + size_left_subtree + 1, preorder_right, inorder_root + 1, inorder_right);return root;}public TreeNode buildTree(int[] preorder, int[] inorder) {int n = preorder.length;// 構(gòu)造哈希映射,幫助我們快速定位根節(jié)點indexMap = new HashMap<Integer, Integer>();for (int i = 0; i < n; i++) {indexMap.put(inorder[i], i);}return myBuildTree(preorder, inorder, 0, n - 1, 0, n - 1);} }6.測試用例
- 普通二叉樹(完全二叉樹、不完全二叉樹)
- 特殊二叉樹(退化為鏈表的二叉樹、只有一個節(jié)點的二叉樹)
- 特殊輸入測試(空樹、錯誤用例)
7. 本題考點
- 考查應(yīng)聘者對二叉樹前序遍歷和中序遍歷的理解程度
- 考查應(yīng)聘者分析復(fù)雜問題的能力
總結(jié)
以上是生活随笔為你收集整理的剑指 Offer 07. 重建二叉树【千字分析,三种方法】的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 【三种解法】剑指 Offer 06. 从
- 下一篇: 【双100%提交】剑指 Offer 09