前言
最近開始刷到一些二叉樹的構建的算法題,挺有意思的,打算總結一下。這裏總結的都是確定二叉樹的構造算法題,可能有多個構造結果的算法題就沒考慮。
從構造目標上來看,這裏討論的算法題可以分爲兩種:
- 二叉樹的構造
- 二叉搜索樹(BST)的構造
從構造條件上來看,這裏討論的算法題也可以分爲兩種:
- 不含重複數值節點的二叉樹的構造
- 含重複數值節點的二叉樹的構造
1.從前序與中序遍歷以及中序和後序遍歷構造二叉樹
這2個題目分別爲:
- LeetCode.105 從前序與中序遍歷序列構造二叉樹,中等難度
- LeetCode.106 從中序與後序遍歷序列構造二叉樹,中等難度
1.1 解題方法
首先,按之前我們給分類條件給這兩種題目一個定性:它們都是一個不含重複節點的二叉樹構造算法題。這2個題目的思路和做法都是一樣的:
- 首先從先序/後序序列中找到根節點,根據根節點將中序序列分爲左子樹和右子樹。
- 遞歸地對左子樹和右子樹進行二叉樹的構造。
思路其實是比較好想的,如果你在面試中遇到了這2個題目,那其實考察的編碼的基本功了。雖然比較好想,但是一次把代碼寫出來且保證AC還是有一定難度的。
1.2 複雜度分析
時間複雜度: O(n),由於每次遞歸我們的inorder和preorder的總數都會減1,因此我們要遞歸n次。 空間複雜度: O(n),遞歸n次,系統調用棧的深度爲n。
1.3 Show the code
public class Q105BuildTree { public TreeNode buildTree(int[] preorder, int[] inorder) { int len = preorder.length; if (len == 0) return null; return buildTreeNode(preorder, 0, len - 1, inorder, 0, len - 1); } private TreeNode buildTreeNode(int[] preorder, int start1, int end1, int[] inorder, int start2, int end2) { int rootVal = preorder[start1]; TreeNode root = new TreeNode(rootVal); if (start1 < end1) { int idx = findRootIdxInOrder(inorder, start2, end2, rootVal); int leftLen = idx - start2; int rightLen = end2 - idx; if (leftLen > 0) { root.left = buildTreeNode(preorder, start1 + 1, start1 + leftLen, inorder, start2, start2 + leftLen - 1); } if (rightLen > 0) { root.right = buildTreeNode(preorder, start1 + 1 + leftLen, end1, inorder, idx + 1, end2); } } return root; } private int findRootIdxInOrder(int[] array, int start, int end, int val) { for (int i = start; i <= end; i++) { if (array[i] == val) { return i; } } throw new UnsupportedOperationException("Unreachable logic!"); } } 複製代碼
public class Q106BuildTree { public TreeNode buildTree(int[] inorder, int[] postorder) { if (inorder.length == 0) return null; return buildTreeTrace(inorder, 0, inorder.length - 1, postorder, 0, postorder.length - 1); } private TreeNode buildTreeTrace(int[] inorder, int inLeft, int inRight, int[] postorder, int postLeft, int postRight) { int rootVal = postorder[postRight]; TreeNode root = new TreeNode(rootVal); if (postLeft == postRight) { return root; } int inOrderRootIdx = findRootIdxInOrder(inorder, inLeft, inRight, rootVal); int leftTreeLen = inOrderRootIdx - inLeft; if (leftTreeLen > 0) { root.left = buildTreeTrace(inorder, inLeft, inLeft + leftTreeLen - 1, postorder, postLeft, postLeft + leftTreeLen - 1); } int rightTreeLen = inRight - inOrderRootIdx; if (rightTreeLen > 0) { root.right = buildTreeTrace(inorder, inOrderRootIdx + 1, inRight, postorder, postLeft + leftTreeLen, postRight - 1); } return root; } private int findRootIdxInOrder(int[] inorder, int inLeft, int inRight, int rootVal) { for (int i = inLeft; i <= inRight; i++) { if (inorder[i] == rootVal) { return i; } } throw new UnsupportedOperationException("Unreachable logic!"); } } 複製代碼
2. 從前序遍歷構造BST以及序列化與反序列化BST
這2個題目分別爲:
- LeetCode.449 序列化和反序列化二叉搜索樹,中等難度
- LeetCode.1008 先序遍歷構造二叉樹,中等難度
同樣地,按之前我們給分類條件給這兩種題目一個定性:它們都是一個不含重複節點的二叉搜索樹構造算法題。其中Q449的題幹描述裏面並沒有給出“不含重複節點”的條件,但是它的測試用例裏面都是“不含重複節點”的用例。這裏,我們就暫且給它加上“不含重複節點”的條件。
很顯然,僅僅是將這2個題目放在一起,我們就發現可以通過Q1008的解法去搞定Q449。於是我們這裏先分析Q1008: 從前序遍歷構造BST,隨後再分析Q449: 序列化與反序列化BST。
2.0 解題思考
問題1:爲什麼可以從前序遍歷還原一個唯一的節點不重複的BST?
- 前序遍歷是:根-左子樹-右子樹,那麼對於一個前序遍歷,我們便可以獲取到BST的根節點。
- 拿到根節點後,我們便可以找到左子樹的所有節點和右子樹的所有節點。同樣地,可以獲取左子樹和右子樹的根節點。
- 由於左子樹和右子樹也是以根-左-右的方式進行遍歷的,那麼也適用於上述的構造方式。
問題2:可不可以通過後序遍歷還原一個唯一的節點不重複的BST?
答案同理是可以的。正因爲如此,下面的給出的5種解法,都對應一種思路類似的從後序遍歷構造BST的解法。所以,對於Q449: 序列化與反序列化BST,我們可以將其序列化成前序或者後序遍歷,再從對應的遍歷構造出BST,這樣通過前序或者後序遍歷反序列化BST這類解法就一共有10種了。至於,從後序遍歷構造BST的5種方法,我這裏就不貼了,有興趣的朋友可以自己寫一下或者參考下我的githubQ1008_1BSTFromPostorder。
2.1 解題方法1: 先序遍歷和中序遍歷構造二叉樹
2.1.1 解題思路
首先將先序遍歷排序得到中序遍歷,隨後使用分治的方法從先序遍歷和中序遍歷構造出二叉搜索樹,即前面的方法。
2.1.2 複雜度分析
時間複雜度: O(nlogn),排序。 空間複雜度: O(n),需要存儲中序遍歷結果。
2.1.3 Show the code
參考前面的代碼,就不重複貼了。
2.2 解題方法2:二分查找插入點
2.2.1 解題思路
參考二分插入排序,思路大致如下: 考慮前n-1個點都構造好了,對於第n個點,我們根據BST樹的性質二分找到對應的插入點,然後插入第n個點。
2.2.2 複雜度分析
時間複雜度:O(nlogn),插入過程耗時T
空間複雜度:O(1)
2.2.3 Show the code
public TreeNode bstFromPreorder(int[] preorder) { TreeNode root = new TreeNode(preorder[0]); for (int i = 1; i < preorder.length; i++) { int val = preorder[i]; TreeNode node = new TreeNode(val); putNode(root, node); } return root; } private void putNode(TreeNode root, TreeNode node) { TreeNode last = null; TreeNode iter = root; while (iter != null) { last = iter; if (iter.val > node.val) { iter = iter.left; } else { iter = iter.right; } } if (last.val > node.val) { last.left = node; } else { last.right = node; } } 複製代碼
2.3 解題方法3:遞歸
2.3.1 解題思路
第一個元素爲root節點,其後的節點比root大的屬於root的右子樹, 比root小的是屬於其左子樹,遞歸構造左右子樹。遍歷找到左右子樹的分界位置。
2.3.2 複雜度分析
時間複雜度:O(n^2),考慮最壞情況,所有節點都在左子樹,這種情況遞歸n次,每次內部迭代1+2+…n-1。
空間複雜度:O(n),遞歸n次,系統調用棧的深度爲n。
2.3.3 Show the code
public TreeNode bstFromPreorder2(int[] preorder) { return bstFromPreorder(preorder, 0, preorder.length - 1); } private TreeNode bstFromPreorder(int[] preorder, int start, int end) { if (start > end) { return null; } TreeNode root = new TreeNode(preorder[start]); int idx = start + 1; while (idx <= end && preorder[idx] < preorder[start]) { idx++; } root.left = bstFromPreorder(preorder, start + 1, idx - 1); root.right = bstFromPreorder(preorder, idx, end); return root; } 複製代碼
2.4 解題方法4:(lower, upper) + 遞歸
這是LeetCode的官方解法,我花了一會才能理解,感覺有點難想啊。
2.4.1 解題思路
- 將 lower 和 upper 的初始值分別設置爲負無窮和正無窮,因爲根節點的值可以爲任意值。
- 從先序遍歷的第一個元素 idx = 0 開始構造二叉樹,構造使用的函數名爲 helper(lower, upper): 如果 idx = n,即先序遍歷中的所有元素已經被添加到二叉樹中,那麼此時構造已經完成; 如果當前 idx 對應的先序遍歷中的元素 val = preorder[idx] 的值不在 [lower, upper] 範圍內,則進行回溯; 如果 idx 對應的先序遍歷中的元素 val = preorder[idx] 的值在 [lower, upper] 範圍內,則新建一個節點 root,並對其左孩子遞歸處理 helper(lower, val),對其右孩子遞歸處理 helper(val, upper)。
2.4.2 複雜度分析
時間複雜度:O(n),僅掃描前序遍歷一次 空間複雜度:O(n),考慮最壞情況,所有節點都在左子樹,這種情況遞歸n次,系統棧深度n
2.4.3 Show the code
int idx = 0; int[] preorder; int n; public TreeNode helper(int lower, int upper) { // if all elements from preorder are used // then the tree is constructed if (idx == n) return null; int val = preorder[idx]; // if the current element // couldn't be placed here to meet BST requirements if (val < lower || val > upper) return null; // place the current element // and recursively construct subtrees TreeNode root = new TreeNode(val); idx++; root.left = helper(lower, val); root.right = helper(val, upper); return root; } public TreeNode bstFromPreorder3(int[] preorder) { this.preorder = preorder; n = preorder.length; return helper(Integer.MIN_VALUE, Integer.MAX_VALUE); } 複製代碼
2.5 解題方法5:迭代
這也是LeetCode的官方解法,我第一次解題的思路和這個類似,不過當時處理邏輯沒想清楚。
2.5.1 解題思路
- 將先序遍歷中的第一個元素作爲二叉樹的根節點,即 root = new TreeNode(preorder[0]),並將其放入棧中。
- 使用 for 循環迭代先序遍歷中剩下的所有元素:
- 將棧頂的元素作爲父節點,當前先序遍歷中的元素作爲子節點。如果棧頂的元素值小於子節點的元素值,則將棧頂的元素彈出並作爲新的父節點,直到棧空或棧頂的元素值大於子節點的元素值。注意,這裏作爲父節點的是最後一個被彈出棧的元素,而不是此時棧頂的元素;
- 如果父節點的元素值小於子節點的元素值,則子節點爲右孩子,否則爲左孩子;
- 將子節點放入棧中。
2.5.2 複雜度分析
時間複雜度:O(n),僅掃描前序遍歷一次 空間複雜度:O(n),考慮最壞情況,所有節點都在左子樹,隊列長度爲n
2.5.3 Show the code
public TreeNode bstFromPreorder4(int[] preorder) { int n = preorder.length; if (n == 0) return null; TreeNode root = new TreeNode(preorder[0]); Deque deque = new ArrayDeque(); deque.push(root); for (int i = 1; i < n; i++) { // take the last element of the deque as a parent // and create a child from the next preorder element TreeNode node = deque.peek(); TreeNode child = new TreeNode(preorder[i]); // adjust the parent while (!deque.isEmpty() && deque.peek().val < child.val) node = deque.pop(); // follow BST logic to create a parent-child link if (node.val < child.val) node.right = child; else node.left = child; // add the child into deque deque.push(child); } return root; } 複製代碼
3. 序列化與反序列化二叉樹
這題目爲:
LeetCode.297 二叉樹的序列化與反序列化,困難難度
同樣地,按之前我們給分類條件給這題目一個定性:它是一個含重複節點的二叉樹構造算法題。這個題目明顯比上述的題目都困難,因爲它的條件最寬泛。
3.0 解題思考
問題:下面我們給的第一種解法就是通過帶null節點的前序遍歷還原二叉樹,那麼可以通過帶null節點中序或者後序遍歷來還原嗎?
- 這裏就我自己的思考,我認爲帶null節點的前序或者後序遍歷是可以還原二叉樹的,而中序遍歷則不行(可能是我還沒寫出來解法)。
- 一個比較關鍵的點就是這2種都可以明晰的知道根節點,這樣就能根據根節點遞歸還原,而中序遍歷卻無法確定根節點。
- 這裏通過後序遍歷還原的代碼與前序比較類似,我就不貼了,有興趣的朋友可以自己寫一下或者參考下我的githubQ297SerializeAndDeserializeBinaryTree。
3.1 解題方法1:帶null節點的前序遍歷(DFS)
3.1.1 解題思路
- 樹序列化的時候將葉子節點的左右null節點孩子也保存到先序遍歷結果中。
- 反序列化的時候可以根據null節點的信息將還原二叉樹。
3.1.2 複雜度分析
序列化: 時間複雜度:O(n),二叉樹的前序遍歷。 空間複雜度: O(n),遞歸需要系統棧和非遞歸需要手動構造的輔助棧。 反序列化: 時間複雜度:O(n),每一個節點處理一次。 空間複雜度: O(n),存儲隊列。
3.1.3 Show the code
public String serialize(TreeNode root) { StringBuilder res = preOrderNonRecur(root, new StringBuilder()); return res.toString(); } /** * 前序遍歷(DFS),根-左-右 * 1 * / \ * 2 3 * / \ * 4 5 * 1,2,null,null,3,4,null,null,5,null,null */ StringBuilder preOrderRecur(TreeNode root, StringBuilder sb) { if (root == null) { sb.append("null,"); return sb; } else { sb.append(root.val); sb.append(","); preOrderRecur(root.left, sb); preOrderRecur(root.right, sb); } return sb; } StringBuilder preOrderNonRecur(TreeNode root, StringBuilder sb) { Stack stack = new Stack<>(); stack.add(root); while (!stack.isEmpty()) { TreeNode pop = stack.pop(); sb.append(pop == null ? "null" : pop.val).append(","); if (pop == null) continue; stack.add(pop.right); stack.add(pop.left); } return sb; } // Decodes your encoded data to tree. public TreeNode deserialize(String data) { // 將序列化的結果轉爲字符串數組 String[] temp = data.split(","); // 字符串數組轉爲集合類便於操作 LinkedList list = new LinkedList<>(Arrays.asList(temp)); return preOrderDeser(list); } /** * 反前序遍歷(DFS)的序列化 */ public TreeNode preOrderDeser(LinkedList list) { TreeNode root; if (list.peekFirst().equals("null")) { // 刪除第一個元素 則第二個元素成爲新的首部 便於遞歸 list.pollFirst(); return null; } else { root = new TreeNode(Integer.parseInt(list.peekFirst())); list.pollFirst(); root.left = preOrderDeser(list); root.right = preOrderDeser(list); } return root; } 複製代碼
3.2 解題方法2:帶null節點的層次遍歷(BFS)
3.2.1 解題思路
- 樹序列化的時候將葉子節點的左右null節點孩子也保存到層次遍歷結果中。
- 反序列化的時候可以根據null節點的信息將還原二叉樹。
3.2.2 複雜度分析
序列化: 時間複雜度:O(n),二叉樹的層次遍歷。 空間複雜度: O(n),輔助隊列。 反序列化: 時間複雜度:O(n),每一個節點處理一次。 空間複雜度: O(n),存儲隊列。
3.2.3 Show the code
/** * 層次遍歷(BFS) */ public String serialize2(TreeNode root) { if (root == null) { return ""; } StringBuilder sb = new StringBuilder(); LinkedList queue = new LinkedList<>(); queue.add(root); while (!queue.isEmpty()) { TreeNode pop = queue.removeFirst(); sb.append(pop == null ? "null," : (pop.val + ",")); if (pop != null) { queue.add(pop.left); queue.add(pop.right); } } return sb.toString(); } /** * 反層次遍歷(BFS)的序列化 */ public TreeNode deserialize2(String data) { if (data.isEmpty()) return null; String[] strs = data.split(","); Integer[] layerNode = new Integer[strs.length]; for (int i = 0; i < strs.length; i++) { layerNode[i] = strs[i].equals("null") ? null : Integer.parseInt(strs[i]); } Queue queue = new ArrayDeque<>(); TreeNode root = new TreeNode(layerNode[0]); queue.add(root); int cur = 1; while (!queue.isEmpty()) { TreeNode pop = queue.poll(); if (layerNode[cur] != null) { pop.left = new TreeNode(layerNode[cur]); queue.add(pop.left); } cur++; if (layerNode[cur] != null) { pop.right = new TreeNode(layerNode[cur]); queue.add(pop.right); } cur++; } return root; } 複製代碼
4. 總結
可以發現,序列化和反序列化二叉樹作爲條件最寬泛的方法是實用於其他條件更強的算法題的。如果也用這個方法去解Q449: 序列化與反序列化BST,我們一共有13種解法,是不是有點誇張~