总结一波树的算法题,助你面试乘风破浪 |刷题打卡

434 阅读4分钟

本文正在参与掘金团队号上线活动,点击 查看大厂春招职位

面试中写算法是必备环节。算法题的一个特点就是如果做过了,有思路的话,很快就能够做出来;如果没做过,就很考验临场发挥了。我在面试时就有时因为紧张,看到算法题发怵,最后影响面试结果😓。

所以我们在平时要注意加强算法方面的训练,在做算法题的过程中沉淀出自己的解题思路,将问题进行归类和转化,做到一通百通。

分享下学习算法的经验

这里我分享一下我在刷算法题过程中总结的经验:

1、养成好的学习习惯

  • 遇到难点,反复思考理解
  • 三分理解,七分练习

2、大道至简,思考本质,找内在的重复性

​ 要注意总结归纳,之后看到类似的题目就能联想到对应的解法。比如:

  • 遇到树的问题,就可以想到树的深度优先遍历广度优先遍历,而深度优先遍历想到的是递归,广度优先遍历则是循环+存数组
  • 遇到链表可以想到的解题思路有遍历+标记
  • 遇到回文,翻转数组,翻转数字想到双指针+交换
  • ... 还有很多场景,多多总结

3、摒弃旧习惯

  • 不要死磕(5分钟做不出来就看题解吧,充分理解是关键)
  • 不要只做一遍,一道题刷五遍(五毒神掌)
  • 不懒于看高手代码

只要掌握了算法题的规律,平时多总结积累,以后遇到算法题时,我们就不怕不怕啦~

树相关的算法题

这里我搜集了一波树的算法题,也是为了将同类型的题目放在一起比较,总结内在规律。

整理的题目

分类难度题目
DFS简单104. 二叉树的最大深度
DFS简单111. 二叉树的最小深度
BFS中等102. 二叉树的层序遍历
BFS中等107. 二叉树的层序遍历 II
DFS简单226. 翻转二叉树
DFS简单101. 对称二叉树

说明

DFS: 深度优先遍历

BFS:广度优先遍历

开始解题

1. 创建树

做树的算法题要先创建树结构,下文提供了node方法来创建一个树。

/*
树结构如下
  7
 3  6
1 2 4 5
*/
function node(val, left = null, right = null) {
  return {
    val,
    left,
    right,
  }
}
let left = node(3, node(1), node(2))
let right = node(6, node(4), node(5))
let head = node(7, left, right)

2.二叉树的最大深度

力扣链接:104. 二叉树的最大深度

思路分析

树的最大深度涉及到树的遍历,只有遍历完整个树后才能计算出树的高度。而树的遍历又有两种:深度优先遍历和广度优先遍历。下面分别用两种方式实现对树深度的计算。

方法1.深度优先遍历

let maxDepth = function (root) {
  if (root == null) {
    return 0
  } else {
    let left = maxDepth(root.left)
    let right = maxDepth(root.right)
    return Math.max(left, right) + 1
  }
}

方法2.广度优先遍历

使用广度优先计算树的深度,实现起来稍微复杂点,注意要点:

  1. 使用数组保存每个节点

  2. 使用两个指针,i指向当前索引,last指向当前层最后一项

  3. 注意层级切换的条件【关键】

    if (i == last) { last = arr.length - 1 height++ }

function maxDepth(node) {
  if (!node) return 0
  let arr = [node],
    i = 0,
    last = 0,
    current,
    height = 0
  while (i < arr.length) {
    current = arr[i]
    if (current.left) {
      arr.push(current.left)
    }
    if (current.right) {
      arr.push(current.right)
    }
    if (i == last) {
      last = arr.length - 1
      height++
    }
    i++
  }
  return height
}

复杂度分析

非科班出身的同学往往对时间复杂度,空间复杂度的概念有点懵圈😶,其实时间复杂度就是计算的次数,计算几次就是几。空间复杂度就是在计算过程中用到的额外变量的个数。涉及到树的遍历,一般都是要遍历整个树结构,此时的时间复杂度就是O(n),n是树的节点个数;空间复杂度和使用的栈空间有关,是O(n)。

时间复杂度:O(n)

空间复杂度:O(n)

3. 二叉树的最小深度

力扣链接:111. 二叉树的最小深度

思路分析

此题我就直接用DFS(深度优先)来求解了,大家在做题过程中还可以思考如果用BFS(广度优先)怎么做。因为面试官看你做的顺利,有时会提高难度,把问题引申一下。咱们也该多思考,留后手。

AC代码

let minDepth = function (root) {
  if (root == null) {
    return 0
  } else {
    let left = minDepth(root.left)
    let right = minDepth(root.right)
    return Math.min(left, right) + 1
  }
}

4. 广度优先遍历

力扣链接1:102. 二叉树的层序遍历

力扣链接2:107. 二叉树的层序遍历 II

这两道都是广度优先遍历的题目,把这两题放在一起思考,可以加深理解。这种类型的题目还可以进行变种,比如要求返回每层节点的和等。

思路分析

广度优先遍历的核心思想是while循环+数组。创建一个数组,将根节点保存到数组里,再对数组中的每个节点进行遍历,如果有子节点,则添加到数组末尾,直到遍历结束。

AC代码

题1:102. 二叉树的层序遍历

var levelOrder = function (root) {
  if (!root) return []
  let resArr = [],
    array = [root],
    i = 0,
    levelLastIndex = 0,
    temp = []
  while (i < array.length) {
    let current = array[i]
    if (current.left) {
      array.push(current.left)
    }
    if (current.right) {
      array.push(current.right)
    }
    temp.push(current.val)
    if (i == levelLastIndex) {
      levelLastIndex = array.length - 1
      resArr.push(temp)
      temp = []
    }
    i++
  }
  return resArr
}

题2:107. 二叉树的层序遍历 II

var levelOrderBottom = function (root) {
  if (!root) return []
  let array = [root],
    i = 0,
    lastIndex = 0,
    temp = [],
    res = []
  while (i < array.length) {
    let current = array[i]
    if (current.left) {
      array.push(current.left)
    }
    if (current.right) {
      array.push(current.right)
    }
    temp.push(current.val)
    if (i == lastIndex) {
      // 在当前层的最后一项
      lastIndex = array.length - 1
      res.unshift(temp)
      temp = []
    }
    i++
  }
  return res
}

5. 翻转二叉树

力扣链接:226. 翻转二叉树

思路分析

利用DFS(深度优先)遍历二叉树,将每个节点都进行交换。这里的核心思路是递归+交换

AC代码

var invertTree = function (root) {
  if (!root) return root
  invertTree(root.left)
  invertTree(root.right)
  let temp = null
  temp = root.left
  root.left = root.right
  root.right = temp
  return root
}

6. 对称二叉树

力扣链接:101. 对称二叉树

思路分析

此题可以用深度优先遍历(递归)的思路求解。

比如看下面这两个子树(他们分别是根节点的左子树和右子树),能观察到这么一个规律:

  • 左子树 2的左孩子 == 右子树 2的右孩子;
  • 左子树 2 的右孩子 == 右子树 2的左孩子
    2         2
   / \       / \
  3   4     4   3
 / \ / \   / \ / \
8  7 6  5 5  6 7  8

我们将根节点的左子树记做 left,右子树记做 right。比较 left 是否等于 right,不等的话直接返回就可以了。 如果相等,比较 left 的左节点和 right 的右节点,再比较 left 的右节点和 right 的左节点

根据上面信息可以总结出递归函数的两个条件:

  1. 终止条件为:left 和 right 不等,或者 left 和 right 都为空
  2. 递归的比较 left,left 和 right.right,递归比较 left,right 和 right.left

AC代码

function isSymmetric(root) {
  return isMirror(root.left, root.right)
  function isMirror(node1, node2) {
    if (node1 === null && node2 === null) {
      return true
    }
    if (node1 === null || node2 === null) {
      return false
    }
    return (
      node1.val === node2.val &&
      isMirror(node1.left, node2.right) &&
      isMirror(node1.right, node2.left)
    )
  }
}

总结

遇到树的问题,我们要联想到树的深度优先遍历或广度优先遍历。而深度优先遍历想到的是递归,广度优先遍历则是循环+数组。在面试时可以先把脑海中想到的这些点拿出来套一下,看能不能解,避免出现没有头绪的情况。