中序遍历是按照:[左子树 - 根节点 - 右子树] 的顺序访问节点的
我们可以先根据前序遍历序列中的第一个元素,找到根节点,然后在中序遍历序列中,定位到根节点的位置,然后把中序遍历的区间,从根节点的位置,一分为二,左侧是左子树的全部节点,右侧是右子树的全部节点,这样我们就得到了左子树节点的数量,和右子树节点的数量。再把这个信息带回到前序遍历的序列中,就能确定左子树的区间,和右子树的区间。然后我们分别对每个区间,进行递归处理即可。
那么,定义这样一个递归函数:
TreeNode build(int[] preorder, int pl, int pr, int[] inorder, int il, int ir);
这个函数,指定了前序遍历序列的区间起止位置,指定了中序遍历序列的区间起止位置。我们可以保证两个遍历序列的区间长度是相同的。那么递归退出的条件是区间长度为0。
容易写出如下的代码
class Solution {
public TreeNode buildTree(int[] preorder, int[] inorder) {
return build(preorder, 0, preorder.length - 1, inorder, 0, inorder.length - 1);
}
private TreeNode build(int[] preorder, int pl, int pr, int[] inorder, int il, int ir) {
if (pl > pr) return null; // reach end
int rootVal = preorder[pl]; // 前序遍历的第一个元素为根节点
int k = il; // 从中序遍历的序列中, 定位到根节点的下标
while (k <= ir) {
if (inorder[k] == rootVal) break;
k++;
}
int leftLen = k - il; // 左子树的节点数量
int rightLen = ir - k; // 右子树的节点数量
TreeNode root = new TreeNode(rootVal);
root.left = build(preorder, pl + 1, pl + leftLen, inorder, il, k - 1); // 递归构建
root.right = build(preorder, pl + leftLen + 1, pr, inorder, k + 1, ir); // 递归构建
return root;
}
}
哈希表优化
观察可知,性能开销主要在于:在中序遍历序列中定位根节点,我们使用的是遍历的方式。
在极端情况下,比如二叉树退化成一个链表,每个节点都只含有左儿子。此时,前序遍历序列和中序遍历序列,恰好是逆序的关系。每次在中序遍历序列中定位根节点,都要走最远的距离,时间复杂度 O ( n 2 ) O(n^2) O(n2)。
我们可以引入一个哈希表,将中序遍历序列中的值和下标的映射,存储起来,这样在中序遍历序列中定位根节点,只需要 O ( 1 ) O(1) O(1) 的时间复杂度。
class Solution {
Map<Integer, Integer> index = new HashMap<>();
public TreeNode buildTree(int[] preorder, int[] inorder) {
for (int i = 0; i < inorder.length; i++) index.put(inorder[i], i);
return build(preorder, 0, preorder.length - 1, inorder, 0, inorder.length - 1);
}
private TreeNode build(int[] preorder, int pl, int pr, int[] inorder, int il, int ir) {
if (pl > pr) return null;
int rootVal = preorder[pl];
int k = index.get(rootVal);
int leftLen = k - il;
TreeNode root = new TreeNode(rootVal);
root.left = build(preorder, pl + 1, pl + leftLen, inorder, il, k - 1);
root.right = build(preorder, pl + leftLen + 1, pr, inorder, k + 1, ir);
return root;
}
}
引入哈希表,虽然整体的时间复杂度优化为了 O ( n ) O(n) O(n),但是空间复杂度也由原先的 O ( 1 ) O(1) O(1) 上升成了 O ( n ) O(n) O(n)
最优解
下面介绍时间复杂度 O ( n ) O(n) O(n),空间复杂度 O ( 1 ) O(1) O(1) 的解法
根据国外大佬Stephan的思路, 可以通过引入一个stop变量,而不需要引入哈希表,来取得 O ( n ) O(n) O(n) 的时间复杂度,并保持 O ( 1 ) O(1) O(1) 的空间复杂度。
在介绍这种方法之前,先上一个我自己的解法。由于前序遍历总是会先访问根节点,我们可以用一个全局的指针p
,结合前序遍历序列,来获取当前的根节点。每次都通过这个指针p
来建立节点
class Solution {
int p = 0; // 当前根节点的位置
public TreeNode buildTree(int[] preorder, int[] inorder) {
return build(preorder, inorder, 0, inorder.length - 1);
}
// [l, r] 仅仅是 中序遍历的区间范围
private TreeNode build(int[] preorder, int[] inorder, int l, int r) {
if (l > r) return null; // reach end
int rootVal = preorder[p++]; // 取得当前根节点, 并将p后移
int k = l; // 在中序遍历中定位根节点
while (k <= r) {
if (inorder[k] == rootVal) break;
k++;
}
TreeNode root = new TreeNode(rootVal);
// 由于是先递归建立左子树, 那么建立左子树时, 根据前序遍历取到的根节点, 就是左子树的根节点
root.left = build(preorder, inorder, l, k - 1);
root.right = build(preorder, inorder, k + 1, r);
return root;
}
}
接下来介绍Stephan大佬的解法,使用两个指针p
和i
,分别用来指示前序遍历序列当前访问的位置,和中序遍历序列当前访问的位置。并新增一个stop
变量,用来指示中序遍历的边界。原贴 点这里,也可以参考这篇 图解。
class Solution {
int p = 0, i = 0;
public TreeNode buildTree(int[] preorder, int[] inorder) {
// 初始时, 传入一个树中不存在的节点, 作为指示中序遍历的边界
return build(preorder, inorder, Integer.MAX_VALUE);
}
// stop 用来指示中序遍历的停止位置
private TreeNode build(int[] preorder, int[] inorder, int stop) {
if (p == preorder.length) return null; // 所有节点都用完了
if (inorder[i] == stop) { // 停止
i++;
return null;
}
int rootVal = preorder[p++];
TreeNode root = new TreeNode(rootVal);
// 由于对于中序遍历序列中, 是先访问左子树, 再访问根节点, 所以对左子树的构造, stop 设为根节点
root.left = build(preorder, inorder, rootVal); // 构建左子树时, 中序遍历的边界, 遇到根节点时停止
// 对于右子树, 其结束的位置, 和根节点结束的位置相同, 所以将原本的 stop 传入即可
root.right = build(preorder, inorder, stop);
return root;
}
}
其实思路就是手动模拟了递归调用时,栈的情况。
稍加观察,可以发现,我们用于构造二叉树的递归函数,也是按照前序遍历的顺序,进行构造的。即,先访问并创建出了根节点,再递归的建立左子树和右子树。构造的方式,和前序遍历保持一致,即可保证每一次根据指针p
都能取到当前位置的根节点。
整个过程大概就是,先从根节点一头扎下去,一直往左走,走到整棵树最左侧的节点。(其实这也是前序遍历的访问顺序)
当一个节点不存在左儿子时,从这个节点开始的前序遍历序列,和中序遍历序列,第一个访问的都是该节点本身。这样,再对这个节点的左儿子进行递归调用时,会遇到stop,直接返回null。
前序是:根 - 左 - 右 ;中序是:左 - 根 - 右。
当左儿子为空,则前序和中序,第一个访问的节点都是根节点。
如果能较好的理解递归解法的最后一种,那么也就能较为容易的理解迭代的方式,其思路是非常相似的。
迭代法中,需要用一个栈,来手动模拟递归调用产生的栈,和回溯的过程。参考这篇 题解
class Solution {
public TreeNode buildTree(int[] preorder, int[] inorder) {
int p = 0, i = 0; // 同样用2个指针来指示, 前序序列的当前位置, 中序序列的当前位置
Deque<TreeNode> stack = new LinkedList<>();
TreeNode root = new TreeNode(preorder[p++]);
TreeNode cur = root;
stack.push(root);
while (p < preorder.length) {
// 一头扎下去, 扎到整棵树的最左侧节点, 并构建左儿子的关系
while (cur.val != inorder[i]) {
cur.left = new TreeNode(preorder[p++]);
stack.push(cur.left);
cur = cur.left;
}
if (p == preorder.length) break; // 已经结束, 提前break
// 已经到最左侧节点了, 弹栈, 找到第一个需要拥有右儿子的节点
while (!stack.isEmpty() && stack.peek().val == inorder[i]) {
cur = stack.pop();
i++;
}
// 构造其右儿子
cur.right = new TreeNode(preorder[p++]);
stack.push(cur.right);
cur = cur.right;
}
return root;
}
}
迭代的解法,其实更符合计算机的思考方式,而不符合人类大脑的思考模式。
要完整的掌握迭代的解法,需要自己画图,一步一步跟,反复模拟。才能理解整个递归和回溯的过程。反正我今天是一整天只研究了这一道题(哭
根据前序和后序遍历构造二叉树
后序遍历的特点是,根节点最后访问。考虑用递归来做,核心的思路还是找到分界点,能够将数组一分为二。
class Solution {
public TreeNode constructFromPrePost(int[] preorder, int[] postorder) {
return build(preorder, 0, preorder.length - 1, postorder, 0, postorder.length - 1);
}
private TreeNode build(int[] preorder, int il, int ir, int[] postorder, int jl, int jr) {
if (il > ir) return null;
int rootVal = preorder[il];
TreeNode root = new TreeNode(rootVal);
if (il == ir) return root; // 当前preorder是最后一个了
// 获取当前根节点的左子树的根节点
int leftVal = preorder[il + 1];
int k = jl;
while (postorder[k] != leftVal) k++;
// 左子树的根节点, 是后序遍历中左子树最后一个访问的
// 可以根据这个节点, 将后序遍历的数组一分为二
int leftLen = k - jl + 1;
root.left = build(preorder, il + 1, il + leftLen, postorder, jl, k);
root.right = build(preorder, il + leftLen + 1, ir, postorder, k + 1, jr - 1);
return root;
}
}
迭代法:待补充。TODO
另:根据前序和后序,无法唯一确定一颗二叉树。
除了基本的划分做法。仍然可以用105的迭代思路来做(引入stop变量)。具体的做法是:
将中序遍历序列和后序遍历序列逆序看待。
然后我们将原二叉树做一下左右镜像翻转。容易得知:
原先的中序遍历序列的逆序,为镜像翻转后的二叉树的中序遍历;
原先的后序遍历序列的逆序,为镜像翻转后的二叉树的前序遍历。
则我们仍然通过引入一个stop变量来做,但是先构造右子树,再构造左子树(镜像反转)
class Solution {
int p = 0, i = 0;
public TreeNode buildTree(int[] inorder, int[] postorder) {
p = i = postorder.length - 1;
return build(postorder, inorder, Integer.MAX_VALUE);
}
private TreeNode build(int[] postorder, int[] inorder, int stop) {
if (p < 0) return null; // end
if (inorder[i] == stop) {
i--;
return null;
}
TreeNode root = new TreeNode(postorder[p--]);
root.right = build(postorder, inorder, root.val);
root.left = build(postorder, inorder, stop);
return root;
}
}