前端leetcde算法面试套路之树

本文详细介绍了前端面试中常见的树相关算法,包括二叉树的前中后序遍历,递归与迭代方法,以及刷题过程中的疑惑点。通过实例解析了二叉树的各种遍历方法,如144. 二叉树的前序遍历、104. 二叉树的最大深度等,还探讨了自顶向下和自底向上的遍历思路。文章适合正在准备前端算法面试的开发者阅读,旨在帮助读者掌握树的算法知识。
摘要由CSDN通过智能技术生成

正文

在前端中确实用到不少与树相关的的知识,比方说 DOM 树,Diff 算法,包括原型链其实都算是树,学会树,其实对于学这些知识还是有比较大的帮助的,当然我们学算法还是得考虑面试,而树恰好也是一个大重点 – 起码在前端而言;

主要原因在于,树它华而不实,比较下里巴人,需要抽象但是又能把图画出来不至于让你毫无头绪,简单而言就是看上去很厉害,但实际上也很接地气,俗称比较一般;要知道做前端的面试算法,考的不就是你有么得主动学习能力,抽象能力等,但是考虑到参差不齐的前端娱乐圈,考得难吧可能就全是漏网之鱼了,所以既要筛选出鱼,但是又不能难度过大,树就是那个比较适中的,所以赶紧刷起来吧朋友们;

这里本来是要遵照 3:5:2 难度来刷,预计刷个30题就差不多,但是实际中等题刷得欲罢不能,难题是欲仙欲死,容易题是味如嚼蜡,所以 XDM 担待一下。选题主要是那个男人精选的例题以及 Leetcode 中 HOT 题和字节专题,总的来说代表性还是够的,刷完应该大概或许能够应付一下树这方面的算法了。

如果觉得有那么点帮助,请点个赞留个言,点赞超过10个就更新下一part;好吧,即便不过也会更新,就是这么臭不要脸,大家伙加油吧,欧力给!!

二叉树的遍历

递归遍历

  1. 递归的时候前中后序都能直接处理完了
  2. 递归是前中后序遍历最简单也是最容易出理解的方法,不懂的画个图就好了

迭代遍历 – 双色标记法

  1. 使用颜色标记节点状态,新节点为白色,已经访问的节点为灰色 – 可以用数字或者其他任意标签标示
  2. 如果遇到的节点是白色,则标记为灰色,然后将右节点,自身,左节点一次入栈 – 中序遍历
  3. 如果遇到的节点是灰色的,则将节点输出
  4. 注意这里是用 stack 栈来存储的,所以是后进先出,所以如果是中序遍历,左 - 中 - 右 ,那么在插入栈的时候要反过来 右 - 中 - 左

按照那个男人的指示,正常我们就用递归做就好,就好像我们做非排序题排序的时候,sort 一下就好了,但是一旦面试官问到用另外的迭代方式的时候,我们再套个模板,会比记住多个迭代写法要简单,毕竟内存容量有限,而后续遍历的迭代写法确实挺坑的,能省一点内存就省一点吧

144. 二叉树的前序遍历

// 144. 二叉树的前序遍历

/** * @分析 -- 递归 */
var preorderTraversal = function (root) {
   
  const ret = [];
  const recursion = (root) => {
   
    if (!root) return;
    ret.push(root.val);
    recursion(root.left);
    recursion(root.right);
  };
  recursion(root);
  return ret;
};

/** * @分析 -- 迭代 -- 双色标记法 * 1. 使用颜色标记节点状态,新节点为白色,已经访问的节点为灰色 -- 可以用数字或者其他任意标签标示 * 2. 如果遇到的节点是白色,则标记为灰色,然后将右节点,自身,左节点一次入栈 -- 中序遍历 * 3. 如果遇到的节点是灰色的,则将节点输出 * 4. 注意这里是用 stack 栈来存储的,所以是后进先出,这里是前序遍历,中 - 左  - 右 ,那么在插入栈的时候要反过来 右 - 左 - 中 */
var preorderTraversal = function (root) {
   
  const ret = [];
  const stack = [];
  stack.push([root, 0]); // 0 是白色未处理的,1 是灰色处理过的
  while (stack.length) {
   
    const [root, color] = stack.pop();
    if (root) {
   
      if (color === 0) {
   
        // 遇到白球,则插入 -- 前序
        stack.push([root.right, 0]);
        stack.push([root.left, 0]);
        stack.push([root, 1]);
      } else {
   
        // 遇到灰球,则收网
        ret.push(root.val);
      }
    }
  }
  return ret;
};


1.94 二叉树的中序遍历

// 94. 二叉树的中序遍历

/** * @分析 * 1. 递归的时候前中后序都能直接处理完了 * 2. 递归是前中后序遍历最简单也是最容易出理解的方法,不懂的画个图就好了 */
var inorderTraversal = function(root) {
   
    const ret  = []
    const recursion = root => {
   
        if(!root) return
        recursion(root.left)
        // 这里是中序,所以在两个递归之间,如果是前序就在前面,后序就在后面
        ret.push(root.val)
        recursion(root.right)
    }
    recursion(root)
    return ret
};

/** * @分析 -- 迭代 -- 双色标记法 * 1. 使用颜色标记节点状态,新节点为白色,已经访问的节点为灰色 -- 可以用数字或者其他任意标签标示 * 2. 如果遇到的节点是白色,则标记为灰色,然后将右节点,自身,左节点一次入栈 -- 中序遍历 * 3. 如果遇到的节点是灰色的,则将节点输出 * 4. 注意这里是用 stack 栈来存储的,所以是后进先出,所以如果是中序遍历,左 - 中 - 右 ,那么在插入栈的时候要反过来 右 - 中 - 左 */
var inorderTraversal = function(root) {
   
    const ret  = []
    const stack = []
    stack.push([root,0]) // 0 是白色未处理的,1 是灰色处理过的
    while(stack.length) {
   
        const  [root,color] = stack.pop()
        if(root){
   
            if(color === 0){
   
                // 遇到白球,则插入 -- 中序遍历
                stack.push([root.right,0])
                stack.push([root,1])
                stack.push([root.left,0])
            }else{
   
                // 遇到灰球,则收网
                ret.push(root.val)
            }
        } 
    }
    return ret
};

145. 二叉树的后序遍历

// 145. 二叉树的后序遍历

/** * @分析 -- 递归 */
var postorderTraversal = function(root) {
   
    const ret = []
    const dfs = (root) => {
   
        if(!root) return 
        dfs(root.left)
        dfs(root.right)
        ret.push(root.val)
    }
    dfs(root)
    return ret
};

/** * @分析 -- 迭代 -- 双色球 */
var postorderTraversal = function(root) {
   
    const ret = []
    const stack = []
    stack.push([root,0])
    while(stack.length){
   
        const [root,color] = stack.pop()
        if(root) {
   
            if(color === 0){
   
                stack.push([root,1])
                stack.push([root.right,0])
                stack.push([root.left,0])
            }else{
   
                ret.push(root.val)
            }
        } 
    }
    return ret
}

刷题过程一些疑惑点

自顶向下(前序遍历)和自低向上(后续遍历)

这两个名词在很多讲树的题解中经常会出现,而这与我们遍历树求值到底关联点在哪里,慢慢刷题之后我发现,虽然 dfs 有三种形式,但在抽象到具体题目的时候,其实是属于不同的方法的。

对于前序遍历而言,就是先获取到根节点的信息,然后做了一定编码后,再向下遍历,这种遍历方式就是所谓的 自顶向下 的思维,我们从根节点开始,可以携带一定的信息,再继续往下遍历时,先处理,得到临时性结果,给顶层的节点作为信息;

对于自顶向下的遍历而已,遍历到根节点,就处理结束所有的节点,也相应的得到预期结果了,所以一般使用前序遍历方法解题的,都会声明一个全局变量,然后遍历完之后,返回这个值.

例子:563. 二叉树的坡度

分析
1. 自底向上返回子树值之和,然后求出对应的坡度,累加起来即可.
2. 需要注意的是,左右子树的累加值大小不确定,需要用绝对值
3. 时间复杂度 ${
   O(N)}$

var findTilt = function (root) {
   
  let ret = 0;
  const recursion = (root) => {
   
    if (!root) return 0;
    const left = recursion(root.left);
    const right = recursion(root.right);
    ret += Math.abs(left - right);
    return left + right + root.val;
  };
  recursion(root);
  return ret;
};

对于后序遍历 而言,是想遍历到叶子节点,然后再向上去处理根节点,也就是所谓的 自底向上

实际上,自底向上是一种递归的方法,先 到叶子节点,处理完返回一定的值,再回来,后续的处理都是根据子树的值作为入参的,所以不要被 遍历 迷惑,后续遍历 可不是遍历完就结束了,那才刚刚开始呢。

所以后面为了区分,在处理自底向上题目的时候,函数名字都不再使用 dfs,而是直接使用 recursion ;

参考视频:传送门

例子:

判断遍历到边界,什么在叶子节点处判断,什么时候直接跑到 null 返回?

先来解释一下,在做 dfs 遍历的时候,我们需要遍历到叶子节点,然后做最终的处理,有的题目我们看到的是判 null 时返回 null/0 等;有的时候我们直接判断是否叶子节点,if(!root.left && !root.right)

这是在刷题过程中感觉忒迷惑的地方,在最开始的时候,我喜欢使用 null ,因为它写的更少,而且顺便把根节点为空的边界也做了,最近刷的时候我开始觉得判断节点会更稳妥一点,而且不用做更深的处理,直到我再写👆上面的文字时,有那么一点想法

在我们使用自底向上的时候,因为需要从子节点中 return 值,这个时候即便是 null 也是有用的,所以使用 null 基本是 OK 的。

例子: 104. 二叉树的最大深度

/** * 1. 自顶向下,带个层数参数,判定为叶子节点就进行最大值判断 */
var maxDepth = function (root) {
   
  if (!root) return 0;
  let ret = 0;
  const dfs = (root, depth) => {
   
    if (root.left) dfs(root.left, depth + 1);
    if (root.right) dfs(root.right, depth + 1);
    ret = Math.max(ret, depth);
    return;
  };
  dfs(root, 1);
  return ret;
};

// 自低向上
var maxDepth = function (root) {
   
  const dfs = (root) => {
   
    if (!root) return 0;
    return Math.max(dfs(root.left), dfs(root.right))+1;
  };
  return dfs(root);
};


而在一些携带数据,自顶向下求值的题目中,如果跑到 null 才结束遍历,就比较容易出现重复计算的错误,而且由于不需要获取 return 值,这个时候我建议是使用判断节点的方法。

例子:1022. 从根到叶的二进制数之和

/** * @分析 * 1. 自顶向下求出每一条路当前对应的数字,保存在入参中 * 2. 在叶子节点处将值累加起来即可 * 3. 需要注意的是,要在叶子节点就处理,而不是在 null 的时候处理,不然会重复计算 */
 var sumRootToLeaf = function(root) {
   
    // if(!root) return 0 //题目已知节点是 1-1000
    let ret = 0
    const dfs = (root,sum) => {
   
        const temp = (sum<<1) + root.val
        if(!root.left && !root.right){
   
            ret +=temp
            return 
        }
        if(root.left) dfs(root.left,temp)
        if(root.right) dfs(root.right,temp)
    }

    dfs(root,0)
    return ret
};

简单题

101. 对称二叉树

分析

  1. 对称二叉树,其实是要求是否镜像对齐,所以递归过程至少需要两个根节点,然后 dfs 主要就是判断是否是对称的两棵树
  2. 这里是自顶向下分配相互比较的子树节点 left 和 right,然后再自底向上的返回最终结果
  3. 在某一次 dfs 中,如果比较双方都是 null,那么证明比较双方是对称的;如果出现只有一方有值,或者双方有值但是值不一样的时候,返回 false;
  4. 每次递归都是左右外层构成比较,左右内层构成比较
  5. 时间复杂度: O(h), 其中 h 是树的高度
// 101. 对称二叉树
var isSymmetric = function (root) {
   
  if (!root) return false;
  const dfs = (left, right) => {
   
    if (!left && !right) return true;
    if (!left || 
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值