LeetCode 题解——判断一棵二叉树是否为二叉搜索树

简介: 前端西瓜哥

我是前端西瓜哥,今天做一道 medium 难度的算法题:判断一棵二叉树是否为二叉搜索树

image.png

LeetCode 上对应的题目为:

98 题:https://leetcode-cn.com/problems/validate-binary-search-tree/《程序员面试金典(第 6 版)》04.05 题:https://leetcode-cn.com/problems/legal-binary-search-tree-lcci/submissions/

什么是二叉搜索树

首先我们要明确二叉搜索树的定义,否则可能带着错误的理解去做题,最终是南辕北辙。

二叉搜索树(Binary Search Tree,简写 BST)是一种比较特别的二叉树。它需要满足:

1.任意节点的值大于其左子树下的所有节点;2.任意节点的值小于其右子树下的所有节点;

下图就是一个二叉搜索树。

image.png

之所以叫二叉搜索树,因为它搜索效率很高,时间复杂度为 O(log n),对标数组的二分查找。

根据二叉搜索树需要满足的条件,我们可以引申出两个特性:

1.二叉搜索树不能有重复的节点值。但实际开发中的搜索树通常还是会有重复值的,但题目如果没有特别指出,就默认不能有重复值。2.中序遍历二叉搜索树,可以拿到单调递增的序列

初学者易犯的错误

初学者容易犯的错误,就是递归时,认为只要每个节点大于左节点,小于右节点就是合法的了。

其实这并不对。

根据定义,二叉搜索树要求每个节点要大于左子树的所有节点,右子树同理。注意,是 所有,而不是左子树的根节点。

其实我们可以举个反例,下图二叉树满足 “每个节点大于左节点,小于右节点”,但节点 5 右子树中的节点 0 不满足 “任意节点的值小于其右子树下的所有节点” 这个定义。

image.png

正确的思路应该是:每个节点需要大于左子树的最大值,小于右子树的最小值

解法1:中序遍历是否有序

这个解法是最好理解和实现的,只需要改造一下我们熟悉的中序遍历的代码即可。

虽然我知道:二叉搜索树的一个特性是中序遍历有序。

但我没想到可以倒推,即 如果中序遍历有序,可以说明这是一棵二叉搜索树。所以我没有用这个解法。

function isValidBST(root: TreeNode | null): boolean {
  let prev = -Infinity;
  let valid = true;
  function dfs (root: TreeNode | null) {
    if (!valid) return; // 确定是不合法的,剪枝
    if (root === null) return;
    dfs(root.left);
    if (prev >= root.val) {
      valid = false;
      return;
    }
    prev = root.val;
    dfs(root.right);
  }
  dfs(root);
  return valid;
};

声明一个名为 prev 的变量,用来记录递归到的当前节点时,它的前一个节点的值。

一开始要初始化为负无穷大,来处理第一个递归的节点没有前一个节点的边界情况。

也可以多用一个数组来存储中序遍历得到的序列,实现上会更不容易出错,代价是增加了一些空间复杂度。

另外,LeetCode 的官方题解给出的是 非递归形式 的解法,实在是有点离谱。建议你还是用递归的写法,不容易写错。

解法2:指定树的取值范围

这个解法就比较巧妙。

递归函数除了传入节点,还要传入以这个节点为根节点的整棵树的需要位于的区间 (min, max)

在递归函数执行中,我们需要做的事情有:

1.判断当前节点是否在 (min, max) 内;2.根据左子树还是右子树调整区间,进行下一轮的递归。具体为:对左节点,它的区间为 (min, root.val);对右节点,区间为 (root.val, max)

function isValidBST(root: TreeNode | null): boolean {
  return isInRange(root, -Infinity, Infinity);
};
function isInRange(root: TreeNode | null, min: number, max: number): boolean {
  if (root === null) return true;
  if (root.val <= min || root.val >= max) return false;
  return isInRange(root.left, min, root.val) && // 左
      isInRange(root.right, root.val, max); // 右
}

解法3:自底向上

下面介绍的是我自己做这题的解法,不是主流的解法。

解法上的判断逻辑其实和解法 2 相同,但我这个是自底向上的。

思路与 求二叉树深度 一脉相承,先拿到左右子树的返回值,然后再做进一步判断。

简单来说就是,我们先得到左右子树是否为合法二叉搜索树,如果一个不合法,就直接返回 false。

如果合法,接着判断 root.val 是否大于左子树的最大值,是否小于右子树的最小值。

function isValidBST(root: TreeNode | null): boolean {
  return getMinAndMax(root)[0];
};
// 返回一棵树的 (1)是否为合法二叉搜索树 (2)最小值 (3)最大值。
function getMinAndMax(root: TreeNode): [boolean, number, number] {
  if (root === null) return [true, Number.MAX_SAFE_INTEGER, Number.MIN_SAFE_INTEGER];
  const [lValid, lMin, lMax] = getMinAndMax(root.left);
  if (!lValid) return [false, -1, -1]; // -1 还是其他值都无所谓,因为已经不合法了。
  const [rValid, rMin, rMax] = getMinAndMax(root.right);
  if (!rValid) return [false, -1, -1];
  if (root.val > lMax && root.val < rMin) {
    return [
      true,
      root.left ? lMin : root.val,
      root.right ? rMax : root.val
    ];
  }
  return [false, -1, -1];
}

这种解法实现起来还挺繁琐的,因为要返回 3 个值。

总结

三种解法的时间复杂度都是 O(n)

最好理解和实现的是中序遍历解法,实现最优雅的是解法 2,我的解法 3 则相当繁琐,勉强能用。

相关文章
|
2月前
【LeetCode 31】104.二叉树的最大深度
【LeetCode 31】104.二叉树的最大深度
20 2
|
2月前
【LeetCode 29】226.反转二叉树
【LeetCode 29】226.反转二叉树
18 2
|
2月前
【LeetCode 28】102.二叉树的层序遍历
【LeetCode 28】102.二叉树的层序遍历
16 2
|
2月前
【LeetCode 45】701.二叉搜索树中的插入操作
【LeetCode 45】701.二叉搜索树中的插入操作
10 1
|
2月前
【LeetCode 44】235.二叉搜索树的最近公共祖先
【LeetCode 44】235.二叉搜索树的最近公共祖先
18 1
|
2月前
【LeetCode 48】108.将有序数组转换为二叉搜索树
【LeetCode 48】108.将有序数组转换为二叉搜索树
40 0
|
2月前
【LeetCode 47】669.修剪二叉搜索树
【LeetCode 47】669.修剪二叉搜索树
10 0
|
2月前
【LeetCode 46】450.删除二叉搜索树的节点
【LeetCode 46】450.删除二叉搜索树的节点
19 0
|
2月前
【LeetCode 43】236.二叉树的最近公共祖先
【LeetCode 43】236.二叉树的最近公共祖先
20 0
|
2月前
【LeetCode 42】501.二叉搜索树中的众数
【LeetCode 42】501.二叉搜索树中的众数
10 0