Contents

TS 实现 BinaryTree(二叉树)与其经典算法

TS 实现 BinaryTree(二叉树)与其经典算法

最近金三银四的面试潮,想必大家都想在面试中拿到一个好的 offer,但是面试的时候,面试官问到树的遍历或者与之相关一些经典算法,你会怎么回答呢?如果你不会,那么这篇文章就是为你准备的,本文将会带你了解树的基本概念、树的遍历以及树的一些经典算法,希望能帮助到你。

Tree 的基本概念

先和大家聊一些基本的概念:

树是数据结构中比较重要也是比较难理解的一类存储结构,它的结构是一种非线性存储结构,存储的是具有一对多关系的数据元素的集合。

示例 1

     1
   /   \
  2     3
 / \   / \
4   5 6   7

将具有一对多关系的集合中的数据元素按照示例的形式进行存储,整个存储形状在逻辑结构上看,类似于实际生活中倒着的树,所以称这种存储结构为"树型"存储结构。

节点

使用树结构存储的每一个数据元素都被称为节点,树节点同 链表节点 类似 (链式存储结构),也是由两大部分组成:一个是用于存储数据的元素域,一个是用于存储指针信息的链接域

每一个非空树都有且只有一个被称为根的节点,只要一个节点不存在父节点,那么它就是该树的根节点,而任何没有子节点的节点称为叶节点

用 TS 实现:

/**
 * 树节点(二叉树)
 *
 * 树节点同链表节点类似, 也是由两大部分组成
 * 一个是用于存储数据的 <元素域>
 * 一个是用于存储指针信息的 <链接域>
 */
export interface TreeNode<Data = number> {
  // -- 元素域 --
  /** 保存的数据 */
  val: Data;

  // -- 链接域 --
  /** 指向左边的子元素 */
  left: TreeNode<Data> | null;
  /** 指向右边的子元素 */
  right: TreeNode<Data> | null;
}

export class TreeNode<Data = number> {
  constructor(public val: Data, public left: TreeNode<Data> | null = null, public right: TreeNode<Data> | null = null) {}
}

节点的度和层次

对于一个节点,拥有的子树数量 (节点有多少分支) 称为节点的度 (Degree)。如示例 1 中,每个节点的子节点都是 2 个即为 2 度。二叉树就是一颗只包含的 0、1、2 度节点组成的树

而一棵树的深度是树中节点所在的最大的层次,也就是最深处的叶节点所处的位置。如示例中,最深处的叶节点的深度为 3,次树的深度亦为 3。

有序和无序

如果树中节点的子树从左到右看,谁在左边,谁在右边,是有规定的,这棵树称为有序树,反之称为无序树。在有序树中,一个节点最左边的子树称为"第一个孩子",最右边的称为"最后一个孩子"。

二叉树

简单地理解,满足以下两个条件的树就是二叉树:

  1. 有序树。
  2. 只包含的 0、1、2 度节点。

二叉树 示例

     1
   /   \
  2     3
 / \   /
4   5 6

二叉树的性质

从其特殊的结构我们可以总结出以下几个性质:

  1. 二叉树中,第 i 层最多有 $2^{(i - 1)}$ 个结点, 比如第 5 层最多能有 2 ** (5 - 1) = 16 个结点。
  2. 如果二叉树的深度为 K,那么此二叉树最多有 $2^K-1$ 个结点,比如深度为 4 的二叉树最多有 2 ** 4 - 1 = 15 个结点。
  3. 二叉树中,叶结点数(即度为 0 的节点)为 n0,度为 2 的结点数为 n2,则 n0 = n2 + 1

满二叉树

当二叉树中除了终端结点,每个结点的度都为 2,则此二叉树称为满二叉树。

示例 1 即为满二叉树

作为特殊的二叉树,满二叉树还具有以下性质:

  1. 满二叉树中第 i 层的节点数为 $2^{(i - 1)}$ 个。
  2. 深度为 K 的满二叉树必有 $2^K-1$ 个节点。叶节点数为 $2^{(K - 1)}$。
  3. 满二叉树中不存在度为 1 的节点,每一个分支点中都是两棵深度相同的子树,且叶子节点都在最底层。
  4. 具有 n 个节点的满二叉树的深度为 $\log_2{(n+1)}$。

二叉树的遍历

说了这么多还没给 xdm 看多少代码,不多废话了,直接上代码:

再废话一句 😁,二叉树的遍历是有技巧的,只要我们掌握一些递归的思想,就能很快的写各种姿势的遍历啦。

  1. 前序遍历,对应的 LeetCode 地址:144. 二叉树的前序遍历 - 力扣(LeetCode)Ï
/**
 * 二叉树的前序遍历
 * @param root 根节点
 */
export function preorderTraversal(root: TreeNode | null) {
  let res: number[] = [];
  if (root) {
    // 在遍历完所有节点之前 提交 val 就是后序遍历
    res.push(root.val);
    if (root.left) {
      res = res.concat(preorderTraversal(root.left));
    }
    if (root.right) {
      res = res.concat(preorderTraversal(root.right));
    }
  }
  return res;
}
  1. 中序遍历,94. 二叉树的中序遍历 - 力扣(LeetCode)
/**
 * 二叉树的中序遍历
 * @param root 根节点
 */
export function inorderTraversal(root: TreeNode | null) {
  let res: number[] = [];
  if (root) {
    if (root.left) {
      res = res.concat(inorderTraversal(root.left));
    }
    // 在遍历 right 节点之前 提交 val 就是中序遍历
    res.push(root.val);
    if (root.right) {
      res = res.concat(inorderTraversal(root.right));
    }
  }
  return res;
}
  1. 后序遍历,145. 二叉树的后序遍历 - 力扣(LeetCode)
/**
 * 二叉树的后序遍历
 * @param root 根节点
 */
export function postorderTraversal(root: TreeNode | null) {
  let res: number[] = [];
  if (root) {
    if (root.left) {
      res = res.concat(postorderTraversal(root.left));
    }
    if (root.right) {
      res = res.concat(postorderTraversal(root.right));
    }
    // 在遍历完所有节点之后 提交 val 就是后序遍历
    res.push(root.val);
  }
  return res;
}
  1. 层序遍历,层序遍历需要我们了解一些多源 BFS 的思路,102. 二叉树的层序遍历 - 力扣(LeetCode)
/**
 * 二叉树的层序遍历
 * @param root 根节点
 */
export function levelOrder(root: TreeNode | null): number[][] {
  // 最终结果的二维数组
  const res: number[][] = [];
  if (!root) return res;
  // 暂存节点的队列, 也就是当前层的队列
  const q: TreeNode[] = [root]; // 初始化时先将 root 添加进队列中
  while (q.length) {
    // 当前层的值数组
    const curVal: number[] = [];
    // 遍历当前层, 因为会动态操作 q 队列, 所以需要使用 for 循环
    const { length } = q;
    for (let i = 0; i < length; i++) {
      // 此处不可能存在 q.shift() 返回 undefined 的情况
      const cur = q.shift() as TreeNode;
      // 提交当前节点的值
      curVal.push(cur.val);
      // 先提交left并使用使用shift即为从左到右的提交顺序
      if (cur.left) q.push(cur.left);
      if (cur.right) q.push(cur.right);
    }
    res.push(curVal);
  }
  return res;
}

二叉树的一些经典算法

没有实例的数据结构都是空洞且乏味的。

  1. 二叉树的最大深度

给定一个二叉树, 找出其最大深度。 二叉树的深度为根节点到最远叶子节点的最长路径上的节点数。 同理可得计算"最深 DOM 节点树的深度"的实现。

LeetCode 地址:104. 二叉树的最大深度 - 力扣(LeetCode)

1.1 用递归实现

/**
 * 求树的最大深度, 使用内部递归的方式
 * @param root 根节点
 */
export function maxDepthUseTraverse(root: TreeNode | null): number {
  let res = 0;
  let depth = 0;
  // 内部遍历框架
  function traverse(node: TreeNode | null) {
    if (!node) {
      // 更新最大深度
      res = Math.max(depth, res);
      return;
    }
    // 入栈
    depth++;
    traverse(node.left);
    traverse(node.right);
    // 出栈
    depth--;
  }
  traverse(root);
  return res;
}

1.2 解决子问题方式实现

/**
 * 求树的最大深度, 使用分解问题成计算左右子树高度的方式
 * @param root 根节点
 */
export function maxDepthUseDecomposition(root: TreeNode | null): number {
  if (!root) {
    return 0;
  }
  // 获取左子树的最大深度
  const leftMaxDepth = maxDepthUseDecomposition(root.left);
  // 获取右子树的最大深度
  const rightMaxDepth = maxDepthUseDecomposition(root.right);
  // 比较它们并加上根节点
  return Math.max(leftMaxDepth, rightMaxDepth) + 1;
}

PS: 计算最深 DOM 节点树的深度

/**
 * 计算最深 DOM 节点树的深度
 * @param root 根节点
 */
export function maxDepthForElement(root: Element): number {
  if (!root.children.length) {
    return 0;
  }
  // 比较它们并加上根节点
  return Math.max(...[...root.children].map(children => maxDepthForElement(children))) + 1;
}
  1. 翻转二叉树

给你一棵二叉树的根节点 root,翻转这棵二叉树,并返回其根节点。

详细参见: 226. 翻转二叉树 - 力扣(LeetCode)

/**
 * 226. 翻转二叉树
 * @param root 根结点
 */
export function invertBinaryTree(root: TreeNode | null): TreeNode | null {
  // 思路: BFS + 层序遍历

  if (!root) return null;

  // 层序遍历模版
  const stack: TreeNode[] = [root];
  while (stack.length) {
    const node = stack.pop() as TreeNode;
    const temp = node.right;
    // 交换节点
    node.right = node.left;
    node.left = temp;
    // 入栈
    if (node.left) stack.push(node.left);
    if (node.right) stack.push(node.right);
  }
  return root;
}
  1. 合并二叉树

给你两棵二叉树:root1root2 合并它们,并返回合并后的二叉树。

详细参见: 617. 合并二叉树 - 力扣(LeetCode)

/**
 * 617. 合并二叉树
 * @param root1 根节点1
 * @param root2 根节点2
 */
export function mergeTwoBinaryTrees(root1: TreeNode | null, root2: TreeNode | null): TreeNode | null {
  // 思路: DFS
  // 取root1为基准合并root2

  // 边界
  if (!root1) return root2;
  if (!root2) return root1;

  const stack: [TreeNode, TreeNode][] = [[root1, root2]];
  while (stack.length) {
    const [r1, r2] = stack.pop() as [TreeNode, TreeNode];
    r1.val += r2.val;
    // 入栈
    if (r1.left && r2.left) {
      stack.push([r1.left, r2.left]);
    }
    if (r1.right && r2.right) {
      stack.push([r1.right, r2.right]);
    }
    // 插入
    if (!r1.left) {
      r1.left = r2.left;
    }
    if (!r1.right) {
      r1.right = r2.right;
    }
  }
  return root1;
}

总结

慌慌张张,匆匆忙忙,为何生活总是这样~ 现在是 4 月末了,金三银四的浪潮只剩尾巴了,赶紧抓紧时间一起学习吧,共勉!

相关链接