最近开始刷到一些二叉树的构建的算法题,挺有意思的,打算总结一下。这里总结的都是确定二叉树的构造算法题,可能有多个构造结果的算法题就没考虑。
从构造目标上来看,这里讨论的算法题可以分为两种:
从构造条件上来看,这里讨论的算法题也可以分为两种:
这2个题目分别为:
首先,按之前我们给分类条件给这两种题目一个定性:它们都是一个不含重复节点的二叉树构造算法题。这2个题目的思路和做法都是一样的:
思路其实是比较好想的,如果你在面试中遇到了这2个题目,那其实考察的编码的基本功了。虽然比较好想,但是一次把代码写出来且保证AC还是有一定难度的。
时间复杂度: O(n),由于每次递归我们的inorder和preorder的总数都会减1,因此我们要递归n次。 空间复杂度: O(n),递归n次,系统调用栈的深度为n。
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个题目分别为:
同样地,按之前我们给分类条件给这两种题目一个定性:它们都是一个不含重复节点的二叉搜索树构造算法题。其中Q449的题干描述里面并没有给出“不含重复节点”的条件,但是它的测试用例里面都是“不含重复节点”的用例。这里,我们就暂且给它加上“不含重复节点”的条件。
很显然,仅仅是将这2个题目放在一起,我们就发现可以通过Q1008的解法去搞定Q449。于是我们这里先分析Q1008: 从前序遍历构造BST,随后再分析Q449: 序列化与反序列化BST。
问题1:为什么可以从前序遍历还原一个唯一的节点不重复的BST?
问题2:可不可以通过后序遍历还原一个唯一的节点不重复的BST?
答案同理是可以的。正因为如此,下面的给出的5种解法,都对应一种思路类似的从后序遍历构造BST的解法。所以,对于Q449: 序列化与反序列化BST,我们可以将其序列化成前序或者后序遍历,再从对应的遍历构造出BST,这样通过前序或者后序遍历反序列化BST这类解法就一共有10种了。至于,从后序遍历构造BST的5种方法,我这里就不贴了,有兴趣的朋友可以自己写一下或者参考下我的githubQ1008_1BSTFromPostorder。
首先将先序遍历排序得到中序遍历,随后使用分治的方法从先序遍历和中序遍历构造出二叉搜索树,即前面的方法。
时间复杂度: O(nlogn),排序。 空间复杂度: O(n),需要存储中序遍历结果。
参考前面的代码,就不重复贴了。
参考二分插入排序,思路大致如下: 考虑前n-1个点都构造好了,对于第n个点,我们根据BST树的性质二分找到对应的插入点,然后插入第n个点。
时间复杂度:O(nlogn),插入过程耗时T
空间复杂度:O(1)
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; } } 复制代码
第一个元素为root节点,其后的节点比root大的属于root的右子树, 比root小的是属于其左子树,递归构造左右子树。遍历找到左右子树的分界位置。
时间复杂度:O(n^2),考虑最坏情况,所有节点都在左子树,这种情况递归n次,每次内部迭代1+2+…n-1。
空间复杂度:O(n),递归n次,系统调用栈的深度为n。
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; } 复制代码
这是LeetCode的官方解法,我花了一会才能理解,感觉有点难想啊。
时间复杂度:O(n),仅扫描前序遍历一次 空间复杂度:O(n),考虑最坏情况,所有节点都在左子树,这种情况递归n次,系统栈深度n
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); } 复制代码
这也是LeetCode的官方解法,我第一次解题的思路和这个类似,不过当时处理逻辑没想清楚。
时间复杂度:O(n),仅扫描前序遍历一次 空间复杂度:O(n),考虑最坏情况,所有节点都在左子树,队列长度为n
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; } 复制代码
这题目为:
LeetCode.297 二叉树的序列化与反序列化,困难难度
同样地,按之前我们给分类条件给这题目一个定性:它是一个含重复节点的二叉树构造算法题。这个题目明显比上述的题目都困难,因为它的条件最宽泛。
问题:下面我们给的第一种解法就是通过带null节点的前序遍历还原二叉树,那么可以通过带null节点中序或者后序遍历来还原吗?
序列化: 时间复杂度:O(n),二叉树的前序遍历。 空间复杂度: O(n),递归需要系统栈和非递归需要手动构造的辅助栈。 反序列化: 时间复杂度:O(n),每一个节点处理一次。 空间复杂度: O(n),存储队列。
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; } 复制代码
序列化: 时间复杂度:O(n),二叉树的层次遍历。 空间复杂度: O(n),辅助队列。 反序列化: 时间复杂度:O(n),每一个节点处理一次。 空间复杂度: O(n),存储队列。
/** * 层次遍历(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; } 复制代码
可以发现,序列化和反序列化二叉树作为条件最宽泛的方法是实用于其他条件更强的算法题的。如果也用这个方法去解Q449: 序列化与反序列化BST,我们一共有13种解法,是不是有点夸张~