98-二叉树-验证二叉搜索树

树 | 深度优先搜索 | 二叉搜索树 | 二叉树

一、二叉搜索树(BST)的性质

首先,了解 二叉搜索树(Binary Search Tree, BST) 的定义和性质是解决这类问题的基础。

BST 的定义

  1. 左子树:节点的左子树只包含 小于 当前节点的数。
  2. 右子树:节点的右子树只包含 大于 当前节点的数。
  3. 递归性质:左子树和右子树本身也必须是二叉搜索树。

简单来说,BST 具有以下特点:

  • 中序遍历 BST 可以得到一个 递增的有序序列
  • 每个节点的值都大于其左子树所有节点的值,小于其右子树所有节点的值。

示例

考虑以下二叉树:

    5
   / \
  3   7
 / \   \
2   4   8

验证这是否是一个有效的 BST:

  • 根节点 5 的左子树 [3, 2, 4] 的所有节点都小于 5,右子树 [7, 8] 的所有节点都大于 5
  • 子树 3 的左子节点 2 小于 3,右子节点 4 大于 3
  • 子树 7 的右子节点 8 大于 7
  • 以上所有条件都满足,所以这是一个有效的 BST。

二、算法思想

为了判断一个二叉树是否为有效的 BST,我们需要确保每个节点都满足 BST 的性质。这意味着不仅要检查当前节点的左子节点小于它,右子节点大于它,还要确保整个左子树的所有节点都小于当前节点,右子树的所有节点都大于当前节点。

使用递归和边界

为了有效地实现这一点,我们可以使用 递归 方法,并在递归过程中维护每个节点值的 上下界

  1. 初始阶段,根节点的值应该在 (-∞, +∞) 范围内。
  2. 递归左子树 时,新的上界变为当前节点的值,因为左子树所有节点都必须小于当前节点。
  3. 递归右子树 时,新的下界变为当前节点的值,因为右子树所有节点都必须大于当前节点。
  4. 终止条件,如果遇到空节点,返回 true,因为空树也是有效的 BST。

通过这种方式,每个节点都被限制在一个特定的范围内,确保整个树满足 BST 的性质。

三、代码解析

让我们逐行分析提供的 JavaScript 代码,理解其工作原理。

var isValidBST = function (root, min = -Infinity, max = Infinity) {
  // 如果是空节点,返回 true,因为空树是有效的 BST
  if (!root) return true;

  // 检查当前节点的值是否在 (min, max) 范围内
  if (root.val <= min || root.val >= max) {
    return false;
  }

  // 递归检查左子树,更新上界为当前节点的值
  // 递归检查右子树,更新下界为当前节点的值
  return (
    isValidBST(root.left, min, root.val) &&
    isValidBST(root.right, root.val, max)
  );
};

参数说明

  • root:当前节点的引用。
  • min:当前节点值的下界,初始为 -Infinity
  • max:当前节点值的上界,初始为 +Infinity

代码逻辑

  1. 检查空节点

    if (!root) return true;
    

    如果当前节点为空,说明没有违反 BST 的性质,返回 true

  2. 验证当前节点值

    if (root.val <= min || root.val >= max) {
      return false;
    }
    

    如果当前节点的值不在 (min, max) 范围内,则违反了 BST 的性质,返回 false

  3. 递归检查左右子树

    return (
      isValidBST(root.left, min, root.val) &&
      isValidBST(root.right, root.val, max)
    );
    
    • 左子树:调用 isValidBST 检查左子树,将上界更新为当前节点的值 root.val
    • 右子树:调用 isValidBST 检查右子树,将下界更新为当前节点的值 root.val
    • 只有当左右子树都满足 BST 的性质时,当前子树才被认为是有效的 BST。

关键点

  • 边界更新:在递归调用中,根据当前节点的位置(左或右),相应地更新边界条件。
  • 开区间:注意这里使用的是开区间 (min, max),确保节点值严格大于 min 且严格小于 max,避免重复值。

四、举例说明

让我们通过一个具体的例子来理解这个算法如何工作。

示例二:无效的 BST

考虑以下二叉树:

    5
   / \
  1   4
     / \
    3   6

验证这是否是一个有效的 BST。

  1. 初始调用

    isValidBST(root = 5, min = -Infinity, max = Infinity)
    
    • 节点 5(-∞, +∞) 内,继续检查左右子树。
  2. 检查左子树

    isValidBST(root = 1, min = -Infinity, max = 5)
    
    • 节点 1(-∞, 5) 内,继续检查左右子树。
    • 左右子节点为空,返回 true
  3. 检查右子树

    isValidBST(root = 4, min = 5, max = +Infinity)
    
    • 节点 4 不在 (5, +∞) 内,违反 BST 的性质,返回 false

由于右子树不满足 BST 的性质,整个树被判定为 无效的 BST

示例三:有效的 BST

使用之前的有效 BST 示例:

    5
   / \
  3   7
 / \   \
2   4   8
  1. 初始调用

    isValidBST(root = 5, min = -Infinity, max = +Infinity)
    
    • 节点 5(-∞, +∞) 内,继续检查左右子树。
  2. 检查左子树

    isValidBST(root = 3, min = -Infinity, max = 5)
    
    • 节点 3(-∞, 5) 内,继续检查左右子树。
    • 检查 3 的左子树
      isValidBST(root = 2, min = -Infinity, max = 3)
      
      • 节点 2(-∞, 3) 内,检查左右子节点为空,返回 true
    • 检查 3 的右子树
      isValidBST(root = 4, min = 3, max = 5)
      
      • 节点 4(3, 5) 内,检查左右子节点为空,返回 true
    • 左右子树都有效,3 有效。
  3. 检查右子树

    isValidBST(root = 7, min = 5, max = +Infinity)
    
    • 节点 7(5, +Infinity) 内,继续检查左右子树。
    • 左子树为空,返回 true
    • 检查 7 的右子树
      isValidBST(root = 8, min = 7, max = +Infinity)
      
      • 节点 8(7, +Infinity) 内,检查左右子节点为空,返回 true
    • 左右子树都有效,7 有效。
  4. 总结
    左右子树都满足 BST 的性质,整个树被判定为 有效的 BST

五、总结与拓展

通过递归并维护每个节点的值在特定范围内,我们能够高效地验证整个二叉树是否符合 BST 的性质。这种方法的优势在于:

  • 时间复杂度O(n),每个节点只被访问一次。
  • 空间复杂度O(h),其中 h 是树的高度,主要用于递归调用栈。

其他方法

除了递归方法,还有其他验证 BST 的方法,例如:

  1. 中序遍历

    • 对 BST 进行中序遍历,应该得到一个严格递增的序列。
    • 可以使用迭代或递归实现中序遍历,并在遍历过程中比较相邻节点的值。
  2. 迭代法

    • 使用栈模拟递归,维护一个节点的值序列,确保递增。

不过,递归加边界的方法直观且易于实现,是解决这类问题的经典方法。


好的,我们继续深入学习验证二叉搜索树(BST)的方法。这一次,我们将介绍 方法二:中序遍历,并通过具体代码示例详细解析其思路和实现细节。

方法二:中序遍历

思路与算法

中序遍历 是二叉树的一种经典遍历方式,它按照 左子树 → 根节点 → 右子树 的顺序访问节点。在二叉搜索树中,中序遍历会访问到一个 严格递增 的节点序列。这一性质为验证二叉搜索树提供了便利:

  1. 中序遍历的性质:对任意二叉搜索树,中序遍历的结果是一个严格递增的序列。
  2. 验证方法:在进行中序遍历的过程中,实时比较当前访问的节点值是否大于前一个访问的节点值。如果所有节点都满足这一条件,则该二叉树是有效的 BST;否则,说明存在不符合 BST 性质的节点。

为了高效地实现这一验证过程,我们可以使用 迭代法(利用栈)来模拟中序遍历,避免递归带来的额外空间开销。

代码实现

下面是使用 JavaScript 实现的中序遍历方法:

var isValidBST = function(root) {
    let stack = [];         // 用于模拟递归的栈
    let inorder = -Infinity; // 记录前一个访问的节点值,初始为负无穷

    while (stack.length || root !== null) {
        // 一直向左走,直到最左节点
        while (root !== null) {
            stack.push(root);
            root = root.left;
        }
        // 访问节点
        root = stack.pop();
        // 如果当前节点的值不大于前一个节点的值,返回 false
        if (root.val <= inorder) {
            return false;
        }
        inorder = root.val; // 更新前一个节点的值
        root = root.right;  // 转向右子树
    }
    return true; // 所有节点都满足 BST 的性质
};

代码解析

让我们逐行分析这段代码,理解其工作原理。

1. 初始化
let stack = [];
let inorder = -Infinity;
  • stack:用于模拟递归调用的栈,存储待访问的节点。
  • inorder:记录中序遍历中上一个访问的节点值,初始设为负无穷,确保第一个节点的值总是大于它。
2. 主循环
while (stack.length || root !== null) {
    // ...
}
  • 条件解释:当栈不为空或当前节点 root 不为 null 时,继续循环。这确保了所有节点都会被访问。
3. 遍历左子树
while (root !== null) {
    stack.push(root);
    root = root.left;
}
  • 作用:不断访问当前节点的左子节点,将经过的节点压入栈中,直到到达最左叶子节点(左子节点为 null)。
4. 访问节点
root = stack.pop();
  • 作用:从栈中弹出最近的节点,即当前子树的最左节点。
5. 验证节点值
if (root.val <= inorder) {
    return false;
}
inorder = root.val;
  • 验证逻辑
    • 检查当前节点的值 root.val 是否大于上一个访问的节点值 inorder
    • 如果当前节点值小于或等于 inorder,说明违反了 BST 的性质,返回 false
    • 否则,更新 inorder 为当前节点的值,继续遍历。
6. 转向右子树
root = root.right;
  • 作用:访问当前节点的右子树,继续中序遍历。
7. 返回结果
return true;
  • 说明:如果所有节点都满足中序遍历时严格递增的条件,说明该二叉树是一个有效的 BST,返回 true

关键点总结

  • 中序遍历的严格递增性:利用二叉搜索树中序遍历得到的序列严格递增这一特性,可以高效地验证 BST 的有效性。
  • 迭代实现:通过使用栈来模拟递归,实现中序遍历,避免了递归调用带来的额外空间开销。
  • 实时比较:在遍历过程中,通过比较当前节点值与上一个节点值,实时判断是否满足 BST 的性质。

举例说明

让我们通过一个具体的例子,详细理解这个算法的运行过程。

示例:有效的 BST

考虑以下二叉树:

    5
   / \
  3   7
 / \   \
2   4   8

判断过程

  1. 初始化stack = [], inorder = -Infinity, root = 5
  2. 遍历左子树
    • 5 压入栈,移动到 3
    • 3 压入栈,移动到 2
    • 2 压入栈,移动到 null
  3. 访问 2
    • 弹出 2,比较 2 > -Infinity,成立
    • 更新 inorder = 2,移动到 2 的右子节点 null
  4. 访问 3
    • 弹出 3,比较 3 > 2,成立
    • 更新 inorder = 3,移动到 3 的右子节点 4
  5. 访问 4
    • 4 压入栈,移动到 null
    • 弹出 4,比较 4 > 3,成立
    • 更新 inorder = 4,移动到 4 的右子节点 null
  6. 访问 5
    • 弹出 5,比较 5 > 4,成立
    • 更新 inorder = 5,移动到 5 的右子节点 7
  7. 访问右子树
    • 7 压入栈,移动到 null
    • 弹出 7,比较 7 > 5,成立
    • 更新 inorder = 7,移动到 7 的右子节点 8
  8. 访问 8
    • 8 压入栈,移动到 null
    • 弹出 8,比较 8 > 7,成立
    • 更新 inorder = 8,移动到 8 的右子节点 null
  9. 结束:栈为空,rootnull,返回 true

因此,这棵树是一个有效的 BST。

示例:无效的 BST

考虑以下二叉树:

    5
   / \
  1   4
     / \
    3   6

判断过程

  1. 初始化stack = [], inorder = -Infinity, root = 5
  2. 遍历左子树
    • 5 压入栈,移动到 1
    • 1 压入栈,移动到 null
  3. 访问 1
    • 弹出 1,比较 1 > -Infinity,成立
    • 更新 inorder = 1,移动到 1 的右子节点 null
  4. 访问 5
    • 弹出 5,比较 5 > 1,成立
    • 更新 inorder = 5,移动到 5 的右子节点 4
  5. 遍历右子树
    • 4 压入栈,移动到 3
    • 3 压入栈,移动到 null
  6. 访问 3
    • 弹出 3,比较 3 > 5,不成立
    • 算法立即返回 false

因此,这棵树 不是 一个有效的 BST。

时间与空间复杂度

  • 时间复杂度O(n),其中 n 是二叉树的节点数。每个节点被访问一次。
  • 空间复杂度O(h),其中 h 是二叉树的高度。栈的最大空间需求取决于树的高度,最坏情况下(例如链式结构)为 O(n),而对于平衡树则为 O(log n)

方法对比与选择

  • 方法一:递归边界法

    • 优点:逻辑清晰,直接基于 BST 的定义进行验证。
    • 缺点:递归调用可能导致栈溢出,尤其是在深度较大的树上。
  • 方法二:中序遍历

    • 优点:可通过迭代实现,避免递归带来的额外栈空间限制。基于中序遍历的性质,验证过程高效。
    • 缺点:需要维护中序遍历的顺序,逻辑上稍复杂一些。

在实际应用中,选择哪种方法取决于具体需求和个人偏好。对于大多数情况,方法二的迭代实现在空间上更具优势,尤其是在处理深度较大的树时。

拓展阅读

  • 递归与迭代的权衡:在处理树的遍历时,递归实现直观简洁,但在树深度较大时可能导致栈溢出。迭代实现虽然稍显复杂,但在空间效率上更优。

  • 其他验证方法

    • 利用中序遍历生成序列:不仅可以实时比较,还可以先生成整个中序遍历的序列,然后检查该序列是否严格递增。这种方法需要额外的空间存储整个序列。
    • 层序遍历辅助:结合层序遍历和节点值范围检查,尽管不如前两种方法高效,但在特定场景下也可应用。

你可能感兴趣的:(javascript)