中序遍歷 前序遍歷 後序遍歷 程式設計題_LeetCode刷題總結之二叉樹的構建演算法-一道題13種解法...
前言
最近開始刷到一些二叉樹的構建的演算法題,挺有意思的,打算總結一下。這裡總結的都是確定二叉樹的構造演算法題,可能有多個構造結果的演算法題就沒考慮。
從構造目標上來看,這裡討論的演算法題可以分為兩種:
- 二叉樹的構造
- 二叉搜尋樹(BST)的構造
從構造條件上來看,這裡討論的演算法題也可以分為兩種:
- 不含重複數值節點的二叉樹的構造
- 含重複數值節點的二叉樹的構造
1.從前序與中序遍歷以及中序和後序遍歷構造二叉樹
這2個題目分別為:
- LeetCode.105 從前序與中序遍歷序列構造二叉樹,中等難度
- LeetCode.106 從中序與後序遍歷序列構造二叉樹,中等難度
1.1 解題方法
首先,按之前我們給分類條件給這兩種題目一個定性:它們都是一個不含重複節點的二叉樹構造演算法題
- 首先從先序/後序序列中找到根節點,根據根節點將中序序列分為左子樹和右子樹。
- 遞迴地對左子樹和右子樹進行二叉樹的構造。
思路其實是比較好想的,如果你在面試中遇到了這2個題目,那其實考察的編碼的基本功了。雖然比較好想,但是一次把程式碼寫出來且保證AC還是有一定難度的。
1.2 複雜度分析
時間複雜度: O(n),由於每次遞迴我們的inorder和preorder的總數都會減1,因此我們要遞迴n次。 空間複雜度: O(n),遞迴n次,系統呼叫棧的深度為n。
1.3 Show the code
<pre language="typescript" code_block="true">public class Q105BuildTree {
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!"); }
}
<pre language="typescript" code_block="true">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
<pre language="typescript" code_block="true"> 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
<pre language="typescript" code_block="true"> 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
<pre language="typescript" code_block="true"> 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
<pre language="typescript" code_block="true"> public TreeNode bstFromPreorder4(int[] preorder) {
int n = preorder.length;
if (n == 0) return null;
TreeNode root = new TreeNode(preorder[0]);
Deque<TreeNode> deque = new ArrayDeque<TreeNode>();
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
<pre language="typescript" code_block="true"> 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<TreeNode> 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<String> list = new LinkedList<>(Arrays.asList(temp));
return preOrderDeser(list);
}
/**
* 反前序遍歷(DFS)的序列化
*/
public TreeNode preOrderDeser(LinkedList<String> 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
<pre language="typescript" code_block="true"> /**
* 層次遍歷(BFS)
*/
public String serialize2(TreeNode root) {
if (root == null) {
return "";
}
StringBuilder sb = new StringBuilder();
LinkedList<TreeNode> 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<TreeNode> 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種解法,是不是有點誇張~