二叉搜索树(BST)题目汇总🎄(一、BST性质利用、遍历时保存上一个遍历节点)

91 阅读10分钟

本节是对二叉搜索树(Binary Search Tree,BST)相关题目的汇总。

总结

  • 二叉搜索树题目一般都可利用其性质
    • 中序遍历有序
    • 搜索时可以确定方向,不用两条路都走
  • 二叉搜索树类题目我们可以将其看成是一个单调递增的数组,递增数组操作起来就会方便很多
  • 基于中序遍历有序,中序遍历时有时需要保存上一个节点,这样能辅助我们在中间节点的逻辑处理
    • 无论是递归还是迭代的中序遍历,都是在中间节点处理逻辑的最后更新上一个节点

二叉搜索树

二叉搜索树也是一棵二叉树,但是具有一些特点:

  1. 对于根节点,左子树中所有节点的值 < 根节点的值 < 右子树中所有节点的值。
  2. 任意节点的左、右子树也是二叉搜索树,即同样满足条件 1.
  3. 中序遍历有序,基于前面的规则,二叉搜索树的中序遍历结果单调递增。

LeetCode-700.二叉搜索树中的搜索

给定二叉搜索树(BST)的根节点 root 和一个整数值 val。 你需要在 BST 中找到节点值等于 val 的节点。 返回以该节点为根的子树。 如果节点不存在,则返回 null 。

利用二叉搜索树的特性,我们可以在每个分岔口(节点)确定路径

代码实现:这里给出递归和迭代的方法,注意这里的代码与遍历无关,因为我们每次都能知道目标值会出现在哪条路。

迭代

var searchBST = function (root, val) {
  let res = root;
  if (!res) return null;
  while (res) {
    if (res.val > val) {
      res = res.left;
    } else if (res.val < val) {
      res = res.right;
    } else {
      break;
    }
  }
  return res;
};

递归

function searchRecursive(node, val) {
  if (!node) return null;
  let res = null;
  if (node.val > val) {
    res = searchRecursive(node.left, val);
  } else if (node.val < val) {
    res = searchRecursive(node.right, val);
  } else {
    res = node;
  }
  return res;
}
var searchBST = function (root, val) {
  return searchRecursive(root, val);
};

LeetCode-98.验证二叉搜索树

给你一个二叉树的根节点 root ,判断其是否是一个有效的二叉搜索树。

本题我们就需要利用到二叉搜索树的一个关键特性了,中序遍历有序

我们可以在中序遍历过程中,判断当前值是否大于上一个值。

递归

递归分析:

  • 返回值和入参:返回当前节点所代表的树是否是二叉搜索树(boolean),入参:当前节点
  • 终止条件:空节点 返回true
  • 单层循环逻辑:
    • 左:递归判断左子树是不是BST,一旦发现不是BST就可以中断递归
    • 中:判断当前值是否小于上一个值
    • 右:递归判断右子树是不是BST,一旦发现不是BST就可以中断递归

实现:重点关注如何获取上一个值

var isValidBST = function (root) {
  let preNode = null;
  //递归中序遍历+获取上一个值
  function valitateBSTRecursive(node) {
    if (!node) return true;

    //左
    const isValidLeft = valitateBSTRecursive(node.left);
    //中断递归
    if (!isValidLeft) return isValidLeft;
    //中
    if (preNode && preNode.val >= node.val) {
      return false;
    }
    preNode = node;
    //右
    const isValidRight = valitateBSTRecursive(node.right);
    //中断递归
    if (!isValidRight) return isValidRight;

    //到这里说明左子树和右子树都是BST,返回true
    return true;
  }
  return valitateBSTRecursive(root);
};

迭代

我们要使用中序遍历的迭代代码,还记得吗?使用栈辅助+标记法。当然,重点还是在于如何获取上一个值

var isValidBST = function (root) {
  let preNode = null;
  const stack = [];
  stack.push(root);
  while (stack.length) {
    const node = stack.pop();
    if (!node) {
      const curNode = stack.pop();
      //操作中间节点的位置,这里与preNode比较并更新preNode
      if (preNode && preNode.val >= curNode.val) {
        return false;
      }
      preNode = curNode;
      continue;
    }
    //栈后序---入栈顺序为右中左
    node.right && stack.push(node.right);
    stack.push(node);
    stack.push(null);
    node.left && stack.push(node.left)
  }
  return true;
};

从本题中我们可以学到:

  • 要合理利用二叉搜索树中序遍历单调递增的性质
  • 在二叉搜索树遍历过程中,想要获得上一个遍历的结点(preNode),我们需要在操作当前结点后更新preNode

LeetCode-530.二叉搜索树的最小绝对差

给你一个二叉搜索树的根节点 root ,返回 树中任意两不同节点值之间的最小差值 。

思路:简单分析一下,要求的是任意两个节点之间的最小差值,我们知道中序遍历是一个单调递增的数组,那么对于一个单调数组来说,最小差值一定存在于两个相邻节点之间,明白了这个,只需要中序遍历的时候保存上一个节点,然后计算差值去更新最小差值就行了。中序遍历和获得上一个节点的代码我们都会了,那这道题就很简单了。

这里只给出递归代码,因为递归更加考察大家的思维,并且代码也相对简短!

递归

递归分析:

  • 外层变量:minDifference 最小差值,preNode上一个遍历处理的节点
  • 返回值和入参:无返回值,入参为当前节点
  • 终止条件:空节点
  • 单层逻辑:
    • 递归左子树
    • 处理中间节点,计算最小差值并更新,更新preNode
    • 递归右子树

实现:

var getMinimumDifference = function (root) {
  let minDifference = Infinity;
  let preNode = null;

  function inorderRecursive(node) {
    if (!node) return;

    //左
    inorderRecursive(node.left);
    //中
    if (preNode) {
      const diff = Math.abs(preNode.val - node.val);
      if (diff < minDifference) minDifference = diff;
    }
    preNode = node;
    //右
    inorderRecursive(node.right);
  }

  inorderRecursive(root);
  return minDifference;
};

LeetCode-501.二叉搜索树中的众数

给你一个含重复值的二叉搜索树(BST)的根节点 root ,找出并返回 BST 中的所有 [众数](即,出现频率最高的元素)。

如果树中有不止一个众数,可以按 任意顺序 返回。 这里规定的二叉搜索树,左孩子(右孩子)小于等于(大于等于)父节点;

思路:

  1. 思路一:我们可以使用中序遍历收集数组,此时相同的值都是连续出现的,那么再遍历一遍数组统计出现次数最多的值就很简单了
  2. 思路二:在中序遍历过程中统计出现最多次的元素,由于中序遍历的单调性,我们可以保存上一个节点,如果当前节点与上一个节点相同,那么次数加一,否则次数清零;

注意:树中不止有一个众数,因此我们需要考虑一下几种情况

  • 当前节点是否是新节点,是的话当前节点出现次数应该设为1,否则+1`
  • 当前节点出现次数 大于 最大出现次数的数,那么应该更新最大出现次数,并且清空已经收集的数再收集;
  • 当前节点出现次数 等于 最大出现次数的数,那么收集即可;

思路一

思路一很简单,就不多说了,给出参考代码:

var findMode = function (root) {
  const sortedArr = [];

  function inorder(node) {
    if (!node) return;
    inorder(node.left);
    sortedArr.push(node.val);
    inorder(node.right);
  }
  //收集中序遍历数组
  inorder(root);
  
  const res = [];
  let maxCount = 0;
  for (let i = 0; i < sortedArr.length;) {
    let count = 0;
    let curNum = sortedArr[i];
    while (sortedArr[i] === curNum) {
      count++;
      i++;
    }
    //如果当前计数大于
    if (count > maxCount) {
      //清空res
      res.length = 0;
      res.push(curNum);
      maxCount = count;
    } else if (count === maxCount) {
      res.push(curNum);
    }
  }
  return res;
};

思路二

递归+保存preNode

var findMode = function (root) {
  let maxCount = 0;
  let res = [];
  let preNode = null;
  let count = 0;
  function findRecursive(node) {
    if (!node) return count;
    
    //左
    findRecursive(node.left);
    
    //中
    //是否是新节点
    const isNew = preNode ? preNode.val !== node.val : true;
    //新节点则重置计数器 否则计数器+1
    if (isNew) {
      count = 1;
    } else {
      count++;
    }

    if (count > maxCount) { //大于最大次数
      res.length = 0;
      res.push(node.val);
      maxCount = count;
    } else if (count === maxCount) { //等于最大次数
      res.push(node.val);
    }
    
    preNode = node;
    
    //右
    findRecursive(node.right);
  }
  findRecursive(root);
  return res;
};

LeetCode-236.二叉树的最近公共祖先

给定一个二叉树, 找到该树中两个指定节点的最近公共祖先。

举例说明:

  • 节点5和节点1 的最近公共祖先是 节点3
  • 节点6和节点4 的最近公共祖先是 节点5

binarytree.png

思路

实际上我们在举例时就已经有一些想法了:

  • 找公共祖先就是在某个节点的左子树和右子树上寻找目标节点,找到了说明这个节点是祖先,ok这里甚至可以确定后序遍历了,因为我们需要左右子树提供的信息
  • 那如何找最近的公共祖先呢?由于我们使用后序来解决这个问题,因此会出现以下两种情况:
    • 当前节点的左子树找到了目标节点,右子树也找到了目标节点,那说明当前节点就是最近的,你可以举例证明
    • 如果在一边找到了,一边没找到,那么说明目标节点都在找到的那一边。比如节点6和节点4,我们当前是在根节点3,左子树找到了,右子树没找到,此时公共祖先应该是左子树上的某个节点。

再来思考:返回值,我们每次返回找到了还是没找到可行吗?对于一边找到了一边没找到的情况,我们并不能确认谁是最近公共祖先,只能知道在哪边。那么我们返回的应该是当前这个节点。

  • 假设寻找节点6和节点4的最近公共祖先,那么在节点2处,左子树没找到,右子树找到了,我们返回节点2到节点5,节点5左子树找到了,右子树也找到了,返回节点5到节点3,节点3左子树找到了,右子树没找到,返回节点5,递归结束。
  • 假设寻找节点6和节点8的最近公共祖先,节点5处左子树找到了,右子树没找到,返回节点5到节点3;节点1处右子树找到了,返回节点1到节点3,此时节点3左子树找到了,右子树也找到了,返回节点3推出递归。

那么是不是可以确定了,我们每次递归的返回值应该是当前节点或空,找到了就是当前节点,没找到就是空;

结合代码再来模拟一下寻找的过程,你会对递归理解更透彻一些。需要明确一点,我们不能中断递归,这个遍历过程一定是执行到找到目标节点或空节点(终止条件)才开始退出递归的,并且我们需要利用返回值进行中间节点的逻辑处理。

实现:

function getAncestor(node, p, q) {
  if (node == p || node === q || node === null) return node;
  //左
  const left = getAncestor(node.left, p, q);
  //右
  const right = getAncestor(node.right, p, q);

  //中
  if (left && right) return node;
  if (!left && right) return right;
  if (left && !right) return left;

  //左边没找到,右边也没找到,返回空节点
  return null;
}
var lowestCommonAncestor = function (root, p, q) {
  return getAncestor(root, p, q);
};

LeetCode-235.二叉搜索树的最近公共祖先

为什么引入上一题,是因为本题的缘故,再上一题的基础上,我们能否利用二叉搜索树的特性来解决呢?

思路

二叉搜索树为我们提供了什么便利?

  1. 如果当前节点的值介于p,q之间,那么当前节点一定就是最近公共祖先
  2. 如果小于p,q最小值,那么往左边找,否则往右边

如下图:目标值为0和5,节点4也是介于之间,但是不是公共祖先,这不就与便利1矛盾了吗?是的,但是如果你从上向下看(先序遍历),那么就不会有问题了!

binarysearchtree_improved.png

实现:

function getAncestor(node, p, q) {
  if (!node) return;
  //中
  const max = Math.max(p.val, q.val);
  const min = Math.min(p.val, q.val);
  if (node.val >= min && node.val <= max) {
    return node;
  }
  let res = null;
  if (node.val > max) { //左
    res = getAncestor(node.left, p, q);
  } else { //右
    res = getAncestor(node.right, p, q);
  }
  return res;
}
var lowestCommonAncestor = function (root, p, q) {
  return getAncestor(root, p, q);
};