根据前序中序还原二叉树

1,241 阅读7分钟

前言

最近突然又找回了在学校刷算法题和学习数据结构的乐趣,在学校未能打好这些基础,所以决定在学习技术框架的时候,也同时学习回顾以前的基本功,这些在面试中特别重要,框架可以学,程序无非就是算法+数据结构,技术跟着官方文档可以快速上手,但基本功是个积累的过程,很多公司看重的恰恰是基本功。好了,废话不多说,先开始今天的主角重建二叉树,也就是根据先序遍历中序遍历还原成一棵完整的二叉树

基本知识介绍

首先我们得了解一下一些有关二叉树的基本知识。二叉树就是如下图所示,也就是每个根结点最多只有两个子结点,这样自上而下形成的一棵树,就是二叉树,我经常搞混的一个点,老是以为左边孩子要大于右边,但实时满足这种性质的是有序二叉树,这里我们只说基本的二叉树。 下面是一棵二叉树

先序遍历(前序遍历)

先序遍历的规则是根左右,所以上面的二叉树前序遍历结果是 6 > 3 > 1 > 11 > 8 > 2 > 5 > 7 > 10 > 13 > 12
解释: 因为当前二叉树的根是6,所以先找到的是6,根据根左右的法则,再找左孩子,找到3,注意这里不是直接就取3了,而是以3为根节点的左子树中再按照根左右的法则去遍历,在3这颗子树中,3为根结点,故6之后是3,根找到了,还有左右没找,这时候再遍历3的左子树,也就是根结点为1的子树,再根据法则,取到1,然后再继续,找到11,11没有子结点了,所以1这颗子树左边也就走完了,再走右找到8,这时候对于3这颗子树来说左边走完了,那么下一个结点是右,也就是找到2,这时候对于6这颗树来说,左边所有的已经走完,再走右边,右边按照法则继续去遍历得到的是5 > 7 > 10 > 13 > 12,所以最后的结果是6 > 3 > 1 > 11 > 8 > 2 > 5 > 7 > 10 > 13 > 12

中序遍历

中序遍历的规则是左根右,所以上面的二叉树中序遍历结果是 11 > 1 > 8 > 3 > 2 > 6 > 7 > 5 > > 13 > 10 > 12,中序遍历的和前序类似,只不过法则换成了左根右

后序遍历

后序遍历的规则是左右根,所以上面的二叉树后序遍历结果是 11 > 8 > 1 > 2 > 3 > 7 > 13 > 12 > 10 > 5 > 6,后序遍历的和前序类似,只不过法则换成了左右根

PS:记忆小技巧,观察根的位置,前序的根在前面,中序的在中间,后序的在最后,左永远在右前面。

题目描述

给出前序遍历中序遍历,还原出一棵完整的二叉树
input:
前序遍历 6 -> 3 > 1 > 11 > 8 > 2 > 5 > 7 > 10 > 13 > 12
中序遍历 11 > 1 > 8 > 3 > 2 > 6 > 7 > 5 > > 13 > 10 > 12
output:

            6            
         /     \         
      3           5      
    /   \       /   \    
  1       2   7       10 
 / \                 / \ 
11  8               13  12

重建二叉树相关思路

根据前序遍历的第一个是根结点的原则,去中序遍历中去找到根节点,记录当前位置,因为中序遍历法则是左根右
1、6 -> 3 > 1 > 11 > 8 > 2 > 5 > 7 > 10 > 13 > 12,第一个是6,所以6是这颗主树的根节点,那么这个时候我们可以拿根结点去中序遍历中找,11 > 1 > 8 > 3 > 2 > 6 > 7 > 5 > 13 > 10 > 12,那么6的左边就是左子树的中序遍历,右边就是右子树的中序遍历。

2、这个时候我们得到左子树的中序遍历11 > 1 > 8 > 3 > 2,那么我们还需要前序遍历才能还原这颗二叉树,那么我们想一下,不管什么遍历,左子树一定会在右子树之前遍历完是不是?,这点可以从我们的法则得出,左在右前面,也就是说,6 -> 3 > 1 > 11 > 8 > 2 > 5 > 7 > 10 > 13 > 12这段前序遍历的右子树遍历结果都集结在末尾,那么这个时候我们要得到左子树的前序遍历是不是就是(根结点的索引位置 + 1,根结点的索引位置 + 左子树的长度),也就是(1,5),最终左子树的前序遍历是3 > 1 > 11 > 8 > 2,剩余的是右子树的前序遍历了。

3、那么我们理一下思路,这时候我们的当前的子问题是不是就是根据前序遍历3 > 1 > 11 > 8 > 2,中序遍历11 > 1 > 8 > 3 > 2,去还原二叉树,有木有发现问题和主问题一样^-^。

4、归纳上面的结果总的来说就是,划分出子树的前序和中序,然后去还原子树,子树的问题中再划分子树的前序和中序,这俄罗斯套娃似的,总之就是我们针对这种问题,也就是子问题和主问题一样的,可以使用递归去解决,关键就在于划分前序和中序出来

代码实现

看主要核心代码即可,其他的只是为了输出成树状,为了好看 ^-^

package com.cj;
import java.util.Arrays;
import java.util.Scanner;
/**
 * 二叉树的重建
 *
 *  根据前序遍历和中序遍历推测出二叉树
 *      6
 *    /  \
 *   3    5
 *  / \  / \
 *  1 2  7 10
 * / \     / \
 * 11 8   13  12
 *
 *  前序遍历  6 -> 3 > 1 > 11 > 8 > 2 > 5 > 7 > 10 > 13 > 12
 *  中序遍历  11 > 1 > 8 > 3 > 2 > 6 > 7 > 5 > > 13 > 10 > 12
 *  后序遍历  11 > 8 > 1 > 2 > 3 > 7 > 13 > 12 > 10 > 5 > 6
 *
 *  推测出二叉树
 */
public class Main {
    //前序遍历存储
    private static Integer []preOrderStringArray;
    //中序遍历存储
    private static Integer []middleOrderStringArray;

    public static void main(String[] args) {

        Scanner sc = new Scanner(System.in);

        System.out.println("请输入二叉树的前序遍历(以逗号隔开):");
        //处理输入
        preOrderStringArray = Arrays.stream(sc.nextLine().split(",")).map(Integer::valueOf).toArray(Integer[]::new);

        System.out.println("请输入二叉树的中序遍历(以逗号隔开):");
        //处理输入
        middleOrderStringArray = Arrays.stream(sc.nextLine().split(",")).map(Integer::valueOf).toArray(Integer[]::new);

        //调用方法获取二叉树
        Node node = getTwoBranchTree(0,preOrderStringArray.length - 1,0,middleOrderStringArray.length - 1);

        //打印二叉树
        show(node);

    }

    /**
     * 核心代码
     * @param preI
     * @param preJ
     * @param middleI
     * @param middleJ
     * @return
     */
    private static Node getTwoBranchTree(int preI,int preJ,int middleI,int middleJ){

        //前序遍历的第一个节点是根节点
        Node root = new Node(preOrderStringArray[preI]);

        //如果当前preI和preJ相等,说明该子树只有一个结点,不需要再遍历左右孩子,直接return
        if(preI == preJ){
            return root;
        }

        //中序遍历根所在的索引位置
        int index = 0;
        //开始分界中序遍历中左右子树范围
        for (int i = middleI ; i < middleJ ; i++){
            if(root.value == middleOrderStringArray[i]){
                index = i;
                break;
            }
        }

        /**
         * 根据范围去递归,循环当前逻辑
            左子树,在前序遍历当中,第一个就是根结点,所以当前根结点的索引是preI,注意index是中序遍历根结点的索引位置
            另外根据前序遍历为(`根结点的索引位置 + 1`,`根结点的索引位置 + 左子树的长度`),中序遍历根结点左边的都是左子树的中序遍历
            所以左子树的长度是(index - middleI)得出前序的索引范围(preI + 1,preI + (index - middleI))
            左子树的中序遍历索引范围是第一个到根结点前一个位置(middleI,index - 1)
          */
        root.left = getTwoBranchTree(preI + 1,preI + (index - middleI),middleI,index - 1);


        /**
         * 根据范围去递归,循环当前逻辑
            右子树,在前序遍历当中,左子树右边就是右子树的前序遍历,所以索引范围是(preI + (index - middleI + 1),preJ)
            中序遍历是根结点右边开始到结束,所以是(index + 1,middleJ)
          */
        root.right = getTwoBranchTree(preI + (index - middleI + 1),preJ,index + 1,middleJ);

        return root;
    }


     // 用于获得树的层数
    private static int getTreeDepth(Node root) {
        return root == null ? 0 : (1 + Math.max(getTreeDepth(root.left), getTreeDepth(root.right)));
    }

    // 将计算的格式写入数组中,便于输出
    private static void writeArray(Node currNode, int rowIndex, int columnIndex, String[][] res, int treeDepth) {
        // 保证输入的树不为空
        if (currNode == null) return;
        // 先将当前节点保存到二维数组中
        res[rowIndex][columnIndex] = String.valueOf(currNode.value);

        // 计算当前位于树的第几层
        int currLevel = ((rowIndex + 1) / 2);
        // 若到了最后一层,则返回
        if (currLevel == treeDepth) return;
        // 计算当前行到下一行,每个元素之间的间隔(下一行的列索引与当前元素的列索引之间的间隔)
        int gap = treeDepth - currLevel - 1;

        // 对左儿子进行判断,若有左儿子,则记录相应的"/"与左儿子的值
        if (currNode.left != null) {
            res[rowIndex + 1][columnIndex - gap] = "/";
            writeArray(currNode.left, rowIndex + 2, columnIndex - gap * 2, res, treeDepth);
        }

        // 对右儿子进行判断,若有右儿子,则记录相应的"\"与右儿子的值
        if (currNode.right != null) {
            res[rowIndex + 1][columnIndex + gap] = "\\";
            writeArray(currNode.right, rowIndex + 2, columnIndex + gap * 2, res, treeDepth);
        }
    }

    //遍历数组进行打印二叉树
    private static void show(Node root) {
        if (root == null) System.out.println("EMPTY!");
        // 得到树的深度
        int treeDepth = getTreeDepth(root);

        // 最后一行的宽度为2的(n - 1)次方乘3,再加1
        // 作为整个二维数组的宽度
        int arrayHeight = treeDepth * 2 - 1;
        int arrayWidth = (2 << (treeDepth - 2)) * 3 + 1;
        // 用一个字符串数组来存储每个位置应显示的元素
        String[][] res = new String[arrayHeight][arrayWidth];
        // 对数组进行初始化,默认为一个空格
        for (int i = 0; i < arrayHeight; i ++) {
            for (int j = 0; j < arrayWidth; j ++) {
                res[i][j] = " ";
            }
        }

        // 从根节点开始,递归处理整个树
        // res[0][(arrayWidth + 1)/ 2] = (char)(root.val + '0');
        writeArray(root, 0, arrayWidth/ 2, res, treeDepth);

        // 此时,已经将所有需要显示的元素储存到了二维数组中,将其拼接并打印即可
        for (String[] line: res) {
            StringBuilder sb = new StringBuilder();
            for (int i = 0; i < line.length; i ++) {
                sb.append(line[i]);
                if (line[i].length() > 1 && i <= line.length - 1) {
                    i += line[i].length() > 4 ? 2: line[i].length() - 1;
                }
            }
            System.out.println(sb.toString());
        }
    }

    //二叉树的结点定义
    static class Node{
        //当前结点的值
        private int value;
        //左孩子
        private Node left;
        //右孩子
        private Node right;

        public Node(int value){
            this.value = value;
        }

        @Override
        public String toString() {
            return "Node{" +
                    "value=" + value +
                    ", left=" + left +
                    ", right=" + right +
                    '}';
        }
    }
}

结语

好久没写文章了,学过的东西还是得总结记录,以后一周一定至少发一篇文章,另外两两遍历都可以推出二叉树,这里只是前序+中序,另外如果有任何错误,欢迎大家指正。
数据结构和算法之路慢慢,吾将上下而求索。