二叉树经典算法套路分析与总结(JavaScript版)

253 阅读3分钟

二叉树在前端的工程日常中不常见,但是多叉树组件——树形控件却经常使用到。所以掌握好二叉树的经典算法,其实多叉树也很容易举一反三,不仅有利于面试,对我们前端的日常开发也可以提供很多优化的思路。
二叉树看似复杂,其实其中蕴含了很多规律,只要掌握了二叉树的特点和规律,理解其中的套路,很多算法题其实都是大同小异。

树的基本知识

1. 树的结构

多叉树的结构

树其实可以看作是一棵棵“单元树”的集合,这个“单元树”由2个元素组成:根节点,子节点集合,而子节点同样也是这样一棵“单元树”:

interface TreeNode<T> {
  value: T;
  children?: Array<TreeNode<T>>;
}

二叉树的结构

而二叉树是树的一种特例,它的节点最多只会有两个子节点,分别把这两个子节点命名为leftright,而leftright同样也是这样一棵“单元二叉树”:

interface BinaryTreeNode<T> {
  value: T;
  left?: BinaryTreeNode<T>;
  right?: BinaryTreeNode<T>;
}

2.树的深度优先遍历(DFS)

什么叫深度优先遍历呢?深度优先遍历要尽可能地搜索树的分支,简单来说,就是如果发现当前节点有子节点,那么继续往下遍历子节点,直到没有子节点为止,也就是叶子节点为止,然后再继续按照这种方式遍历另外一个分支。
因为树可以看作是一棵棵结构相同“单元树”的集合,我们可以采取递归的思想,将同样的算法应用到同样的结构上。 递归,是一种函数设计思路,就是在函数内部调用自己。 深度优先遍历的口诀是:

  1. 访问根节点
  2. 对根节点的子节点挨个进行深度优先遍历
const dfs = (root) => {
  if(!rott) return;
  // 访问根节点
  console.log(root.value);
  // 对子节点同样进行dfs
  root?.children.forEach(dfs);
}

转换为二叉树:

const binaryDfs = (root) => {
  if(!rott) return;
  // 访问根节点
  console.log(root.value);
  // 对子节点同样进行dfs
  binaryDfs(root.left);
  binaryDfs(root.right);
}

3. 树的广度优先遍历(BFS)

什么是广度优先遍历呢?恰好与深度优先遍历相反,广度优先遍历要先访问离根节点最近的子节点,简单来说,就是按照树的高度,从上往下,遍历完同一层的所有节点后,再遍历下一层节点。
也就是说,当我们从根节点从上往下遍历节点时,先接触到的节点要先遍历,满足先进先出的特性,所以考虑使用队列辅助遍历。 广度优先遍历的口诀是:

  1. 新建一个队列,把根节点入队
  2. 把队头出队并访问
  3. 把队头的子节点挨个入队
  4. 重复2,3步,直到队列为空
const bfs = (root) => {
  const q = [root];
  // 每一次while遍历其实就是一层节点的遍历
  while(q.length) {
    // 先进先出
    const n = q.shift();
    console.log(n.value);
    // 子节点挨个入队,下一次while时就优先出队
    n?.children.forEach(child => {
      q.push(child);
    });
  }
}

转换为二叉树:

const binaryBfs = (root) => {
  if(!rott) return;
  const q = [root];
  // 每一次while遍历其实就是一层节点的遍历
  while(q.length) {
    // 先进先出
    const n = q.shift();
    console.log(n.value);
    // 子节点挨个入队,下一次while时就优先出队
    if(n.left) {
      q.push(n.left);
    }
    if(n.right) {
      q.push(n.right);
    }
  }
}

(未完待续...)