如何根据二叉树的遍历顺序生成树的结构

2,660 阅读4分钟

从事前端快三年了,很少用到算法相关的知识,特别是树,但是作为一个有追求的切图仔,不会算法的前端不是好后端,主要还是em。。。你懂的,最近研究了点前端常见的算法问题。

  • 线性结构:数组、链表
  • 常见排序算法(冒泡、插入、选择、快排、归并)推荐这篇文章
  • 树型结构(二叉树、查找二叉树)
  • 贪心算法、动态规划(主要是找零问题、斐波那契爬楼梯、最长公共子序列LCS等)

我觉得其中最难理解的一个就是如何根据二叉树的遍历顺序还原树结构的问题,这也是我一直好奇在leetcode中好奇那些树相关的题目为何给的测试用例都是数组。看完题解后豁然开朗,在此分享一下。原题树根据前序和中序遍历结果还原二叉树,理解了思路后,自己又尝试完成了根据中序和后序生成。

树的两个特点

  • 单根:如果一个节点A指向另一个节点B,仅能通过A找到B
  • 无环:节点的指向不能形成闭环

遍历顺序

如果一颗树每个节点的最大子节点数为2,则该树是二叉树,上图就是一颗二叉树。 树有三种遍历顺序,前序遍历DLR(根=>左=>右,ABDCE)、中序遍历LDR(左=>根=>右,DBCAE)、后序遍历LRD(左=>右=>根,DCBEA)。

三种顺序遍历代码很简单,采用分治法,递归遍历

节点构造函数定义

function Node(value){
    this.value = value;
    this.left = null;
    this.right = null;
}

前序

function DLR(root){
    if(root){
        console.log(root.value);
        DLR(root.left);
        DLR(root.right);
    }
}

中序

function LDR(root){
    if(root){
        LDR(root.left);
        console.log(root.value);
        LDR(root.right);
    }
}

后序

function LRD(root){
    if(root){
        LRD(root.left);
        LRD(root.right);
        console.log(root.value);
    }
}

重点,给定前序和中序的结果,还原生成二叉树的结构

实现代码

实现getTree方法,接收前序遍历dlr和中序ldr两个遍历结果数组。 还原的思想,如果要手动生成一棵树,肯定要先有根节点,然后依次赋值左右两个节点的值,所以这里如何还原二叉树也是一样,首先要在遍历结果中找到根节点,然后递归设置跟节点左右两边的值

    function getTree(dlr,ldr){
        // 异常情况
        if(dlr.length !== ldr.length){
            throw new Error('invalid params')
        }
        // 递归出口
        if(dlr.length === 0){
            return null;
        }
        // 中序dlr的第一个肯定是根节点
        const rootValue = dlr[0];
        const root = new Node(rootValue);
        // 左节点
        const index = ldr.indexOf(rootValue);// 根节点在中序遍历中的索引
        const leftLDR = ldr.slice(0,index);// 根左节点的中序遍历结果
        const leftDLR = dlr.slice(1, leftLDR.length + 1);// 根做节点的前序遍历结果,前序和中序遍历长度肯定一样,取leftLDR.length
        //这里递归添加左节点
        root.left = getTree(leftDLR, leftLDR);
        // 右节点
        const rightLDR = ldr.slice(index + 1);
        const rightDLR = dlr.slice(leftDLR.length + 1);
        root.right = getTree(rightDLR, rightLDR);
        return root;
    }

测试下 LRD(getTree(['A‘,'B‘,'D','C','E'],['D','B','C','A','D'])打印结果确实是DCBEA

核心步骤

关键部分上面加了注释,我在这里卡了很久:先解决自身,再用同样的方法解决左右,主要三个步骤:
1、找到根节点,前序遍历是根=>左=>右的顺序,第一项肯定是根节点(A)
2、构造左节点进入递归的参数条件,在中序结果中找到根节点的位置,根节点位置左边的肯定就是左节点的中序遍历结果(DBC),然后左节点前序数组长度和中序遍历长度肯定相等为3,所以左节点前序结果就是前序的第二项开始取中序的长度(第一项为跟节点)(BDC),这样左节点就获取到了。
3、构造右节点进入递归的参数条件,过程和构造左节点一致。右节点的中序结果就是根节点右边剩下的(E),右节点的前序就是前序去除首项(A)再去除左节点前序的长度3剩下的(E)
4、返回值节点

附赠中序+后序生成方法,思想和上面一样的

   function getTree(ldr, lrd) {
        if (ldr.length === 0) {
            return null;
        }
        const rootValue = lrd[lrd.length - 1];
        const root = new Node(rootValue);
        const index = ldr.indexOf(rootValue);
        const leftLDR = ldr.slice(0, index);
        const leftLRD = lrd.slice(0, leftLDR.length);
        root.left = getTree(leftLDR, leftLRD);
        const rightLDR = ldr.slice(index + 1);
        const rightLRD = lrd.slice(lrd.length - 1 - rightLDR.length, lrd.length - 1);
        root.right = getTree(rightLDR, rightLRD);
        return root;
    }