前端算法面试必刷题系列[41]

270 阅读5分钟

这个系列没啥花头,就是纯 leetcode 题目拆解分析,不求用骚气的一行或者小众取巧解法,而是用清晰的代码和足够简单的思路帮你理清题意。让你在面试中再也不怕算法笔试。

73. 验证二叉搜索树 (validate-binary-search-tree)

标签

  • BST
  • DFS
  • 中等

题目

leetcode 传送门

这里不贴题了,leetcode打开就行,题目大意:

给定一个二叉树,判断其是否是一个有效的二叉搜索树

假设一个二叉搜索树具有如下特征:

  • 节点的左子树只包含小于当前节点的数。
  • 节点的右子树只包含大于当前节点的数。
  • 所有左子树和右子树自身必须也是二叉搜索树。
输入:
    5
   / \
  1   4
     / \
    3   6
输出: false
解释: 输入为: [5,1,4,null,null,3,6]。
     根节点的值为 5 ,但是其右子节点值为 4

基本思路

其实核心子问题就是,一个节点,左右节点是有范围的,在范围内就满足,否则就返回 false。每个节点相同处理。

  • 先实现dfs函数,参数(节点、该节点的下界上界)

  • 对左右子树进行递归,如果左右子树都满足条件,则返回true,如果有一个返回false,就返回false

  • 递归出口:

    • 遍历到null节点,返回 true
    • 当前节点的值没有落在规定区间内,返回false

写法实现

var isValidBST = function (root) {
  // 其实这就是个 dfs 深搜
  const dfs = (root, minVal, maxVal) => {
    // 递归出口
    if (root === null) {
      return true
    }
    // 不满足条件,不是 BST
    if (root.val <= minVal || root.val >= maxVal) {
      return false
    }
    // 分别递归判断左右子树
    return dfs(root.left, minVal, root.val) && dfs(root.right, root.val, maxVal)
  }
  // 初始根节点没有限制
  return dfs(root, -Infinity, Infinity)
};

74. 恢复二叉搜索树 (recover-binary-search-tree)

标签

  • BST
  • 困难

题目

leetcode 传送门

这里不贴题了,leetcode打开就行,题目大意:

给你二叉搜索树的根节点 root ,该树中的两个节点被错误地交换。请在不改变其结构的情况下,恢复这棵树。

进阶:使用 O(n) 空间复杂度的解法很容易实现。你能想出一个只使用常数空间的解决方案吗?

image.png

输入:root = [1,3,null,null,2]
输出:[3,1,null,null,2]
解释:3 不能是 1 左孩子,因为 3 > 1 。交换 13 使二叉搜索树有效。

image.png

输入:root = [3,1,4,null,null,2]
输出:[2,1,4,null,null,3]
解释:2 不能在 3 的右子树中,因为 2 < 3 。交换 23 使二叉搜索树有效。

基本思路

关键点是

  • 只有2个节点是错误的,找到交换即可。
  • BST 的 中序遍历 依次访问的节点值是递增的。

至于第二条如果不清楚为什么 请移步 这篇二叉树中序遍历和 BST 基本性质

先简单看下中序遍历框架吧

const midOrderTraversal = (node) => {
    if(node !== null) {
      // 先遍历左子树
      midOrderTraversal(node.left)           // ---------> (左)
      // 然后访问根节点
      resList.push(node.val)                 // ---------> (根)
      // 再遍历右子树
      midOrderTraversal(node.right)          // ---------> (右)
    }
}

那就剩下如何找到这两个错误点了,当然直接把中序遍历搞出来,然后对比也行,但可以有更好方式。

顺序错误有可能有2种

  • 相邻的两个节点顺序错了,
  • 不相邻的,不相邻的第一处肯定是前大后小,第二处是前小后大,所以想变回正确,第一对是取前者而第二对是取后者然后交换。

只用比较前后访问的节点值,prev 保存上一个访问的节点当前访问的是 root 节点。

每访问一个节点,如果prev.val >= root.val,就找到了一对“错误对”。 检查一下它是第一对错误对,还是第二对错误对。

遍历结束,就确定了待交换的两个错误点,进行交换。

注意最后交换这两个结点的时候,只是交换他们的值就可以了,而不是交换这两个结点相应的指针指向。

写法实现

const recoverTree = (root) => {
  let [preNode, err1, err2]  = [new TreeNode(-Infinity), null, null];
  
  // 下面就是中序遍历
  const midOrder = (root) => {
    if (root !== null) {
      midOrder(root.left);

      // 访问根做操作,找到错误的 index
      // 当前是第一对错误
      if (preNode.val >= root.val && err1 === null) { 
        // 第一对错误点记录前一个元素
        err1 = preNode;                            
      }
      if (preNode.val >= root.val && err1 !== null) { 
        // 第二对错误点记录后面一个元素(当然可能两对是一对)
        err2 = root;                            
      }
      // 当前点用完,就更新前置节点
      preNode = root; 

      midOrder(root.right);
    }
  };

  midOrder(root);
  // 错误交换就变回正确的顺序了
  [err1.val, err2.val] = [err2.val, err1.val]
};

75. 不同的二叉搜索树 (unique-binary-search-trees)

标签

  • 分治
  • BST (binary-search-trees)
  • 中等

题目

leetcode 传送门

这里不贴题了,leetcode打开就行,题目大意:

给定一个整数 n,求以 1 ... n 为节点组成的二叉搜索树有多少种。

输入:3
输出:5
解释:
以上的输出对应以下 5 种不同结构的二叉搜索树:

   1         3     3      2      1
    \       /     /      / \      \
     3     2     1      1   3      2
    /     /       \                 \
   2     1         2                 3

相关知识

这题是上篇的简略版。不了解请移步二叉搜索树

基本思路

  • 当 n = 0 时,没有数字,只能形成一种 BST :空树。

  • 当 n = 1 时,只有一个数字,只能形成一种 BST :单个节点。

  • 用 i 个节点构建 BST,除去根节点,剩 i−1 个节点分别构建左、右子树,左子树分配 0 个,则右子树分配到 i−1 个,那么左子树分配 j 个,则右子树分配i−1-j 个。

  • 那么 实际上 i 个节点 BST 数 就是 j0 遍历到 i - 1G(j) * G(i−1-j) 的和 G(i) 表示用连续 i 个数,所构建出的 BST 所有种类数。

这题跟前面不同是不用构建树,算总数就行,更简单。也可以用动态规划找出动态方程解决。

写法实现

const numTrees = (n) => {
  // 0 和 1能创造出的 BST 只能是 1种,递归出口
  if (n == 0 || n == 1) {
    return 1;
  }
  let num = 0;
  for (let j = 0; j < n; j++) {
    num += numTrees(j) * numTrees(n - 1 - j);
  }
  return num;
};

console.log(numTrees(3))

另外我们可以把每轮递归算出的答案缓存起来,提高效率。

const numTrees = (n) => {
  const cache = new Map()

  const divideAndConquer = (n) => {
    if (n == 0 || n == 1) {
      return 1;
    }
    // 如果存在,用缓存,直接 return
    if (cache.has(n)) {
      return cache.get(n);
    }
    let count = 0;
    for (let j = 0; j < n; j++) {
      count += divideAndConquer(j) * divideAndConquer(n - 1 - j);
    }
    // 缓存下本次结果,共下次使用
    cache.set(n, count);
    return count;
  };

  return divideAndConquer(n);
};

console.log(numTrees(3))

另外向大家着重推荐下这位大哥的文章,非常深入浅出,对前端进阶的同学非常有作用,墙裂推荐!!!核心概念和算法拆解系列

今天就到这儿,想跟我一起刷题的小伙伴可以加我微信哦 搜索我的微信号infinity_9368,可以聊天说地 加我暗号 "天王盖地虎" 下一句的英文,验证消息请发给我 presious tower shock the rever monster,我看到就通过,暗号对不上不加哈,加了之后我会尽我所能帮你,但是注意提问方式,建议先看这篇文章:提问的智慧

参考