二叉树的表示

  1. 从字符串生成二叉树
  2. 从前序与中序遍历序列构造二叉树
  3. 从中序与后序遍历序列构造二叉树
  4. 根据前序和后序遍历构造二叉树
  5. 前序遍历构造二叉搜索树
  6. 遍历问题

在 Java 中我们常用如下类表示二叉树节点,然后用根节点表示整棵二叉树。

java 复制代码
public class TreeNode {

    public int val;  // 节点值
    public TreeNode left; // 左子树根节点
    public TreeNode right; // 右子树根节点

}

上面这种表示方法中,根节点是显而易见的,我们也能通过根节点中 left / right 字段获取左/右子树的根节点,用这两个节点分别表示左/右子树。

OI(Olympiad in Informatics,信息学奥林匹克竞赛)中,通常会以如下方式给出二叉树,

输入一个整数 <math xmlns="http://www.w3.org/1998/Math/MathML"> n n </math>n ,表示二叉树中节点个数,编号为 <math xmlns="http://www.w3.org/1998/Math/MathML"> [ 1 , n ] [1, n] </math>[1,n] 。约定 <math xmlns="http://www.w3.org/1998/Math/MathML"> 1 1 </math>1 号节点为二叉树的根节点。然后输入 <math xmlns="http://www.w3.org/1998/Math/MathML"> n n </math>n 行,每行包括两个整数,第 <math xmlns="http://www.w3.org/1998/Math/MathML"> i i </math>i 行表示编号为 <math xmlns="http://www.w3.org/1998/Math/MathML"> i i </math>i 的节点的左子节点和右子节点的编号。如果某个节点没有左子节点,那么对应输行的第一个整数为 <math xmlns="http://www.w3.org/1998/Math/MathML"> 0 0 </math>0 ;如果某个节点没有右子节点,那么对应行的第二个整数为 <math xmlns="http://www.w3.org/1998/Math/MathML"> 0 0 </math>0 。

例如,下图二叉树

会以如下方式输入

json 复制代码
5
2 5
3 4
0 0
0 0
0 0

对于这种方式给出的二叉树,我们也可以用根节点编号表示整棵二叉树,那怎么通过根节点编号获得左/右子树编号呢?

我们可以定义两个一维数组 left[] / right[] 分别存储每个节点左/右子树根节点。代码如下,

java 复制代码
// left[i] / right[i] 分别存储编号为 i 的根节点的左/右子树根节点编号
public int[] left, right;

public void input() {
    Scanner scanner = new Scanner(System.in);
    int n = scanner.nextInt();  // 总节点数
    // 编号为 [1, n], 故数组长度为 n+1
    this.left = new int[n + 1];
    this.right = new int[n + 1];
    for (int i = 1; i <= n; i++) {
        left[i] = scanner.nextInt();
        right[i] = scanner.nextInt();
    }
}


// 后序遍历根节点编号为 r 的二叉树
public void postorder(int r, List<Integer> list){
    if(r == 0){  // 空树
        return;
    }
    postorder(left[r],list);  // ① 后序遍历左子树
    postorder(right[r],list); // ② 后序遍历右子树
    list.add(r); // ③ 访问根节点
}

这种题目通常用来考察二叉树结构,所以每个节点不会用来存值,如果节点需要存储值,我们只需要添加一个 val[] 数组,存储每个节点值即可。

另外,二叉树还有另外一种表示方式 ------ 括号表示法

括号表示法不光可以用来表示二叉树,任何树都可以用括号表示法表示,括号表示法具体如下

  1. 任何树都需要位于一对小括号中;
  2. 如果树为空树,则用括号内无值,即 () 表示空树;
  3. 如果树非空,括号中首先为根节点值,然后是分别用括号表示法表示的左/右子树。值得注意的是,二叉树为有序树,如果左/右子树为空,也要用 () 表示出来,用以区分左/右子树。

例如,对于如下二叉树,我们可以用字符串 (12(7(62()())(9()()))(111()(16()()))) 表示。

括号表示法表示的二叉树,可以用如下代码进行后序遍历

java 复制代码
public void postorder(String tree, List<String> list) {
    if ("()".equals(tree)) {  // 空树
        return;
    }

    // ① 求根节点
    int indexOf = tree.indexOf('(', 1);
    // 例如, (12(7(62()())(9()()))(111()(16()()))), 根节点位于第一个左括号和第二个左括号之间
    String r = tree.substring(1, indexOf); 

    // 剥离根节点, 求左/右子树的括号表示
    tree = tree.substring(indexOf, tree.length() - 1);
    
    int k = 0;
    int left = 0;
    // 求左子树括号表示结束位置
    while (k < tree.length()) {
        if (tree.charAt(k) == '(') {
            left++;
        }
        if (tree.charAt(k) == ')') {
            left--;
        }
        if (left == 0) { // 左/右括号刚好匹配完成, 左子树括号表示结束
            break;
        }
        k++;
    }
    postorder(tree.substring(0, k + 1), list);  // ② 后序遍历左子树 
    postorder(tree.substring(k + 1), list); // ③ 后序遍历右子树
    list.add(r);  // ④ 访问根节点
}

从上面代码可以看出,对于括号表示的二叉树,我们可以通过这种表示方法求出根节点和左/右子树的括号表示法,然后递归解决问题。

根据二叉树的 递归 定义,理论上讲,如果我们想要以某种方式表示二叉树,只需要要能从该表示方式中区分 根节点 , 并能从这种表示方式中获取 左/右子树的这种表示方式 即可。

看下 NOIP 2004 普及组一道题目《 FBI树 》,

我们可以把由 <math xmlns="http://www.w3.org/1998/Math/MathML"> 0 0 </math>0 和 <math xmlns="http://www.w3.org/1998/Math/MathML"> 1 1 </math>1 组成的字符串分为三类:全 <math xmlns="http://www.w3.org/1998/Math/MathML"> 0 0 </math>0 串称为 <math xmlns="http://www.w3.org/1998/Math/MathML"> B B </math>B 串,全 <math xmlns="http://www.w3.org/1998/Math/MathML"> 1 1 </math>1 串称为 <math xmlns="http://www.w3.org/1998/Math/MathML"> I I </math>I 串,既含 <math xmlns="http://www.w3.org/1998/Math/MathML"> 0 0 </math>0 又含 <math xmlns="http://www.w3.org/1998/Math/MathML"> 1 1 </math>1 的串则称为 <math xmlns="http://www.w3.org/1998/Math/MathML"> F F </math>F 串。

<math xmlns="http://www.w3.org/1998/Math/MathML"> F B I FBI </math>FBI 树是一种二叉树,它的结点类型也包括 <math xmlns="http://www.w3.org/1998/Math/MathML"> F F </math>F 结点, <math xmlns="http://www.w3.org/1998/Math/MathML"> B B </math>B 结点和 <math xmlns="http://www.w3.org/1998/Math/MathML"> I I </math>I 结点三种。由一个长度为 <math xmlns="http://www.w3.org/1998/Math/MathML"> 2 N 2N </math>2N 的 <math xmlns="http://www.w3.org/1998/Math/MathML"> 01 01 </math>01 串 <math xmlns="http://www.w3.org/1998/Math/MathML"> S S </math>S 可以构造出一棵 <math xmlns="http://www.w3.org/1998/Math/MathML"> F B I FBI </math>FBI 树 <math xmlns="http://www.w3.org/1998/Math/MathML"> T T </math>T ,递归的构造方法如下:

  1. <math xmlns="http://www.w3.org/1998/Math/MathML"> T T </math>T 的根结点为 <math xmlns="http://www.w3.org/1998/Math/MathML"> R R </math>R ,其类型与串 <math xmlns="http://www.w3.org/1998/Math/MathML"> S S </math>S 的类型相同;

  2. 若串 <math xmlns="http://www.w3.org/1998/Math/MathML"> S S </math>S 的长度大于 <math xmlns="http://www.w3.org/1998/Math/MathML"> 1 1 </math>1 ,将串 <math xmlns="http://www.w3.org/1998/Math/MathML"> S S </math>S 从中间分开,分为等长的左右子串 <math xmlns="http://www.w3.org/1998/Math/MathML"> S 1 S_1 </math>S1和 <math xmlns="http://www.w3.org/1998/Math/MathML"> S 2 S_2 </math>S2 ;由左子串 <math xmlns="http://www.w3.org/1998/Math/MathML"> S 1 S_1 </math>S1 构造R的左子树 <math xmlns="http://www.w3.org/1998/Math/MathML"> T 1 T_1 </math>T1 ,由右子串 <math xmlns="http://www.w3.org/1998/Math/MathML"> S 2 S_2 </math>S2 构造 <math xmlns="http://www.w3.org/1998/Math/MathML"> R R </math>R 的右子树 <math xmlns="http://www.w3.org/1998/Math/MathML"> T 2 T_2 </math>T2 。

现在给定一个长度为 <math xmlns="http://www.w3.org/1998/Math/MathML"> 2 N 2^N </math>2N 的 <math xmlns="http://www.w3.org/1998/Math/MathML"> 01 01 </math>01 串,请用上述构造方法构造出一棵 <math xmlns="http://www.w3.org/1998/Math/MathML"> F B I FBI </math>FBI 树,并输出它的后序遍历序列。

字符串 10001011 所表示的 FBI 树如下

一个二进制串可以用来表示一棵 FBI 树,因为

  1. 可以通过该串确定根节点值 ------ 全 <math xmlns="http://www.w3.org/1998/Math/MathML"> 0 0 </math>0 根节点为 <math xmlns="http://www.w3.org/1998/Math/MathML"> B B </math>B ,全 <math xmlns="http://www.w3.org/1998/Math/MathML"> 1 1 </math>1 根节点为 <math xmlns="http://www.w3.org/1998/Math/MathML"> I I </math>I ,既含 <math xmlns="http://www.w3.org/1998/Math/MathML"> 0 0 </math>0 又含 <math xmlns="http://www.w3.org/1998/Math/MathML"> 1 1 </math>1 根节点为 <math xmlns="http://www.w3.org/1998/Math/MathML"> F F </math>F ;
  2. 将该二进制串一分为二后,又可以用分后的两个二进制串分别表示左/右子树;

因为二进制串直接就可以表示一棵二叉树,所以我们就不需要再构造 FBI 树,直接后序遍历即可,代码如下

java 复制代码
// 确定二机制串 str 表示的 FBI 树根节点值
public char rootVal(String str) {
    // 二进制串中"1"个数
    long count = str.chars().filter(k -> k == '1').count();
    if (count == str.length()) {   // 全"1"
        return 'I';
    } else if (count == 0) {  // 全"0"
        return 'B';
    } else {
        return 'F'; // 即含"1"又含"0"
    }
}

public void postorder(String str, List<Character> list) {
  
    if (str.length() > 1) {
        postorder(str.substring(0, str.length() / 2), list); // ① 后序遍历左子树
        postorder(str.substring(str.length() / 2), list); // ② 后序遍历右子树
    }
    // ③ 访问根节点
    list.add(rootVal(str));
}

另外,可用如下代码层序遍历用二进制串表示的 FBI 树,

java 复制代码
public List<List<Character>> levelOrder(String str) {

    int level = 1; // 层号

    // 层号 → 该层从左至右所有子树
    TreeMap<Integer, List<String>> map = new TreeMap<>();

    List<String> currentLevel = new ArrayList<>(); // 当前层
    currentLevel.add(str); // ① 初始时, 整树

    while (!currentLevel.isEmpty()) {
        map.put(level++, currentLevel);
        List<String> nextLevel = new ArrayList<>();  // 下一层
        for (String strTree : currentLevel) { // ② 通过当前层推演下一层
            if (strTree.length() > 1) {
                nextLevel.add(strTree.substring(0, strTree.length() / 2));
                nextLevel.add(strTree.substring(strTree.length() / 2));
            }
        }

        // ③ 下一层变为当前层, 循环推演下下层
        currentLevel = nextLevel;
    }

    return map.values().stream()
            // Tree → root Value
            .map(k -> k.stream().map(this::rootVal).collect(Collectors.toList()))
            .collect(Collectors.toList());

}

对于二叉树的遍历,还有一个经常出现的问题 ------ 能否通过二叉树三种遍历序列(先序/中序/后续)中的某两种表示/确定一棵二叉树?

解决这类问题关键还是上文所介绍的,如果某两种遍历序列能表示一棵二叉树,那么这两种遍历序列应该满足如下条件

  1. 能从这两种遍历序列中能求出根节点;
  2. 能从这两种遍历中求出左/右子树的这两种遍历序列;

先看下 先序序列中序序列 能否用来表示一棵二叉树

先序遍历原理如下

如上图,先序遍历规则为,先访问根节点,然后先序遍历左子树,最后先序遍历右子树。

所以先序遍历序列应该是由如下几部分构成

①根 ➥ ②"左子树先序序列" ➥ ③"右子树先序序列"

实际先序遍历序列这三部分①/②/③是粘在一起,这里为了展示清晰才将其分开。

先序遍历序列第一个元素肯定是根节点,所以条件一是可以通过先序遍历序列求出的。但是,先序序列中 ②/③ 是粘在一起的,无法剥离出"左子树先序序列"/"右子树先序序列"。

我们再看看中序序列,中序遍历原理如下

中序遍历规则为,先中序遍历左子树,然后访问根节点,最后中序遍历右子树。

所以中序遍历序列应该是由如下几部分构成

①"左子树中序序列" ➥ ②根 ➥ ③"右子树中序序列"

同样,中序遍历序列中这三部分①/②/③是粘在一起。

中序序列中根节点把"左子树中序序列"/"右子树中序序列"分割开了,而根节点是已知的 ------ 先序序列中第一个元素为根节点,这样左/右子树的中序序列便可求。

所以这里有一个重要前提 ------ 二叉树中每个节点的值都不相同。因为,如果节点值相同的话,在中序序列中找根节点就可能出现多个,这样就不能分割中序序列中"左子树中序序列"/"右子树中序序列"。

求出左/右子树中序序列后,便可知左/右子树节点数量 ln / rn ,这样,在先序序列第一个元素往后的 ln 个元素即为"左子树先序序列",后面剩余即为"右子树先序序列"。

综上,如果已知二叉树先序序列与中序序列是可以求出根节点,以及左/右子树的先序序列与中序序列,故二叉树 先序序列与中序序列可以表示二叉树

下图展示了用先序序列 12345 和中序序列 32415 表示二叉树时,绘制该二叉树过程

可用如下代码求用先序和中序序列表示的二叉树的层序序列

java 复制代码
public List<List<Integer>> levelOrder(int[] preorder, int[] inorder) {

    int level = 1; // 层号

    // 层号 → 该层从左至右所有子树
    // 用两个序列表示树, 所以存树时需要存先序和中序
    // 这里用 Map.Entry, C++ 可直接使用 STL Pair 类
    TreeMap<Integer, List<Map.Entry<int[], int[]>>> map = new TreeMap<>();

    List<Map.Entry<int[], int[]>> currentLevel = new ArrayList<>(); // 当前层
    currentLevel.add(new SimpleImmutableEntry<>(preorder, inorder)); // ① 初始时, 整树

    while (!currentLevel.isEmpty()) {
        map.put(level++, currentLevel);
        List<Map.Entry<int[], int[]>> nextLevel = new ArrayList<>();  // 下一层
        for (Map.Entry<int[], int[]> entry : currentLevel) { // ② 通过当前层推演下一层
            int[] pre = entry.getKey();  // 先序
            int[] in = entry.getValue(); // 中序
            
            int left = 0; // 中序中根节点位置
            while (left < in.length && in[left] != pre[0]) {
                left++;
            }
            if (left > 0) {  // 根左边有元素, 左子树存在
                // ③ 左子树入队
                nextLevel.add(new SimpleImmutableEntry<>(Arrays.copyOfRange(pre, 1, left + 1),
                        Arrays.copyOfRange(in, 0, left)));
            }
            if (left != in.length - 1) { // 根不是最后一个元素, 根右边有元素, 右子树存在
                // ③ 右子树入队
                nextLevel.add(new SimpleImmutableEntry<>(Arrays.copyOfRange(pre, left + 1, pre.length),
                        Arrays.copyOfRange(in, left + 1, in.length)));
            }
        }

        // ④ 下一层变为当前层, 循环推演下下层
        currentLevel = nextLevel;
    }

    return map.values().stream()
            // 先序第一个元素为根节点
            .map(k -> k.stream().map(q -> q.getKey()[0]).collect(Collectors.toList()))
            .collect(Collectors.toList());

}

也可以使用如下求用先序和中序序列表示的二叉树的后序序列

java 复制代码
public void postorder(int[] preorder, int[] inorder, List<Integer> list) {

    int left = 0; // 中序中根节点位置
    while (left < inorder.length && inorder[left] != preorder[0]) {
        left++;
    }
    if (left > 0) {  // 根左边有元素, 左子树存在

        // ① 后序遍历左子树
        postorder(Arrays.copyOfRange(preorder, 1, left + 1),
                Arrays.copyOfRange(inorder, 0, left), list);
    }
    if (left != inorder.length - 1) { // 根不是最后一个元素, 根右边有元素, 右子树存在
        // ② 后序遍历右子树
        postorder(Arrays.copyOfRange(preorder, left + 1, preorder.length),
                Arrays.copyOfRange(inorder, left + 1, inorder.length), list);
    }

    // ③访问根节点
    list.add(preorder[0]);
}

当然如果觉得这种表示方式不够直接,也可以转换为 TreeNode 方式表示

java 复制代码
public TreeNode buildTree(int[] preorder, int[] inorder) {

    // ① 转换根节点
    TreeNode r = new TreeNode(preorder[0]);

    int left = 0; // 中序中根节点位置
    while (left < inorder.length && inorder[left] != preorder[0]) {
        left++;
    }
    if (left > 0) {  // 根左边有元素, 左子树存在
        // ② 转换左子树
        r.left = buildTree(Arrays.copyOfRange(preorder, 1, 1 + left), Arrays.copyOfRange(inorder, 0, left));
    }

    if (left != inorder.length - 1) { // 根不是最后一个元素, 根右边有元素, 右子树存在
        // ③ 转换右子树
        r.right = buildTree(Arrays.copyOfRange(preorder, left + 1, preorder.length),
                Arrays.copyOfRange(inorder, left + 1, inorder.length));
    }
    return r;
}

和先序序列与中序序列类似,中序序列与后序序列也可以用于表示/确定一棵二叉树。

后序遍历规则为,先后序遍历左子树,然后后序遍历右子树,最后访问根节点。

所以一棵二叉树树后序遍历序列应该是由如下几部分构成

①"左子树后序序列" ➥ ②"右子树后序序列" ➥ ③根

后序遍历序列中这三部分①/②/③也是粘在一起。

后序序列的最后一个元素为根节点。由于根节点已经确定,同样,我们可以在中序序列中找到根节点,根节点把中序序列分割成"左子树中序序列"与"右子树中序序列",这样左子树与右子树节点数量便可确定,后序序列中"左子树后序序列" /"右子树后序序列"便也可分割开来。

故中序序列与后序序列可用于表示/确定一棵二叉树。

那先序序列与后序序列能表示/确定一棵二叉树吗?

对比一下先序序列与后序序列

①根 ➥ ②"左子树先序序列" ➥ ③"右子树先序序列"

①"左子树后序序列" ➥ ②"右子树后序序列" ➥ ③根

可以发现先序序列第一个元素和后序序列最后一个元素均为根节点,所以根节点是比较容易得到的。但是,也出现另外一个问题,根节点不能像前面两种情况那样把左右子树分割开了。

先序序列/后序序列去除根节点后,变为如下

②"左子树先序序列" ➥ ③"右子树先序序列"

①"左子树后序序列" ➥ ②"右子树后序序列"

如果只有一个子树序列,那么先序序列中②/③就不能确定是哪棵子树的先序序列了,同理,中序序列中①/②也不能确定是哪棵子树的中序序列,就会有两种选择,要么是左子树的,要么是右子树的。

所以, 先序序列与后序序列是不能表示/确定一棵二叉树

例如,如下四棵二叉树的先序序列与后序序列均相同。

但是如果,这棵树中 所有 节点的左右子树都存在,在上面序列序列中分割出左/右子树的先/后序序列也不是不可能。

"左子树先序序列"第一个节点为左子树根节点,其在"左子树后序序列"中位于最后一个节点,所以我们可以在后序序列中找"左子树先序序列"第一个节点,找到后,该节点以及该节点之前部分均为"左子树后序序列",这样左右子树节点数量便可求,左/右子树的先/后序序列便可分割。

现在考虑这样一个问题 ------ 给出一个先序序列和后序序列,问有多少棵二叉树树的先序序列和后序序列为该序列?

通过上面所述,我们知道先序序列和后序序列不能确定二叉树地方在于如果这棵二叉树中某棵子树中只有一个子树序列时,不能确定该序列是其左子树序列还是右子树序列,

如下图,先序序列为 <math xmlns="http://www.w3.org/1998/Math/MathML"> 12345 12345 </math>12345, 后序序列为 <math xmlns="http://www.w3.org/1998/Math/MathML"> 35421 35421 </math>35421,根节点为 <math xmlns="http://www.w3.org/1998/Math/MathML"> 1 1 </math>1,该根节点只有一个子树序列,其可能为 <math xmlns="http://www.w3.org/1998/Math/MathML"> 1 1 </math>1 的左子树的,也可能为 <math xmlns="http://www.w3.org/1998/Math/MathML"> 1 1 </math>1 的右子树的,所以这里就有两种情况。

假设,先序序列 <math xmlns="http://www.w3.org/1998/Math/MathML"> 2345 2345 </math>2345 和中序序列 <math xmlns="http://www.w3.org/1998/Math/MathML"> 3542 3542 </math>3542 所表示的子树为根节点 <math xmlns="http://www.w3.org/1998/Math/MathML"> 1 1 </math>1 的左子树,那么该子树根节点为 <math xmlns="http://www.w3.org/1998/Math/MathML"> 2 2 </math>2。 但是根节点为 2 的左右子树都存在,是可以确定的 ------ 左子树先序/中序为 <math xmlns="http://www.w3.org/1998/Math/MathML"> 3 3 </math>3 / <math xmlns="http://www.w3.org/1998/Math/MathML"> 3 3 </math>3 ,右子树先序/中序为 <math xmlns="http://www.w3.org/1998/Math/MathML"> 45 45 </math>45 / <math xmlns="http://www.w3.org/1998/Math/MathML"> 54 54 </math>54 。

对于先序/中序为 <math xmlns="http://www.w3.org/1998/Math/MathML"> 45 45 </math>45 / <math xmlns="http://www.w3.org/1998/Math/MathML"> 54 54 </math>54 这棵二叉树,根节点为 <math xmlns="http://www.w3.org/1998/Math/MathML"> 4 4 </math>4, 其也只有一个子树序列,又有两种情况。

综上所述,先序序列为 <math xmlns="http://www.w3.org/1998/Math/MathML"> 12345 12345 </math>12345 , 后序序列为 <math xmlns="http://www.w3.org/1998/Math/MathML"> 35421 35421 </math>35421 一共可以表示 <math xmlns="http://www.w3.org/1998/Math/MathML"> 2 × 2 = 4 2\times2=4 </math>2×2=4 棵二叉树 ,即有 <math xmlns="http://www.w3.org/1998/Math/MathML"> 4 4 </math>4 棵二叉树先序序列为 <math xmlns="http://www.w3.org/1998/Math/MathML"> 12345 12345 </math>12345 且后序序列为 <math xmlns="http://www.w3.org/1998/Math/MathML"> 35421 35421 </math>35421。

所以解决该类问题,其实就是找出所有子树中只有一个子树序列数量 <math xmlns="http://www.w3.org/1998/Math/MathML"> n n </math>n,最终答案就为 <math xmlns="http://www.w3.org/1998/Math/MathML"> 2 n 2^n </math>2n。 代码如下,

java 复制代码
int total = 1;  // 方案数, 最开始假设只能表示一棵二叉树

// 确定有多少棵二叉树先序序列为 preorder, 且后序序列为 postorder
public void traverse(int[] preorder, int[] postorder) {
    
    if (preorder.length == 1) {   // 如果只有一个节点, 只有一棵二叉树
        return;
    }
    
    // ① 去除先序/中序中根节点
    preorder = Arrays.copyOfRange(preorder, 1, preorder.length);  // 先序第一个节点为根节点
    postorder = Arrays.copyOfRange(postorder, 0, postorder.length - 1);  // 后序最后一个节点为根节点
    
    int left = 0;  // 子树先序第一个节点为子树根点
    // 后序中查找子树根节点,该根节点为后序最后一个节点, 找到后便能确定子树节点数量
    while (left < postorder.length && postorder[left] != preorder[0]) {
        left++;
    }
    
    // ② 第一个序列为左子树序列, 如果只有一个序列也假设该序列为左子树序列
    traverse(Arrays.copyOfRange(preorder, 0, left + 1),
            Arrays.copyOfRange(postorder, 0, left + 1));

    if (left < postorder.length - 1) {  // ③ 第二个序列存在, 为右子树序列
        traverse(Arrays.copyOfRange(preorder, left + 1, preorder.length),
                Arrays.copyOfRange(postorder, left + 1, postorder.length));
    } else {
        // 只有一个子树序列, 该子树序列可能为左子树, 也可能为右子树
        total <<= 1;
    }
}

再加上层序序列呢,层序序列再加先序/中序/后序序列中那种序列能表示一棵二叉树呢?

这里直接给出答案,中序序列后层序序列可以表示/确定一棵二叉树?

首先层序序列第一个节点为根节点,中序序列为顺序为

①"左子树中序序列" ➥ ②根 ➥ ③"右子树中序序列"

根节点确定后,同样我们可以通过根节点把中序序列中"左子树中序序列"和"右子树中序序列"分割开来,这样左右子树中节点就能确定下来。最后,我们在层序序列中依次找到左/右子树的这些节点,就分别组成了左/右子树的层序序列。

例如,如下二叉树中序序列为 <math xmlns="http://www.w3.org/1998/Math/MathML"> 7 , 4 , 2 , 8 , 10 , 5 , 1 , 3 , 6 , 9 , 10 7,4,2,8,10,5,1,3,6,9,10 </math>7,4,2,8,10,5,1,3,6,9,10 ,层序序列为 <math xmlns="http://www.w3.org/1998/Math/MathML"> 1 , 2 , 3 , 4 , 5 , 6 , 7 , 8 , 9 , 10 , 11 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11 </math>1,2,3,4,5,6,7,8,9,10,11 。

通过根节点 <math xmlns="http://www.w3.org/1998/Math/MathML"> 1 1 </math>1 分割的左/右子树中序序列分别是 <math xmlns="http://www.w3.org/1998/Math/MathML"> 7 , 4 , 2 , 8 , 10 , 5 7,4,2,8,10,5 </math>7,4,2,8,10,5 与 <math xmlns="http://www.w3.org/1998/Math/MathML"> 3 , 6 , 9 , 10 3,6,9,10 </math>3,6,9,10 , 这样就可以知道左子树包含 <math xmlns="http://www.w3.org/1998/Math/MathML"> 7 , 4 , 2 , 8 , 10 , 5 7,4,2,8,10,5 </math>7,4,2,8,10,5 这些节点,右子树包含 <math xmlns="http://www.w3.org/1998/Math/MathML"> 3 , 6 , 9 , 10 3,6,9,10 </math>3,6,9,10 这些节点。在层序序列中保持原顺序依次筛选出左子树所有节点便可得到左子树层序序列 <math xmlns="http://www.w3.org/1998/Math/MathML"> 2 , 4 , 5 , 7 , 8 , 10 2, 4, 5, 7, 8, 10 </math>2,4,5,7,8,10 ,在层序序列中保持原顺序依次筛选出右子树所有节点便可得到右子树层序序列 <math xmlns="http://www.w3.org/1998/Math/MathML"> 3 , 6 , 9 , 11 3, 6, 9, 11 </math>3,6,9,11。

这样根节点可求, 左/右子树的中序序列以及层序序列也可求,所以中序和层序是可以表示/确定一棵二叉树的。

先序序列/后序序列加层序序列是不能唯一表示/确定一棵二叉树的。

中序序列与层序序列表示方式转换成 TreeNode 方式表示参考如下代码

java 复制代码
public TreeNode transfer(int[] inorder, int[] levelorder) {

    
    TreeNode r = new TreeNode(levelorder[0]);  // ① 层序第一个元素为根节点

    if (inorder.length > 1) {
        int k = 0;
        // ② 中序中找根节点
        while (k < inorder.length && inorder[k] != levelorder[0]) {
            k++;
        }
        // 根节点位置在中序序列中不是首位置, 说明根节点之前存在元素, 即左子树中序存在, 也即左子树存在
        if (k > 0) {
            // 中序中根节点之前元素为左子树中序序列
            int[] leftin = Arrays.copyOfRange(inorder, 0, k);
            // 空间换时间, 将左子树节点放在 Set 中, 可以用 O(1) 时间复杂度判断一个节点是否是左子树节点
            Set<Integer> set = Arrays.stream(leftin).boxed().collect(Collectors.toSet());
            // 从层序序列中依次筛出左子树节点, 构成左子树层序序列
            // 如果左子树节点不预处理到 Set 中, 
            // 线性判断层序序列中某元素是否是左子树节点时间复杂度为 O(n), 那么如下操作总时间复杂度为 O(n^2) , 可能会超时(亲测)
            int[] leftlevel = Arrays.stream(levelorder).filter(set::contains)
                    .toArray();
            r.left = transfer(leftin, leftlevel);  // ③ 递归转换左子树
        }
        // 根节点位置在中序序列中不是最后一个元素, 说明根节点之后存在元素, 即右子树中序存在, 也即右子树存在
        if (k != inorder.length - 1) {
            // ④ 中序中根节点之后元素为右子树中序序列
            int[] rightin = Arrays.copyOfRange(inorder, k + 1, inorder.length);
            // 同上
            Set<Integer> set = Arrays.stream(rightin).boxed().collect(Collectors.toSet());
            int[] rightlevel = Arrays.stream(levelorder).filter(set::contains)
                    .toArray();
            r.right = transfer(rightin, rightlevel); // ③ 递归转换右子树
        }
    }

    return r;

}
相关推荐
沐怡旸3 小时前
【算法】【链表】328.奇偶链表--通俗讲解
算法·面试
掘金安东尼6 小时前
Amazon Lambda + API Gateway 实战,无服务器架构入门
算法·架构
码流之上7 小时前
【一看就会一写就废 指间算法】设计电子表格 —— 哈希表、字符串处理
javascript·算法
快手技术9 小时前
快手提出端到端生成式搜索框架 OneSearch,让搜索“一步到位”!
算法
CoovallyAIHub1 天前
中科大DSAI Lab团队多篇论文入选ICCV 2025,推动三维视觉与泛化感知技术突破
深度学习·算法·计算机视觉
NAGNIP1 天前
Serverless 架构下的大模型框架落地实践
算法·架构
moonlifesudo1 天前
半开区间和开区间的两个二分模版
算法
moonlifesudo1 天前
300:最长递增子序列
算法
CoovallyAIHub1 天前
港大&字节重磅发布DanceGRPO:突破视觉生成RLHF瓶颈,多项任务性能提升超180%!
深度学习·算法·计算机视觉
CoovallyAIHub1 天前
英伟达ViPE重磅发布!解决3D感知难题,SLAM+深度学习完美融合(附带数据集下载地址)
深度学习·算法·计算机视觉