(八)数据结构之“树”

140 阅读5分钟

@TOC

树是什么?

  • 一种分层数据的抽象模型。
  • 前端工作中常见的树包括:DOM树、级联选择、树形控件......

在这里插入图片描述

  • JS中没有树,但是可以用Object和Array构建树

在这里插入图片描述

  • 树的常用操作:深度/广度优先遍历、先中后序遍历

什么是深度/广度优先遍历?

  • 深度优先遍历,尽可能深的搜索树的分支 在这里插入图片描述

  • 广度优先遍历:先访问离根节点最近的节点

在这里插入图片描述

深度优先遍历算法口诀

在这里插入图片描述

  • 访问根节点

  • 对根节点的children挨个进行深度优先遍历

const tree = {
    val: 'a',
    children: [
        {
            val: 'b',
            children: [
                {
                    val: 'd',
                    children: [],
                },
                {
                    val: 'e',
                    children: [],
                }
            ],
        },
        {
            val: 'c',
            children: [
                {
                    val: 'f',
                    children: [],
                },
                {
                    val: 'g',
                    children: [],
                }
            ],
        }
    ],
};

const dfs = (root) => {
    console.log(root.val);
    root.children.forEach(dfs);
};

dfs(tree);

广度优先遍历算法口诀

在这里插入图片描述

  • 新建一个队列,把根节点入队。

  • 把队头出队并访问

  • 把队头的children挨个入队

  • 重复第二、三步,直到队列为空

const tree = {
    val: 'a',
    children: [
        {
            val: 'b',
            children: [
                {
                    val: 'd',
                    children: [],
                },
                {
                    val: 'e',
                    children: [],
                }
            ],
        },
        {
            val: 'c',
            children: [
                {
                    val: 'f',
                    children: [],
                },
                {
                    val: 'g',
                    children: [],
                }
            ],
        }
    ],
};

const bfs = (root) => {
    const q = [root];
    while (q.length > 0) {
        const n = q.shift();
        console.log(n.val);
        n.children.forEach(child => {
            q.push(child);
        });
    }
};

bfs(tree);

二叉树的先中后序遍历

二叉树是什么

  • 树中每个节点最多只能有两个子节点
  • 在JS中通常用Object来模拟二叉树

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

const bt = {
    val: 1,
    left: {
        val: 2,
        left: {
            val: 4,
            left: null,
            right: null,
        },
        right: {
            val: 5,
            left: null,
            right: null,
        },
    },
    right: {
        val: 3,
        left: {
            val: 6,
            left: null,
            right: null,
        },
        right: {
            val: 7,
            left: null,
            right: null,
        },
    },
};

module.exports = bt;

先序遍历算法口诀(根 > 左 > 右)

在这里插入图片描述

  • 访问根节点

  • 对根节点的左子树进行先序遍历

  • 对根节点的右子树进行先序遍历

const bt = require('./bt');

const preorder = (root) => {
    if (!root) { return; }
    console.log(root.val);
    preorder(root.left);
    preorder(root.right);
};

// 非递归版
// const preorder = (root) => {
//     if (!root) { return; }
//     const stack = [root];
//     while (stack.length) {
//         const n = stack.pop();
//         console.log(n.val);
//         if (n.right) stack.push(n.right);
//         if (n.left) stack.push(n.left);
//     }
// };

preorder(bt);

中序遍历算法口诀(左 > 根 > 右)

  • 对根节点的左子树进行中序遍历
  • 访问根节点
  • 对根节点的右子树进行中序遍历

在这里插入图片描述

const bt = require('./bt');

const inorder = (root) => {
    if (!root) { return; }
    inorder(root.left);
    console.log(root.val);
    inorder(root.right);
};

// 非递归版
// const inorder = (root) => {
//     if (!root) { return; }
//     const stack = [];
//     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);

后序遍历算法口诀(左 > 右 > 根)

  • 对根节点的左子树进行后序遍历
  • 对根节点的右子树进行后序遍历
  • 访问根节点

在这里插入图片描述

const bt = require('./bt');

const postorder = (root) => {
    if (!root) { return; }
    postorder(root.left);
    postorder(root.right);
    console.log(root.val);
};

// 非递归版,做法:类似先序遍历倒过来
// const postorder = (root) => {
//     if (!root) { return; }
//     const outputStack = [];
//     const stack = [root];
//     while (stack.length) {
//         const n = stack.pop();
//         outputStack.push(n);
//         if (n.left) stack.push(n.left);
//         if (n.right) stack.push(n.right);
//     }
//     while(outputStack.length){
//         const n = outputStack.pop();
//         console.log(n.val);
//     }
// };

postorder(bt);

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

使用深度优先遍历解决 在这里插入图片描述 解题思路 求最大深度,考虑使用深度优先遍历 在深度优先遍历过程中,记录每个节点所在的层级,找出最大的层级即可 解题步骤 新建一个变量,记录最大深度 深度优先遍历整棵树,并记录每个节点的层级,同时不断刷新最大深度这个变量 遍历结束返回最大深度这个变量 在这里插入图片描述 时间复杂度O(n),n是整棵树的节点数 有函数调用堆栈,函数没有执行完变量不能释放,所以空间复杂度为O(最大深度),好的情况是每个都有两个叉,空间复杂度为O(log(n)),最坏的情况是节点数等于最大深度,只在一个叉上不断延续的二叉树,空间复杂度为O(n)

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

使用广度优先遍历解决 在这里插入图片描述

解题思路 求最小深度,考虑使用广度优先遍历 在广度优先遍历过程中,遇到叶子节点,停止遍历,返回节点层级 解题步骤 广度优先遍历整棵树,并记录每个节点的层级 遇到叶子节点,返回节点层级,停止遍历

在这里插入图片描述

时间复杂度O(n),n是整棵树的节点数 空间复杂度为O(n)

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

在这里插入图片描述 解题思路 层序遍历顺序就是广度优先遍历 不过在遍历时候需要记录当前节点所处的层级,方便将其添加到不同的数组中 解题步骤 广度优先遍历二叉树 遍历过程中,记录每个节点的层级,并将其添加到不同的数组中

  • 法一

在这里插入图片描述

  • 法二 在这里插入图片描述 时间复杂度O(n),空间复杂度为O(n)

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

在这里插入图片描述 递归版 在这里插入图片描述 非递归版 在这里插入图片描述 时间复杂度O(n),空间复杂度为O(n)

LeetCode:112.路径总和

在这里插入图片描述 解题思路 在深度优先遍历的过程中,记录当前路径的节点值的和 在叶子节点处,判断当前路径的节点值的和是否等于目标值 解题步骤 深度优先遍历二叉树,在叶子节点处,判断当前路径的节点值的和是否等于目标值,是就返回true 遍历结束,如果没有匹配,就返回false 在这里插入图片描述 时间复杂度O(n),n为树的节点数,空间复杂度为O(树的高度),最坏为O(n),最好为O(log(n))

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

const json = {
    a: { b: { c: 1 } },
    d: [1, 2],
};

const dfs = (n, path) => {
    console.log(n, path);
    Object.keys(n).forEach(k => {
        dfs(n[k], path.concat(k));
    });
};

dfs(json, []);

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

在这里插入图片描述

技术要点

树是一种分层数据的抽象模型,在前端广泛应用 树的常用操作:深度/广度优先遍历、先中后序遍历......

思考题

1、用React或Vue编写一个省市区级联选择 2、用React或Vue编写一个树插件,要求可以将json渲染到页面上即可