对不同结构的二叉树进行前序,后序或中序遍历时,得到的结果可以是相同的,例如:
上面三种结构的二叉树在进行前序遍历时,得到的序列都是:{3,4,5},但如果给定两种遍历序列,例如再加上中序遍历序列:{4,3,5}, 那么二叉树的结构就可以唯一确定了,满足这两种遍历序列的二叉树只能是中间那颗二叉树。
于是算法题目是,给定一颗二叉树的两种遍历序列,一种序列是中序遍历,一种序列的前序遍历,要求你根据这两种遍历序列,还原出其对应的二叉树。假设给定两个序列,中序遍历的节点序列为:{1,2,3,4,5,6,7,8,9,10},前序遍历的节点序列为:{6,4,2,1,3,5,9,7,8,10},要求你给出算法,根据这两个遍历序列,构造出下面这颗二叉树:
给定了前序遍历,那么我们可以知道哪些节点是祖先节点,哪些节点是后代节点,例如根据上面的前序遍历序列,我们可以知道的是,节点6是所以节点的祖先节点,节点4,2是节点6的后代节点,然而我们无法确定的是,节点4,2都在节点6的右边还是都在左边,或者节点4在左边,节点2在右边。这个确定我们就必须要参照中序遍历的节点次序。
如何确定节点4是节点6的左孩子还是右孩子呢,我们需要参照下中序遍历序列,如果节点4在中序遍历序列中,出现在节点6的前面,那么节点4是节点6的左孩子,或者节点4在节点6的左子树,反之如果节点4在中序遍历序列中,出现在节点6的右边,那么节点4是节点6的右孩子,或者节点4属于节点6的右子树。
由此我们推出二叉树的构造算法如下:
1, 从前序遍历序列中逐个取出节点值,如果当前二叉树为空,那么就拿当前节点值构造一个二叉树节点作为树的根节点。
2,如果此时二叉树不为空,那么先从根节点开始,在中序遍历序列中查看当前节点处于根节点的左边,还是右边,如果处于左边,表明当前节点属于根节点的左子树,如果当前根节点的左孩子为空,那么就把当前节点当做根节点的左孩子,要不然进入根节点的左子树。如果当前节点在根节点的右边,那么如果根节点的右孩子为空,那么把当前节点作为根节点的右孩子,要不然进入根节点的右子树。
3,在中序遍历序列中,查找当前节点与当前子树节点的相互位置,如果当前节点处于但却子树节点的左边,那么当前节点属于当前子树节点的左子树,如果当前子树节点的左孩子为空,则把当前节点作为当前树节点的左孩子,要不然进入节点的左子树,继续重复步骤3,如果当前节点在中序遍历序列中处于树节点的右边,那么当前节点属于树节点的右子树,如果树节点的右子树为空,那么把当前节点作为树节点的右孩子,要不然进入树节点的右子树,然后重复步骤3.
当前序遍历序列中的每个节点都依照上面算法步骤遍历一遍后,所构成的二叉树就是我们需要的二叉树,以下是相应的代码实现:
import java.util.HashMap;
public class BTreeBuilder {
private HashMap<Integer, Integer> nodeMap = new HashMap<Integer, Integer>();
private TreeNode root = null;
public BTreeBuilder(int[] inorder, int[] preorder) {
for (int i = 0; i < inorder.length; i++) {
nodeMap.put(inorder[i], i);
}
buildTree(preorder);
}
private void buildTree(int[] preorder) {
if (root == null) {
root = new TreeNode(preorder[0]);
}
for (int i = 1; i < preorder.length; i++) {
int val = preorder[i];
TreeNode current = root;
while (true) {
if (nodeMap.get(val) < nodeMap.get(current.val)) {
if (current.left != null) {
current = current.left;
} else {
current.left = new TreeNode(val);
break;
}
} else {
if (current.right != null) {
current = current.right;
} else {
current.right = new TreeNode(val);
break;
}
}
}
}
}
public TreeNode getTreeRoot() {
return root;
}
}
在代码开始时,先遍历中序序列,把每个节点在中序队列中的位置放入到map中,以便后面查找。buildTree对应的就是前面所说的算法步骤,它遍历前序节点队列中每一元素值,然后执行前面所说的算法三步骤,把当前节点值作为一个节点插入到二叉树的合适位置。
最后getTreeRoot返回构建好后的二叉树根节点。我们再看看程序入口处代码:
import java.util.ArrayList;
public class BinaryTree {
public static void main(String[] s) {
int[] inorder = new int[]{1,2,3,4,5,6,7,8,9,10};
int[] preorder= new int[] {6,4,2,1,3,5,9,7,8,10};
BTreeBuilder treeBuilder = new BTreeBuilder(inorder, preorder);
TreeNode root = treeBuilder.getTreeRoot();
PrintTreeList pt = new PrintTreeList(root);
pt.printTree();
}
}
首先程序构建了一个节点中序遍历序列inorder 和前序遍历序列preorder,然后通过类BTreeBuilder来构建对应二叉树,拿到二叉树后,通过类PrintTreeList将二叉树的节点一层一层的打印出来,该类的算法原理我们在以前的课程中有过详细介绍,程序运行后打印的结果为:
6 4 9 2 5 7 10 1 3 8
不难发现,这个层级打印出来的节点序列,跟我们上头的二叉树是一致的,由此可见,我们的算法实现是正确的。
算法需要遍历前序队列中的每个节点,此时复杂度是O(n), 每访问一个节点,我们都需要在当前已经建好的二叉树中进行相应的查找,二叉树查找的算法复杂度是h, 也就是树的高度,对于含有n个节点的二叉树,其高度为lg(n),因此总的算法复杂度为O(n*lg(n)).
在算法实现中,我们需要使用一个map来存储树的节点及位置,因此空间复杂度为O(n).
更详实的讲解和代码调试演示,请参看视频。
更多技术信息,包括操作系统,编译器,面试算法,机器学习,人工智能,请关照我的公众号: