这个系列总结一下,给定前/中/后序遍历其中两个遍历的结果,要求构造出原二叉树,这种题。
主要思路是用递归+分治
普通二叉树
前序+中序
leetcode题目地址
有以下两种方法
- 传递inOrder的左右边界和preOrder的offset。offset是指从preOrder获取到的当前子树的根的下标(preOrder是先遍历根节点,所以对于每一个子树,对应preOrder的序列的第一个元素就是根节点)
【易错点】offset计算要注意一下,别算错了。各个变量的含义要始终明确。
【时间复杂度】O(nlgn)
public class Solution {
public TreeNode BuildTree(int[] preorder, int[] inorder) {
return Build(preorder, inorder, 0, inorder.Length-1, 0);
}
//left, right: belong inorder
public TreeNode Build(int[] preorder, int[] inorder, int left, int right, int offset){
if(left>right||offset>preorder.Length) return null;
var root = new TreeNode(preorder[offset]);
//2. find
var index = 0;
for(var i=left;i<=right;i++){
if(inorder[i]==root.val){
index = i;
break;
}
}
//3.connect left and right
root.left = Build(preorder, inorder, left, index-1, offset+1);
root.right = Build(preorder, inorder, index+1, right, offset+1+index-left);
return root;
}
}
- 用字典把元素在inorder里面的下标记录下来(预处理),offset通过规律发现直接自增就work。
【巧妙的点】用字典记录下标,preOrder的第一个元素是根节点,找这个根节点在inOrder中的位置的时候就O(1)复杂度。而且如果按照类似先序的解决问题的递归,在preOrder中找根节点的时候,就类似和preOrder同步了,所以根节点下标可以通过全局变量自增。这种方法至少在C#的代码速度分布图中是最快的那个。
【易错点】易错点就在于。。。这种巧妙地方法要记住哇
【复杂度】O(n)
public class Solution {
int offset = 0;
Dictionary<int, int> dict = new Dictionary<int, int>();
public TreeNode BuildTree(int[] preorder, int[] inorder)
{
for (var i=0;i<inorder.Length;i++)
{
dict[inorder[i]] = i;
}
return helper(preorder, inorder, 0, inorder.Length);
}
private TreeNode helper(int[] preorder, int[] inorder, int left, int right)
{
if (left == right)
return null;
var rootVal = preorder[offset];
var root = new TreeNode(rootVal);
var index = dict[rootVal];
offset++;
root.left = helper(preorder, inorder, left, index);
root.right = helper(preorder, inorder, index + 1, right);
return root;
}
}
对比后,发现第二种方法会快一点,所以后面的两种题都用第二种方法。
中序+后序
leetcode题目地址
【思路】沿用使用dictionary存储元素下标的思想。
【要注意的点】现在的offset是postOrder中的,所以要想直接通过offset递减来获取每次子树的根,那么,在递归的时候,要先递归右子树,再递归左子树
public class ConstructTreeFromPostIn
{
int offset = 0;
Dictionary<int, int> dict = new Dictionary<int, int>();
public TreeNode BuildTree(int[] inorder, int[] postorder)
{
for (var i = 0; i < inorder.Length; i++)
{
dict[inorder[i]] = i;
}
offset = postorder.Length - 1;
return helper(postorder, inorder, 0, inorder.Length);
}
private TreeNode helper(int[] postorder, int[] inorder, int left, int right)
{
if (left == right)
return null;
var rootVal = postorder[offset];
var root = new TreeNode(rootVal);
var index = dict[rootVal];
offset--;
root.right = helper(postorder, inorder, index + 1, right);
root.left = helper(postorder, inorder, left, index);
return root;
}
}
前序+后序
leetcode题目地址
【思路】依然沿用使用dictionary存储元素下标的思想。只不过在这道题中,不但要存储preorder的下标,还要存postorder的下标,因为在后面的递归函数中,我们需要先通过preorder的当前根节点后第一个元素找到该元素在postorder中的位置,再通过postorder当前根节点之前最后一个元素找到该元素在preorder中的位置。
【不同】在这道题中,递归函数中的left和right参数,指的是preorder中的边界,在上两道题中都是指inorder的边界
public class ConstructTreeFromPrePost
{
Dictionary<int, int> preDict = new Dictionary<int, int>();
Dictionary<int, int> postDict = new Dictionary<int, int>();
public TreeNode ConstructFromPrePost(int[] preorder, int[] postorder)
{
for (var i = 0; i < preorder.Length; i++)
preDict[preorder[i]] = i;
for (var i = 0; i < postorder.Length; i++)
postDict[postorder[i]] = i;
return helper(preorder, postorder, 0, postorder.Length - 1);
}
public TreeNode helper(int[] preorder, int[] postorder, int left, int right)
{
//Console.WriteLine(left+" "+right+" ");
if (left > right)
return null;
var root = new TreeNode(preorder[left]);
if (left == right)
return root;
var r = postDict[root.val] - 1;
if (r < 0 || r >= postorder.Length) return root;
var l = preDict[postorder[r]];
root.left = helper(preorder, postorder, left + 1, l - 1);
root.right = helper(preorder, postorder, l, right);
return root;
}
}
BST
前序
leetcode题目地址
【思路】还是类似上面普通的二叉树的构造,但BST有很好的特性,左边所有节点大于root,右边所有节点小于root,所以解决这道题的话,就先把第一个元素拿出来当root,然后找它后面比它大的元素的位置,然后后面的元素就分成了两半,可以进行递归。
left和right是指当前递归到的子树在数组中的边界。
【小Tip】可以用Binary Search找后面第一个比root大的元素的位置,如果在给定范围内找不到,就返回右边界加一,也就是该元素在给定范围情况下应该插入的位置
public class ConstructBstFromPreorder
{
public TreeNode BstFromPreorder(int[] preorder)
{
return helper(preorder, 0, preorder.Length - 1);
}
public TreeNode helper(int[] preorder, int left, int right)
{
if (left > right) return null;
var root = new TreeNode(preorder[left]);
if (left == right) return root;
var k = Find(preorder, root.val, left + 1, right);
root.left = helper(preorder, left + 1, k - 1);
root.right = helper(preorder, k, right);
return root;
}
public int Find(int[] arr, int target, int lo, int hi)
{
if (lo < 0 || lo >= arr.Length) return lo;
while (lo < hi)
{
int mid = lo + (hi - lo) / 2;
if (target <= arr[mid])
{
hi = mid;
}
else
{
lo = mid + 1;
}
}
if (arr[lo] >= target)
{
return lo;
}
else
{
return hi + 1;
}
}
}
中序
leetcode题目地址
【思路】BST的中序遍历就是一个有序数组,所以从中序构造BST的话就有好多种构造方法,但一般就用最简单最整齐的,每次取中间值作为root的方法。这道题应该是这几种里面比较简单的,就是注意一下记一下思路就行。
public class ConvertSortedArrayToBST
{
public TreeNode SortedArrayToBST(int[] nums)
{
return ToBST(nums, 0, nums.Length - 1);
}
public TreeNode ToBST(int[] nums, int start, int end)
{
if (start > end) return null;
var mid = (start + end) / 2;
var root = new TreeNode(nums[mid]);
root.left = ToBST(nums, start, mid - 1);
root.right = ToBST(nums, mid + 1, end);
return root;
}
}