小五的算法系列 - Hello,树先生

·  阅读 1185
小五的算法系列 - Hello,树先生

Hello, 各位勇敢的小伙伴, 大家好, 我是你们的嘴强王者小五, 身体健康, 脑子没病.

本人有丰富的脱发技巧, 能让你一跃成为资深大咖.

一看就会一写就废是本人的主旨, 菜到抠脚是本人的特点, 卑微中透着一丝丝刚强, 傻人有傻福是对我最大的安慰.

欢迎来到小五算法系列Hello,树先生.

前言

此系列文章以《算法图解》和《学习JavaScript算法》两书为核心,其余资料为辅助,并佐以笔者愚见所成。力求以简单、趣味的语言带大家领略这算法世界的奇妙。

tree2.jpg

树对于前端而言简直无处不在,DOM树、CSSOM树、级联选择器、嵌套路由等等;其特殊形态二叉树也在算法的面试中占据了非常重要的一环;接下来我们就分别进入这两所奇特的殿堂,来探寻他们的故事。

建议前置阅读:深度优先遍历与广度优先遍历

前端与树

DOM树

<html>
  <head>
    <title>标题</title>
    <style>...</style>
  </head>
  <body>
    <h1>Hello world !</h1>
    <div>
      <p>内容1</p>
      <p>内容2</p>
    </div>
    <ul>
      <li></li>
      <li></li>
      <li></li>
    </ul>
  </body>
</html>
复制代码

树形结构如下:

tree3.png

CSSOM树

#div1 .c .d {}
.f .c .d {}
.a .c .e {}
#div1 .f {}
.c .d {}
复制代码

树形结构如下:

tree4.png

可以看出,css是从右向左进行解析的

  • 减少嵌套,降低选择器的深度有助于css的查找

  • 通配符、标签选择器也会逐层向上查找,建议采用类选择器代替

特殊的树 -- 二叉树

各个节点的度不超过2的树即为二叉树

tree1.png

为了便于阅读,这里列举一些树的名词解释(不要细化概念,知道大体意思即可,建议扫读):

根节点

  • 图中 root 节点

  • 树顶端的节点称为根节点,其没有父节点;

父节点

  • 图中 A 为 A1 的父节点

  • 若一个节点含有子节点,则这个节点称为其子节点的父节点;

子节点

  • 图中 A1 为 A 的子节点

  • 若一个节点含有父节点,则这个节点为其父节点的子节点(概念中子节点是除根节点和叶子节点外的节点,本文不做区分);

兄弟节点

  • 图中 A1、A2 互为兄弟节点

  • 具有相同父节点的节点称为兄弟节点;

叶子节点

  • 图中 A1、A2、B1、B2 为叶子节点

  • 没有子节点的节点(度为0的节点)即为叶子节点;

子树

  • 图中 A、A1、A2 为左子树,B、B1、B2 为右子树

  • 子节点及其后代组成的树即为子树,二叉树中左侧的子树为左子树,右侧的子树为右子树;

  • 图中 root 节点的度为2,A1 节点的度为0

  • 一个节点含有的子树的个数称为该节点的度;一颗树中,最大的节点的度为该树的度;

  • 图中树为3层结构

  • 从根节点开始,根节点为第一层,其子节点为第二层,以此类推;

深度、高度

  • 根深叶高

  • 节点到根的距离为深度,节点到叶的距离为高度;

树结构定义

下文的树均沿用该结构

interface TreeNode {
  val: String | Number,
  left: TreeNode,
}
复制代码

先序遍历

遍历顺序 -> 根左右

tree5.png

先序遍历结果如下:A -> B -> D -> E -> C -> G -> F

🌲 递归解法:

按照其遍历顺序 根左右 递归即可;

  • 推入当前节点的值

  • 递归调用左子树

  • 递归调用右子树

代码如下:

const preorder = root => {
  if (!root) return [];
  
  const result = [];
  const dfs = (root) => {
    if (!root) return;
    
    result.push(root.val);
    dfs(root.left);
    dfs(root.right);
  }
  dfs(root);

  return result;
};
复制代码

🌲 迭代解法:

我们看这个遍历过程:

  • A -> B -> D

  • 回退到 B

  • B -> E

  • 回退到 B

  • 回退到 A

  • A -> C -> G

  • 回退到 C

  • C -> F

看过上一篇文章的朋友应该知道,这就是一个深度优先遍历的过程,而深度优先遍历的本质是栈

那这个问题就转变为了:

  • A入栈

  • A出栈

  • C、B入栈

  • B出栈

  • E、D入栈

  • D出栈

  • E出栈

  • C出栈

  • F、G入栈

  • G出栈

  • F出栈

代码实现如下:

const preorder = root => {
  if (!root) return [];
  
  const stack = [];
  const result = [];
  stack.push(root);

  while (stack.length) {
    const top = stack.pop();
    result.push(top.val);
    top.right && stack.push(top.right);
    top.left && stack.push(top.left);
  }

  return result;
};
复制代码

后序遍历

遍历顺序 -> 左右根

tree6.png

后序遍历结果如下:D -> E -> B -> G -> F -> C -> A

🌲 递归解法:

按照其遍历顺序 左右根 递归即可,代码如下:

const postorder = root => {
  if (!root) return [];

  const result = [];
  const dfs = (root) => {
    if (!root) return;

    root.left && dfs(root.left);
    root.right && dfs(root.right);
    result.push(root.val);
  }
  dfs(root);

  return result;
};
复制代码

🌲 迭代解法:

我们不妨想象一下,把图中箭头全部反过来,是不是就和先序遍历联系起来了:

  • A入栈

  • A出栈

  • B、C入栈

  • C出栈

  • G、F入栈

  • F出栈

  • G出栈

  • B出栈

  • D、E入栈

  • E出栈

  • D出栈

怎么实现这个逆过程呢?有朋友会说可以将结果数组翻转,当然可以,但怎么能省去这次遍历过程呢?push 对应 的 unshilft 会帮我们解决这个问题,其代码如下:

const postorder = root => {
  if (!root) return [];
  
  const stack = [];
  const result = [];
  stack.push(root);

  while (stack.length) {
    const top = stack.pop();
    result.unshift(top.val);
    top.left && stack.push(top.left);
    top.right && stack.push(top.right);
  }

  return result;
};
复制代码

中序遍历

遍历顺序 -> 左根右

tree7.png

中序遍历结果如下:D -> B -> E -> A -> G -> C -> F

🌲 递归解法:

按照其遍历顺序 左根右 递归即可,代码如下:

const inorder = root => {
  if (!root) return [];

  const result = [];
  const dfs = (root) => {
    if (!root) return;
    root.left && dfs(root.left);
    result.push(root.val);
    root.right && dfs(root.right);
  }
  dfs(root);

  return result;
};
复制代码

🌲 迭代解法:

中序遍历为左根右的顺序,故思路如下:当前节点一路向左入栈,到叶子节点后依次出栈,若其有右子树,入栈右子树,反复循环;值得注意的是,左子树用完后记得清空,避免重复入栈。

过程如下:

  • A入栈

  • B、D入栈,D出栈

  • B出栈,E入栈

  • E出栈

  • A出栈,C入栈

  • G入栈,G出栈

  • C出栈,F入栈

  • F出栈

代码如下:

const inorder = root => {
  if (!root) return [];

  const stack = [];
  const result = [];
  stack.push(root);
  let cur = root;

  while (stack.length) {
    while (cur.left) {
      cur = cur.left;
      stack.push(cur);
    }

    cur = stack.pop();
    cur.left = null;
    result.push(cur.val);
        
    if (cur.right) {
      cur = cur.right;
      stack.push(cur);
    };
  }

  return result;
};
复制代码

层序遍历

tree8.png

层序遍历结果如下:A -> B -> C -> D -> E -> G -> F

标准的广度优先遍历,采用队列,详细可见上篇文章广度优先部分,代码如下:

const levelOrder = root => {
  const queue = [];
  const result = [];
  queue.push(root);

  while (queue.length) {
    const head = queue.shift();
    result.push(head.val);
    if (top.left) queue.push(head.left);
    if (top.right) queue.push(head.right);
  }

  return result;
}
复制代码

二叉搜索树

二叉搜索树(BST)是一种有序的二叉树,其需满足 左节点 <= 根节点 <= 右节点

tree9.png

查询

🌲 最大值和最小值

tree10.png

如图,有序的特点使我们不断向左查询即可找到最小值,不断向右查询即可找到最大值

const min = root => {
  if (!root) return null;
  
  let cur = root;
  while (cur.left) {
    cur = cur.left;
  }
  
  return cur.val;
}
复制代码

🌲 任意值

tree11.png

如图,目标值大于当前节点值,向右走;目标值小于当前节点值,向左走;

const searchBST = (root, val) => {
  let cur = root;

  while (cur) {
    if (val === cur.val) return cur;
    else if (val > cur.val) cur = cur.right;
    else cur = cur.left;
  }

  return null;
};
复制代码

添加

tree12.png

和查找任意值如出一辙;目标值大于当前节点值,向右走;目标值小于当前节点值,向左走;走到叶子节点时,根据其与叶子节点的比对,插入相应位置。

const insertIntoBST = (root, val) => {
  if (!root) return new TreeNode(val);

  let cur = root;
  while (cur) {
    if (val > cur.val) {
      if (cur.right) {
        cur = cur.right;
      } else {
        cur.right = new TreeNode(val);
        break;
      }
    } else {
      if (cur.left) {
        cur = cur.left;
      } else {
        cur.left = new TreeNode(val);
        break;
      }
    }
  }

  return root;
};
复制代码

删除

  • 第一步,找到需要删除的节点

  • 若删除节点为叶子节点,直接置为空

tree14.png

  • 若删除节点仅有左子树或仅有右子树,则其前置节点的next等于其后置节点即可

tree15.png

  • 若其两侧均有子树,则找寻其左子树的最大值或右子树的最小值,替换并删除找到的叶子节点

tree16.png

const deleteNode = (root, key) => {
  if (!root) return null;

  if (key > root.val) {
    root.right = deleteNode(root.right, key);
    return root;
  }

  if (key < root.val) {
    root.left = deleteNode(root.left, key);
    return root;
  }

  if (key === root.val) {
    if (!root.left && !root.right) return null;

    if (root.left && root.right) {
      let cur = root.left;
      while (cur.right) {
        cur = cur.right;
      };
      root.val = cur.val;
      root.left = deleteNode(root.left, root.val);
      return root;
    }
    
    if (!root.left || !root.right) return root.left || root.right;
  }
};
复制代码

平衡二叉树

平衡二叉树(AVL)是一种特殊的二叉搜索树,其任意节点的左右子树的高度差都不大于1;

tree17.png

平衡二叉树是对二叉搜索树的一种优化,其解决二叉搜索树可能存在某条边过深的性能问题;

tree18.png

判定平衡二叉树

平衡二叉树的特点为树上每个节点左右子树高度差不大于一,每个节点均要判断代表着需要递归,高度差不大于1即判定条件。

tree19.png

当前节点高度为其左右节点高度的最大值加1

代码如下:

const isBalanced = (root) => {
  let flag = true;

  const dfs = (root) => {
    if (!root) return 0;

    const left = dfs(root.left);
    const right = dfs(root.right);

    if (Math.abs(left - right) > 1) {
      flag = false;
    }

    return Math.max(left, right) + 1;
  }
  dfs(root);

  return flag;
};
复制代码

构造平衡二叉树

给定一颗二叉搜索树,将其构造成平衡二叉树

二叉树的中序遍历是有序的,可取其中序遍历后的值做二分处理,生成平衡二叉树,如下图:

tree20.png

tree21.png

代码如下:

const balanceBST = (root) => {
  const mid = [];

  const midSearch = (root) => {
    root.left && midSearch(root.left);
    mid.push(root.val);
    root.right && midSearch(root.right);
  }
  midSearch(root);

  const createAVL = (arr) => {
    const mid = Math.floor(arr.length / 2);
    const leftArr = arr.slice(0, mid);
    const rightArr = arr.slice(mid + 1, arr.length);

    let left = null;
    let right = null;

    if (leftArr.length) left = createAVL(leftArr);
    if (rightArr.length) right = createAVL(rightArr);

    return new TreeNode(arr[mid], left, right);
  }
  return createAVL(mid);
};
复制代码

小试牛刀

LeetCode 111. 二叉树的最小深度

👺 题目描述

给定一个二叉树,找出其最小深度。最小深度是从根节点到最近叶子节点的最短路径上的节点数量。

tree24.jpeg

👺 题目分析

逐层查找 -- 广度优先遍历

边界值 -- 叶子节点

代码如下:

const minDepth = (root) => {
  if (!root) return 0;

  const result = [];
  let deep = 0;
  result.push(root);

  while (result.length) {
    deep++;
    const len = result.length;
    for (let i = 0; i < len; i++) {
      const head = result.shift();
      head.left && result.push(head.left);
      head.right && result.push(head.right);
      if (!head.left && !head.right) return deep;
    }
  }
  return deep;
};
复制代码

LeetCode 112. 路径总和

👺 题目描述

给你二叉树的根节点 root 和一个表示目标和的整数 targetSum。判断该树中是否存在根节点到叶子节点的路径,这条路径上所有节点值相加等于目标和 targetSum。如果存在,返回 true;否则,返回 false。

tree22.jpeg

👺 题目分析

路径求和 -- 深度优先遍历

触发条件 -- 叶子节点

触发值 -- targetSum === 路径总和

代码实现如下:

const hasPathSum = (root, targetSum) => {
  if (!root) return false;
  
  let flag = false;
  const dfs = (root, sum) => {
    sum = sum + root.val;
    if (!root.left && !root.right && sum === targetSum) flag = true;
    root.left && dfs(root.left, sum);
    root.right && dfs(root.right, sum);
  }
  dfs(root, 0);

  return flag;
};
复制代码

LeetCode 226. 翻转二叉树

👺 题目描述

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

tree23.jpeg

👺 题目分析

递归二叉树,将树的左右节点互换即可

const invertTree = (root) => {
  if (!root) return null;

  const dfs = (root) => {
    const left = root.left;
    root.left = root.right;
    root.right = left;

    root.left && dfs(root.left);
    root.right && dfs(root.right);
  }
  dfs(root);

  return root;
};
复制代码

后记

🔗 本系列其它文章链接:

excel7.gif

分类:
前端
标签:
收藏成功!
已添加到「」, 点击更改