二叉树 (Binary-Tree) 及其经典问题

536 阅读5分钟

「这是我参与2022首次更文挑战的第1天,活动详情查看:2022首次更文挑战

什么是树 🌲

  • 树是一种分层数据的抽象模型

    • 现实生活中最常见的树的例子是家谱或是公司的组织架构图
  • 一个树结构包含一系列存在父子关系的节点

    • 每个节点都有一个父节点(除了顶部的第一个节点)以及零个或多个子节点
  • 下面为二叉树的示例:

  • 用代码来表示上述 树节点

// 链表节点
class ListNode {
  val: number
  next: ListNode | null
  
  constructor(val?: number, next?: ListNode | null) {
    this.val = (val === undefined ? 0 : val)
    this.next = (next === undefined ? null : next)
  }
}


// 树节点
class TreeNode {
  val: number
  child: TreeNode[] | null

  constructor(val?: number, child?: TreeNode[] | null) {
    this.val = (val === undefined ? 0 : val)
    this.child = (child === undefined ? null : left)
  }
}
  • 可以看到,链表就是一种特殊的树结构
    • 如图中:1->4->7->null
  • 树的有一个重要属性叫 ,指的就是每个节点的子节点个数
    • 图中,1 号节点的度为 3,2号节点的度为 1,3 号节点的度为 0,4 号节点的度为 2

二叉树

  • 二叉树的特性
    • 每个节点的度最多为 2
    • 度为 0 的节点比度为 2 的节点数量上多 1 个

证明二叉树特点:

  • 1、很明显,n 个节点的树,有 n-1 条边
  • 2、度为 0 的节点记为 n0,度为 1 的节点记为 n1,度为 2 的节点记为 n2,所以 n0 + n1 + n2 = n
  • 3、树的所有边的数量为 n0 * 0 + n1 * 1 + n2 * 2 = n - 1
  • 4、n0 + n1 + n2 - 1 = n1 + 2 * n2
  • 5、n0 = n2 + 1

二叉树的三种重要遍历

  • 前序遍历

    • 根 -> 左子树 -> 右子树
  • 中序遍历

    • 左子树 -> 根 -> 右子树
  • 后序遍历

    • 左子树 -> 右子树 -> 根
  • 有个很有意思的特性

    • 通过中序遍历的结果和前序/后序遍历的结果,可以还原二叉树
  • 当然还有其他遍历方式,如层序遍历等

完全二叉树

  • complete binary tree
  • 只有在树的最后一层的右侧缺少节点的这类二叉树,才叫做完全二叉树
  • 特性:
    • 树的根节点以 1 为初始编号,那么编号为 i 的子节点:
      • 左孩子节点的编号为:2 * 1
      • 右孩子节点的编号为:2 * i + 1
      • 完全二叉树可以节省用于存储 的空间,因为通过节点编号可以计算出其子节点的编号
  • 可以用连续空间进行存储,如数组
  • 下面为完全二叉树的示例:

满二叉树

  • full binary tree
  • 没有 度 为 1 的节点的二叉树
  • 下面为满二叉树的示例:

完美二叉树

  • perfect binary tree
  • 每一层节点都满了的二叉树
  • 下面为完美二叉树的示例:

关于树结构的深入理解

  • 节点代表的是集合,边代表的是节点之间的关系
    • 父节点就是全集,各个子节点就是互不相交的子集
    • 所有子节点代表的子集,加起来就等于,父节点代表的全集
    • 集合涉及到的最基础的问题就是查找,而树结构一般情况下,是适用于各种场景下的查找操作

二叉树的作用

是理解高级数据结构的基础

是练习递归技巧的最佳选择

  • 设计/理解递归程序:
  1. 赋予递归函数一个明确的意义
  2. 思考边界边界条件
  3. 实现递归过程
  4. 数学归纳法,也叫结构归纳法
    1. 确定 k0 是正确的,也就是边界条件正确
    2. 假设 k(i) 正确,能推导 k(i + 1) 也正确
  • 举个例子 🌰,求斐波那契数列中的第 n 个数字
    1. 函数意义:第 n 项斐波那契数列的数字
    2. 边界条件:n 为 1 或者 n 为 2 时,直接返回 n
    3. 递归过程:函数返回 f(n - 1) + f(n - 2)
    4. 数学归纳法证明
      1. 确定 k(0) 是正确的,也就是输入 边界条件 得出的结果是正确的
      2. 假设 k(n-1) 与 k(n-2) 都是正确的,可以推导出 k(n) = k(n-1) + k(n-2) 是正确的
      3. 所以下面的递归程序是正确的
const fb = (n) => {
  if(n <= 2) return n;
  
  return fb(n - 1) + fb(n - 2)
}
  

可以节省存储空间

  • 使用左孩子右兄弟表示法可以节省存储空间

  • 左孩子右兄弟表示法,指的是将一颗树,转换为二叉树的形式

    • 该树的根节点不变
    • 树中节点的左孩子为原树的当前节点的左孩子
    • 树中节点的右孩子为原树的当前节点的兄弟节点
  • 下图左边是一颗 多叉树,右边是该树的左孩子右兄弟表示法表示的二叉树

  • 上述方法为什么可以节省树的存储空间?

    • 上图两颗树

      • 假设左边树是三叉树,每个节点拥有 3 个指针域,且该树有 6 个节点,5 条边
        • 由于不是每个节点的每个指针域都是被使用的
        • 计算可得左树总共浪费的指针域的数量为:6 * 3 - 5 = 13
      • 右边树为二叉树,每个节点拥有 2 个指针域,且该树有 6 个节点,5 条边
        • 计算可得右树总共浪费的指针域的数量为:6 * 2 - 5 = 7
      • 所以,上述右树相比于左树,是节约了存储空间的
    • 一个有 n 个节点的 k 叉树

      • 其指针域总数量为 n * k
      • 其边的数量为 n - 1,也就是有 n - 1 个指针域被使用
      • 浪费的指针域数量为:n * k - (n - 1) = (k -1) * n + 1

最后

  • 二叉树的分享就到这里了,欢迎大家在评论区里面讨论自己的理解 👏。
  • 如果觉得文章写的不错的话,希望大家不要吝惜点赞,大家的鼓励是我分享的最大动力🥰