首先,了解 二叉搜索树(Binary Search Tree, 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 的性质。这意味着不仅要检查当前节点的左子节点小于它,右子节点大于它,还要确保整个左子树的所有节点都小于当前节点,右子树的所有节点都大于当前节点。
为了有效地实现这一点,我们可以使用 递归 方法,并在递归过程中维护每个节点值的 上下界:
(-∞, +∞)
范围内。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
。检查空节点:
if (!root) return true;
如果当前节点为空,说明没有违反 BST 的性质,返回 true
。
验证当前节点值:
if (root.val <= min || root.val >= max) {
return false;
}
如果当前节点的值不在 (min, max)
范围内,则违反了 BST 的性质,返回 false
。
递归检查左右子树:
return (
isValidBST(root.left, min, root.val) &&
isValidBST(root.right, root.val, max)
);
isValidBST
检查左子树,将上界更新为当前节点的值 root.val
。isValidBST
检查右子树,将下界更新为当前节点的值 root.val
。(min, max)
,确保节点值严格大于 min
且严格小于 max
,避免重复值。让我们通过一个具体的例子来理解这个算法如何工作。
考虑以下二叉树:
5
/ \
1 4
/ \
3 6
验证这是否是一个有效的 BST。
初始调用:
isValidBST(root = 5, min = -Infinity, max = Infinity)
5
在 (-∞, +∞)
内,继续检查左右子树。检查左子树:
isValidBST(root = 1, min = -Infinity, max = 5)
1
在 (-∞, 5)
内,继续检查左右子树。true
。检查右子树:
isValidBST(root = 4, min = 5, max = +Infinity)
4
不在 (5, +∞)
内,违反 BST 的性质,返回 false
。由于右子树不满足 BST 的性质,整个树被判定为 无效的 BST。
使用之前的有效 BST 示例:
5
/ \
3 7
/ \ \
2 4 8
初始调用:
isValidBST(root = 5, min = -Infinity, max = +Infinity)
5
在 (-∞, +∞)
内,继续检查左右子树。检查左子树:
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
有效。检查右子树:
isValidBST(root = 7, min = 5, max = +Infinity)
7
在 (5, +Infinity)
内,继续检查左右子树。true
。7
的右子树:isValidBST(root = 8, min = 7, max = +Infinity)
8
在 (7, +Infinity)
内,检查左右子节点为空,返回 true
。7
有效。总结:
左右子树都满足 BST 的性质,整个树被判定为 有效的 BST。
通过递归并维护每个节点的值在特定范围内,我们能够高效地验证整个二叉树是否符合 BST 的性质。这种方法的优势在于:
O(n)
,每个节点只被访问一次。O(h)
,其中 h
是树的高度,主要用于递归调用栈。除了递归方法,还有其他验证 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 的性质
};
让我们逐行分析这段代码,理解其工作原理。
let stack = [];
let inorder = -Infinity;
stack
:用于模拟递归调用的栈,存储待访问的节点。inorder
:记录中序遍历中上一个访问的节点值,初始设为负无穷,确保第一个节点的值总是大于它。while (stack.length || root !== null) {
// ...
}
root
不为 null
时,继续循环。这确保了所有节点都会被访问。while (root !== null) {
stack.push(root);
root = root.left;
}
null
)。root = stack.pop();
if (root.val <= inorder) {
return false;
}
inorder = root.val;
root.val
是否大于上一个访问的节点值 inorder
。inorder
,说明违反了 BST 的性质,返回 false
。inorder
为当前节点的值,继续遍历。root = root.right;
return true;
true
。让我们通过一个具体的例子,详细理解这个算法的运行过程。
考虑以下二叉树:
5
/ \
3 7
/ \ \
2 4 8
判断过程:
stack = []
, inorder = -Infinity
, root = 5
5
压入栈,移动到 3
3
压入栈,移动到 2
2
压入栈,移动到 null
2
:
2
,比较 2 > -Infinity
,成立inorder = 2
,移动到 2
的右子节点 null
3
:
3
,比较 3 > 2
,成立inorder = 3
,移动到 3
的右子节点 4
4
:
4
压入栈,移动到 null
4
,比较 4 > 3
,成立inorder = 4
,移动到 4
的右子节点 null
5
:
5
,比较 5 > 4
,成立inorder = 5
,移动到 5
的右子节点 7
7
压入栈,移动到 null
7
,比较 7 > 5
,成立inorder = 7
,移动到 7
的右子节点 8
8
:
8
压入栈,移动到 null
8
,比较 8 > 7
,成立inorder = 8
,移动到 8
的右子节点 null
root
为 null
,返回 true
因此,这棵树是一个有效的 BST。
考虑以下二叉树:
5
/ \
1 4
/ \
3 6
判断过程:
stack = []
, inorder = -Infinity
, root = 5
5
压入栈,移动到 1
1
压入栈,移动到 null
1
:
1
,比较 1 > -Infinity
,成立inorder = 1
,移动到 1
的右子节点 null
5
:
5
,比较 5 > 1
,成立inorder = 5
,移动到 5
的右子节点 4
4
压入栈,移动到 3
3
压入栈,移动到 null
3
:
3
,比较 3 > 5
,不成立false
因此,这棵树 不是 一个有效的 BST。
O(n)
,其中 n
是二叉树的节点数。每个节点被访问一次。O(h)
,其中 h
是二叉树的高度。栈的最大空间需求取决于树的高度,最坏情况下(例如链式结构)为 O(n)
,而对于平衡树则为 O(log n)
。方法一:递归边界法:
方法二:中序遍历:
在实际应用中,选择哪种方法取决于具体需求和个人偏好。对于大多数情况,方法二的迭代实现在空间上更具优势,尤其是在处理深度较大的树时。
递归与迭代的权衡:在处理树的遍历时,递归实现直观简洁,但在树深度较大时可能导致栈溢出。迭代实现虽然稍显复杂,但在空间效率上更优。
其他验证方法: