JavaScript版:数据结构之“树”

560 阅读4分钟

一、 树简介

1.1 树是什么?

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

image.png

  • JS中没有树,但是可以用Object 和 Array 构建树
{
    value: 'guangdong',
    label: 'guangdong',
    children: [
       {
            value: 'guangzhou',
            label: 'guangzhou',
       },{
            value: 'shenzhen',
            label: 'shenzhen',
       }
    ]
}

1.2 树的常用操作

  • 深度/广度优先遍历
  • 先中后序遍历

二、 深度与广度优先遍历

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

  • 深度邮件遍历:尽可能深的搜索树的分支。
  • 广度优先遍历:先访问离跟节点最近的节点。 image.png

2.2 深度优先遍历(dfs)算法口诀

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

2.2.1 深度优先遍历(dfs)实现

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(child=>{dfs(child)})
    root.children.forEach(dfs);
};

dfs(tree);

2.3 广度优先遍历(bfs)算法口诀

  • 新建一个队列,把根节点入队
  • 把队头出队并访问
  • 把队头的children挨个入队
  • 重复第二、第三步,知道队列为空

2.3.1 广度优先遍历(bfs)实现

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);

三、 二叉树的先中后序遍历

3.1 二叉树是什么

image.png

  • 树中每个节点最多只有两个子节点
  • 在JS中通常使用Object 来模拟二叉树
{
    val: 1,
    left: {},
    right: {}
}

3.11 创建一个二叉树(bt)

  • 下面的代码展示会用到
// bt.js
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;

3.2 先序遍历(preorder)的算法口诀[根-左-右]

  • 访问根节点
  • 对根节点的子树进行先序遍历
  • 对根节点的子树进行先序遍历 image.png
const bt = require('./bt');

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

preorder(bt);

3.3 中序遍历(inorder)的算法口诀[左-根-右]

  • 对根节点的左子树进行中序遍历
  • 访问根节点
  • 对根节点的右子树进行中序遍历 image.png
const bt = require('./bt');

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

inorder(bt);

3.4 后序遍历(postorder)的算法口诀[左-右-根]

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

image.png

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

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

四、为何二叉树那么重要,而不是三叉树、四叉树呢?

4.1 如何让性能整体最优?

有序结构

  • 数组:查找易,增删难
  • 链表:增删易,查找难

将两者优点结合起来 —— 二叉搜索树 BST :查找易,增删易 —— 可使用二分算法

二叉搜索树 BST

  • 左节点(包括其后代) <= 根节点
  • 右节点(包括其后代) >= 根节点

image.png

4.2 高级二叉树

二叉搜索树 BST ,如果左右不平衡,也无法做到最优。
极端情况下,它就成了链表 —— 这不是我们想要的。

4.2.1 平衡二叉搜索树 BBST :要求树左右尽量平衡

  • 树高度 h 约等于 logn
  • 查找、增删,时间复杂度都等于 O(logn)

4.2.2 红黑树:一种自动平衡的二叉树

  • 节点分 红/黑 两种颜色,通过颜色转换来维持树的平衡
  • 相比于普通平衡二叉树,它维持平衡的效率更高

image.png

4.2.3 B 树:物理上是多叉树,但逻辑上是一个 BST 。

用于高效 I/O ,如关系型数据库就用 B 树来组织数据结构。

image.png

五、堆有什么特点,和二叉树有什么关系

5.1 JS 执行时代码中的变量

  • 值类型 - 存储到栈
  • 引用类型 - 存储到堆

image.png

5.2 堆的特点:

  • 节点的值,总是不大于(或不小于)其父节点的值
  • 完全二叉树

image.png

堆,虽然逻辑上是二叉树,但实际上它使用数组来存储的。

image.png

// 上图是一个堆(从小到大),可以用数组表示
const heap = [-1, 10, 14, 25, 33, 81, 82, 99] // 忽略 0 节点

// 节点关系
const parentIndex = Math.floor(i / 2)
const leftIndex = 2 * i
const rightIndex = 2 * i + 1

5.3 堆的排序规则,没有 BST 那么严格,这就造成了

  • 查询比 BST 慢
  • 增删比 BST 快,维持平衡也更快
  • 但整体复杂度都是 O(logn) 级别,即树的高度

5.4 堆的应用场景

  • 一般使用内存地址(栈中保存了)来查询,不会直接从根节点搜索
  • 堆的物理结构是数组,所以查询复杂度就是 O(1)

5.5 总结

  • 物理结构是数组(空间更小),逻辑结构是二叉树(操作更快)
  • 适用于“堆栈”结构

5.6 答案

  • 二叉树,可以充分利用二分法
  • 二叉树可以同时规避数字和链表的缺点
  • 引申到 BST BBST 等其他扩展结构

5.7 划重点

  • 二分法的神奇力量
  • 各个高级数据结构的存在价值、设计初衷
  • 数据结构是基本功能