【JavaScript版数据结构与算法面向大厂面试】第八章 树

第八章 树

8.1 树简介

  1. 树:一种分层数据的抽象类型。
  2. 前端工作中常见的树:DOM、树、级联选择、树形控件…
  3. JS中没有树,但是可以用Object和Array构建树
  4. 树的常用操作:深度/广度优先遍历、先中后序遍历

8.2 深度与广度优先遍历

1.深度优先遍历:先访问一个节点下的深层次的节点(即尽可能深的搜索树的分支)

在这里插入图片描述

2.广度优先遍历:先横向遍历同级节点,再遍历下级的同级节点(即先访问离根节点最近的节点)

在这里插入图片描述

  1. 深度优先遍历(其实就是一个递归)

在这里插入图片描述

算法思想:
1.访问根节点
2.对根节点的children 挨个进行深度优先遍历

// 用Object声明一颗树,新建一颗树 
//var tree = {val:值,child[],...}
const tree = {
    val: 'a',//有一个val节点
    children: [
        {
            val: 'b',//有一个val节点
            children: [
                {
                    val: 'd',//有一个val节点
                    children: [],//有一个children数组
                },
                {
                    val: 'e',//有一个val节点

                    children: [],//有一个children数组
                }

            ],//有一个children数组
        },
        {
            val: 'c',//有一个val节点
            children: [
                {   > val: 'f',//有一个val节点
                children: [],//有一个children数组
              },
        {
            val: 'g',//有一个val节点
            children: [],//有一个children数组
        }
    ],//有一个children数组
}
  ],//有一个children数组
    }

const dfs = (root) => {// 新建一个函数,让这个函数接收这个root的根节点
    console.log(root.val); //访问这个根节点
    // root.children.forEach((child) => { //对这个根节点的children挨个进行深度优先遍历
    //     dfs(child)
    // });
    root.children.forEach(dfs);//对这个根节点的children挨个进行深度优先遍历
};

dfs(tree); //把整颗树传进去

4. 广度优先遍历

在这里插入图片描述

  1. 新建一个队列,把根节点入队
  2. 把队头出队并访问
  3. 把队头的 children 挨个入队
  4. 重复第二、第三步,直到队列为空
// 用Object声明一颗树,新建一颗树 
const tree = {
    val: 'a',//有一个val节点
    children: [
        {
            val: 'b',//有一个val节点
            children: [
                {
                    val: 'd',//有一个val节点
                    children: [],//有一个children数组
                },
                {
                    val: 'e',//有一个val节点
                    children: [],//有一个children数组
                }
            ],//有一个children数组
        },
        {
            val: 'c',//有一个val节点
            children: [
                {
                    val: 'f',//有一个val节点
                    children: [],//有一个children数组
                },
                {
                    val: 'g',//有一个val节点
                    children: [],//有一个children数组
                }
            ],//有一个children数组
        }
    ],//有一个children数组
}
const bfs = (root) => {
    const q = [root];  //新建一个队列Q,并把根节点入队
    while (q.length > 0) { // 在队列不为空的情况下,不断地循环第二、三步
        const n = q.shift();//队头出队
        console.log(n.val);  //访问这个队头   
        n.children.forEach(child => { // 把对头的child挨个入队
            q.push(child);//把child加入队列中
        });// 把队头的child挨个入队 
    }
}
bfs(tree); //把整颗树传进去

8.3 二叉树的先中后序遍历

  1. 树的类型:
    1. 多叉树:树中每个节点有多个子节点
    2. 二叉树:树中每个节点最多只能有两个子节点
  2. 二叉树遍历方式
    1. 先序遍历算法口诀(根左右
      1. 访问根节点
      2. 对根节点的左子树进行先序遍历
      3. 对根节点的右子树进行先序遍历
    2. 中序遍历算法口诀(左根右
      1. 对根节点的左子树进行中序遍历
      2. 访问根节点
      3. 对根节点的右子树进行中序遍历
    3. 后序遍历算法口诀(左右根
      1. 对根节点的左子树进行后序遍历
      2. 对根节点的右子树进行后序遍历
      3. 访问根节点

建二叉树存于文件bt.js中

// 用Object声明一颗树,新建一颗树 
const tree = {
    val: 'a',//有一个val节点
    children: [
        {
            val: 'b',//有一个val节点
            children: [
                {
                    val: 'd',//有一个val节点
                    children: [],//有一个children数组
                },
                {
                    val: 'e',//有一个val节点
                    children: [],//有一个children数组
                }
            ],//有一个children数组
        },
        {
            val: 'c',//有一个val节点
            children: [
                {
                    val: 'f',//有一个val节点
                    children: [],//有一个children数组
                },
                {
                    val: 'g',//有一个val节点
                    children: [],//有一个children数组
                }
            ],//有一个children数组
        }
    ],//有一个children数组
}
const bfs = (root) => {
    const q = [root];  //新建一个队列Q,并把根节点入队
    while (q.length > 0) { // 在队列不为空的情况下,不断地循环第二、三步
        const n = q.shift();//队头出队
        console.log(n.val);  //访问这个队头   
        n.children.forEach(child => { // 把对头的child挨个入队
            q.push(child);//把child加入队列中
        });// 把队头的child挨个入队 
    }
}
bfs(tree); //把整颗树传进去

先序遍历算法实现(递归版)

const bt = require('./bt');  //利用require语法导入构建二叉树的JS文件
//写一个先序遍历的方法
const preorder = (root) => {
    if (!root) {return;}// 如果为空则不要了
    console.log(root.val); //访问根节点
    preorder(root.left); //递归访问左子树
    preorder(root.right); //递归访问右子树
};

preorder(bt);  //调用先序遍历的方法,把二叉树丢进去

中序遍历算法实现(递归版)

const bt = require('./bt');  //利用require语法导入二叉树文件
const inorder = (root) => {//写一个中序遍历的方法,参数为root
    if (!root) {// 如果为空则不要了
        return;
    }
    preorder(root.left); //递归访问左子树
    console.log(root.val); //访问根节点
    preorder(root.right); //递归访问右子树
};

inorder(bt); //调用中序遍历的方法,把二叉树丢进去

后序遍历算法实现(递归版)

const bt = require('./bt');  //利用require语法导入二叉树文件
//写一个后序遍历的方法,参数为root
const postorder = (root) => {
    if (!root) { // 根节点如果为空则不要了
        return;
    }
    preorder(root.left); //递归访问左子树
    preorder(root.right); //递归访问右子树
    console.log(root.val); //访问根节点
};

postorder(bt); //调用后序遍历的方法,把二叉树丢进去

8.4 二叉树的先中后序遍历(非递归版)

1. 先序遍历

const bt = require('./bt');  //利用require语法导入二叉树文件

const preorder = (root) => {//写一个先序遍历的方法,参数为root
    if (!root) {// 根节点如果为空则不要了
        return;
    }
    const stack = [root];// 新建一个stack来模拟函数调用堆栈。把root传进栈,代表当前访问的这个节点是根节点

    // 我们知道如果在函数里面调用另外一个函数,则我们要往栈里面再推入一个函数
    // 用一个while循环来
    while (stack.length) {//当stack有值的时候跑起来
        const n = stack.pop(); //把根节点的值弹出来,下面再访问它
        console.log(n.val); //直接访问它
        // 如果根节点存在的话,我们就把它推入函数的调用堆栈中
        //根据栈的后进先出的特性,right要比left先进入栈
        if (n.right) {
            stack.push(n.right);//right进栈
        }
        if (n.left) {
            stack.push(n.left);//left进栈
        }
    }
};
preorder(bt);//调用先序遍历的方法,把二叉树丢进去

2. 中序遍历

const bt = require('./bt');  //利用require语法导入二叉树文件

//写一个中序遍历的方法,参数为root
const inorder = (root) => {
    // 根节点如果为空则不要了
    if (!root) {
        return;
    }
    // 我们知道如果在函数里面调用另外一个函数,则我们要往栈里面再推入一个函数
    const stack = [];// 新建一个stack代表一个函数的调用堆栈。把root传进栈,代表当前访问的这个节点是根节点

    // 第一步要把所有的左子树全部入栈,需要用到一个指针
    let p = root; //新建一个指针
    while (stack.length || p) {
        // 遍历整棵树的左节点,并把所有的左节点都推入栈中
        while (p) {
            stack.push(p); //每遍历一个节点都把它推入栈中
            p = p.left;//在指针有值的情况下
        }

        const n = stack.pop(); // 把最尽头的左节点弹出来
        console.log(n.val);//并访问左节点的值
        p = n.right; // 访问右节点,直接把指针指向右节点
    }
};

inorder(bt);//调用中序遍历的方法,把二叉树丢进去

3. 后序遍历

把先序遍历倒过来就是后序遍历了,但倒过来左右孩子还是不一样,需要改写一下

const bt = require('./bt');  //利用require语法导入二叉树文件

//写一个后序遍历的方法,参数为root
const postorder = (root) => {
    if (!root) {// 根节点如果为空则不要了
        return;
    }

    // 需要用到两个栈,声明两个栈
    const stack = [root];
    const outputStack = []; //这个栈用来倒置
    // 我们知道如果在函数里面调用另外一个函数,则我们要往栈里面再推入一个函数
    // 用一个while循环来
    while (stack.length) {
        const n = stack.pop(); //把根节点的值弹出来,下面再访问它
        outputStack.push(n); //直接把当前根节点推入栈
        // 如果根节点存在的话,我们就把它推入函数的调用堆栈中
        // 因为先序遍历倒序过来和后序遍历的左右孩子的顺序不太对,所心得换一下左右孩子的进栈顺序
        if (n.left) {//在根节点的左孩子有值的情况下
            stack.push(n.left);//left进栈
        }
        if (n.right) {
            /在根节点的右孩子有值的情况下
            stack.push(n.right);//right进栈
        }
        //全部是根节点先入倒置的那个栈
    }

    // 倒序输出
    while (outputStack.length) {
        const n = outputStack.pop();
        console.log(n.val);
    }
};

postorder(bt);//调用后序遍历的方法,把二叉树丢进去

8.5 LeetCode: 104. 二叉树的最大深度

在这里插入图片描述

解题思路:

  1. 求二叉树的最大深度,考虑使用深度优先遍历
  2. 在深度优先遍历过程中,记录每个节点所在的层级,找出最大层级

解题步骤:

  1. 新建变量,记录最大深度
  2. 深度优先遍历整棵树,并记录每个节点的层级,同时不断更新最大深度这个变量

代码实现:
法一:

/**
 * Definition for a binary tree node.
 * function TreeNode(val, left, right) {
 *     this.val = (val===undefined ? 0 : val)
 *     this.left = (left===undefined ? null : left)
 *     this.right = (right===undefined ? null : right)
 * }
 */
/**
 * @param {TreeNode} root
 * @return {number}
 */
var maxDepth = function (root) {
    let res = 0;
    const dfs = (n, l) => {
        if (!n) { return; }
        res = Math.max(res, l);
        dfs(n.left, l + 1);
        dfs(n.right, l + 1);
    };
    dfs(root,1);
    return res;
};

代码解读:

  1. 先对二叉树进行了深度优先遍历:

在这里插入图片描述

  1. 获取每个节点的层级数:

在这里插入图片描述

法二:
代码实现:

/**
 * Definition for a binary tree node.
 * function TreeNode(val, left, right) {
 *     this.val = (val===undefined ? 0 : val)
 *     this.left = (left===undefined ? null : left)
 *     this.right = (right===undefined ? null : right)
 * }
 */
/**
 * @param {TreeNode} root
 * @return {number}
 */
var maxDepth = function (root) {
    var res = 0;
    var dfs = (n, l) => {
        if (!n) { return; }
        if (!n.left && !n.right) {
            res = Math.max(res, l);
        }
        dfs(n.left, l + 1);
        dfs(n.right, l + 1);
    };
    dfs(root, 1);
    return res;
};

代码解读:

判断n是否是叶子节点,如果是则刷新最大的层级数: 如果当前的左节点为空并且右节点也为空,则刷新最大的层级数。

在这里插入图片描述

  1. 时间复杂度:O(n) n是整棵树的节点数,深度遍历循环了n次
  2. 空间复杂度:O(logn-n) 为二叉树的深度,最好深度为logn,最坏的情况深度为n

综上:法一效率更高些

8.6 LeetCode: 111. 二叉树的最小深度

在这里插入图片描述
在这里插入图片描述

在这里插入图片描述

在这里插入图片描述
代码实现:

/**
 * Definition for a binary tree node.
 * function TreeNode(val, left, right) {
 *     this.val = (val===undefined ? 0 : val)
 *     this.left = (left===undefined ? null : left)
 *     this.right = (right===undefined ? null : right)
 * }
 */
/**
 * @param {TreeNode} root
 * @return {number}
 */

// 树的最小深度
var minDepth = function (root) {
    if (!root) { return 0; }
    var q = [[root, 1]];
    while (q.length) {
        var [n, l] = q.shift();
        if (!n.left && !n.right) {
            return l;
        }
        if (n.left) q.push([n.left, l + 1]);
        if (n.right) q.push([n.right, l + 1]);

    }
};

代码解读:

整棵树的广度优先遍历:

在这里插入图片描述

记录访问的每个节点的层级数。

在这里插入图片描述

8.7 LeetCode: 102. 二叉树的层序遍历

在这里插入图片描述
在这里插入图片描述
法一:
代码实现:

/**
 * Definition for a binary tree node.
 * function TreeNode(val, left, right) {
 *     this.val = (val===undefined ? 0 : val)
 *     this.left = (left===undefined ? null : left)
 *     this.right = (right===undefined ? null : right)
 * }
 */
/**
 * @param {TreeNode} root
 * @return {number[][]}
 */
var levelOrder = function (root) {
    if (!root) return [];//如果没有根节点,则返回一人空数组
    const q = [[root,0]];//新建一个根节点和层级数的数组
    const res = [];//新建一个数组,
    while (q.length) {
        const [n,level] = q.shift();
        if (!res[level]) {
            res.push([n.val]);
        } else {
            res[level].push(n.val);
        }
        if (n.left) q.push(n.left,level+1);//记录左节点,和其节点的层级数,为父节点加一
        if (n.right) q.push(n.right,level+1);   
    }
    return res;
};

代码解读:

整棵树的广度优先遍历:

在这里插入图片描述

记录访问的每个节点的层级数。

在这里插入图片描述

法二:
代码实现:

/**
 * Definition for a binary tree node.
 * function TreeNode(val, left, right) {
 *     this.val = (val===undefined ? 0 : val)
 *     this.left = (left===undefined ? null : left)
 *     this.right = (right===undefined ? null : right)
 * }
 */
/**
 * @param {TreeNode} root
 * @return {number[][]}
 */
var levelOrder = function (root) {
    if (!root) return [];
    var q = [root];
    var res = [];
    while (q.length) {
        let len = q.length;
        res.push([]);
        while (len--) {
            var n = q.shift();
            res[res.length - 1].push(n.val);
            if (n.left) q.push(n.left);
            if (n.right) q.push(n.right);
        }
    }
    return res;
};

8.8 LeetCode: 94. 二叉树的中序遍历

在这里插入图片描述

递归版:
代码实现:

/**
 * Definition for a binary tree node.
 * function TreeNode(val, left, right) {
 *     this.val = (val===undefined ? 0 : val)
 *     this.left = (left===undefined ? null : left)
 *     this.right = (right===undefined ? null : right)
 * }
 */
/**
 * @param {TreeNode} root
 * @return {number[]}
 */
// 递归版
var inorderTraversal = function (root) {
    var res = [];
    var rec = (n) => {
        if (!n) return;
        rec(n.left);
        res.push(n.val);
        rec(n.right);
    };
    rec(root);
    return res;
};

迭代版:
代码实现:

/**
 * Definition for a binary tree node.
 * function TreeNode(val, left, right) {
 *     this.val = (val===undefined ? 0 : val)
 *     this.left = (left===undefined ? null : left)
 *     this.right = (right===undefined ? null : right)
 * }
 */
/**
 * @param {TreeNode} root
 * @return {number[]}
 */
// 迭代版
var inorderTraversal = function (root) {
    var res = [];
    var stack = [];
    let p = root;
    while (stack.length || p) {
        while (p) {
            stack.push(p);
            p = p.left;
        }
        var n = stack.pop();
        res.push(n.val);
        p = n.right;
    }
    return res;
};

综上:迭代版的算法比递归版的算法效率要高。

8.9 LeetCode: 112. 路径总和

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

在这里插入图片描述
在这里插入图片描述

代码实现:

/**
 * Definition for a binary tree node.
 * function TreeNode(val, left, right) {
 *     this.val = (val===undefined ? 0 : val)
 *     this.left = (left===undefined ? null : left)
 *     this.right = (right===undefined ? null : right)
 * }
 */
/**
 * @param {TreeNode} root
 * @param {number} targetSum
 * @return {boolean}
 */
// 路径总和
var hasPathSum = function (root, sum) {
    if (!root) return false;
    let res = false;
    var dfs = (n, s) => {
        if (!n.left && !n.right && s === sum) {
            res = true;
        }
        if (n.left) dfs(n.left, s + n.left.val);
        if (n.right) dfs(n.right, s + n.right.val);
    };
    dfs(root,root.val);
    return res;
};

代码解读:

深度遍历二叉树:

在这里插入图片描述

记录每条路径值的总各和:

在这里插入图片描述

判断路径值是否和目标值相同:

在这里插入图片描述

8.10 前端与树:遍历JSON的所有节点

新建一个JSON

在这里插入图片描述

拿到所有节点的值:

在这里插入图片描述

可以知道值所属的节点:

在这里插入图片描述

8.11 前端与树:渲染Antd中的树组件

利用深度优先算法来渲染,在前端中经常用。

请添加图片描述

在这里插入图片描述
在这里插入图片描述

8.12 树总结

在这里插入图片描述

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

@ZGLi

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值