二叉搜索树:从定义到实战操作全解析 🌳
一、什么是二叉搜索树?📚
二叉搜索树(Binary Search Tree,简称 BST)是一种特殊的二叉树,它的存在让数据的查找、插入和删除操作变得高效又有序。具体来说,它有两个核心定义:
- 要么是一棵空树;
- 要么是由根节点、左子树和右子树组成的树,且左子树和右子树都是二叉搜索树,左子树的所有节点值都小于根节点值,右子树的所有节点值都大于根节点值(左子树 < 根节点 < 右子树)。
正因为这种严格的大小关系,二叉搜索树也被称为 "排序二叉树"。它的一个神奇特性是:中序遍历的结果是严格递增的有序序列 !这让它在需要有序数据的场景中大放异彩。
例如:
6
/ \
3 8
/ \ / \
1 4 7 9
对这棵 BST 进行中序遍历,结果为:1 → 3 → 4 → 6 → 7 → 8 → 9,完美呈现递增有序。
从时间复杂度来看,二叉搜索树的平均操作效率是 O (logn)(类似二分查找),但在最坏情况下(比如所有节点都只有左子树或右子树,变成 "斜树"),效率会退化为 O (n)。不过总体来说,它依然是支持高效查找的得力助手~
二、二叉搜索树的相关操作 🔧
二叉搜索树的核心操作有三个,也是我们日常使用中最频繁的功能:
- 查找数据域为某一特定值的节点 🔍
- 插入一个新的节点 ➕
- 删除一个指定节点 ➖
接下来我们就逐一拆解这些操作的逻辑和实现~
三、查找数据域为某一特定值的节点 🔍
查找是二叉搜索树最基础的操作,得益于它 "左小右大" 的特性,我们可以像 "走捷径" 一样定位目标节点。
查找步骤:
-
起始:从树的根节点开始(如果根节点为空,直接返回 null,说明树中无数据);
-
循环对比 + 定向查找:
- 若当前节点值等于目标值:找到啦!返回该节点;
- 若当前节点值大于目标值:目标一定在左子树(因为左子树都比根小),更新当前节点为左子节点;
- 若当前节点值小于目标值:目标一定在右子树(因为右子树都比根大),更新当前节点为右子节点;
-
终止:要么找到目标节点,要么遍历到 null(说明目标不存在)。
核心逻辑:
靠 BST 的有序性 "精准导航",不盲目遍历,只走一条路,全程无回溯,效率自然高~
代码实现:
javascript
// 查找值为n的节点
function search(root, n) {
// 根节点为空,直接返回(树中无数据)
if (!root) {
return null;
}
// 找到目标节点,返回该节点
if (root.val === n) {
return root;
} else if (root.val > n) {
// 当前节点值大于目标值,去左子树找
return search(root.left, n);
} else {
// 当前节点值小于目标值,去右子树找
return search(root.right, n);
}
}
// 测试用例:构造一棵二叉搜索树
class TreeNode {
constructor(val) {
this.val = val;
this.left = null;
this.right = null;
}
}
const root = new TreeNode(6);
root.left = new TreeNode(3);
root.right = new TreeNode(8);
root.left.left = new TreeNode(1);
root.left.right = new TreeNode(4);
root.right.left = new TreeNode(7);
root.right.right = new TreeNode(9);
console.log(search(root, 4)); // TreeNode { val: 4, left: null, right: null }
四、插入一个新的节点 ➕
插入操作的核心是 "找到唯一合法的空位",并保证插入后依然满足 BST 的有序性。
插入步骤:
-
起始:从树的根节点开始;
-
循环对比 + 找插入空位:
- 若当前节点值大于新节点值:往左子树走(更新当前节点为左子节点);
- 若当前节点值小于新节点值:往右子树走(更新当前节点为右子节点);
- 一直走直到遇到 null(这个 null 就是唯一合法的插入位置);
-
插入 + 终止:把新节点挂在这个 null 的位置(作为对应父节点的左 / 右子节点),插入完成。
核心逻辑:
新节点一定是 "叶子节点"(即没有子节点),因为我们是沿着 BST 的规则找到最末端的空位,不会破坏原有节点的结构和大小关系。
代码实现:
javascript
// 向BST中插入值为n的节点
function insertIntoBst(root, n) {
// 若根节点为空,直接返回新节点(树为空时,新节点就是根)
if (!root) {
return new TreeNode(n);
}
// 不考虑值相等的情况(BST通常不允许重复值,若需支持可额外处理)
if (root.val > n) {
// 当前节点值大于新值,往左子树插入(递归更新左子树)
root.left = insertIntoBst(root.left, n);
} else {
// 当前节点值小于新值,往右子树插入(递归更新右子树)
root.right = insertIntoBst(root.right, n);
}
// 返回更新后的根节点
return root;
}
为什么 "只能在原树叶子节点后插入新节点"?
- 原树的非叶子节点已有子节点,无合法空位容纳新节点,直接插入会违背二叉树 / BST 的结构规则,破坏有序性和数据完整性,增加插入的难度;
- 任何合法的 BST 插入,都必须依托 "原树的末端空位"(即原树叶子节点的左 / 右子节点空位,或原树的空节点),这个空位是唯一无需改动原有节点就能容纳新节点的位置;
- 在原树叶子节点后插入,既能保证新节点插入时是叶子节点(无子女,不冲击原有 BST 规则),又能保证不改动任何原有节点的结构,维持 O (logn) 的高效插入效率,这是保证 BST 有序性和高效性的唯一最优解。
五、删除一个指定节点 ➖
删除操作是 BST 中最复杂的操作,因为删除节点后需要维持 "左小右大" 的规则,需要分情况处理。
操作步骤:
- 找到目标节点:从根节点开始,根据值的大小向左 / 右子树查找,直到找到值等于目标值的节点;
- 按 3 种情况处理目标节点(核心中的核心):
情况 1:目标节点是叶子节点(无左右子树)
直接把它置为 null 即可(父节点的对应指针指向 null),因为删除叶子节点不会影响其他节点的大小关系。
情况 2:目标节点有左子树(无论有没有右子树)
用左子树中最大的节点替换目标节点的值(左子树最大节点是最右侧的节点),然后删除左子树中这个最大节点(此时这个最大节点一定是叶子节点或只有左子树,递归处理即可)。
情况 3:目标节点只有右子树(无左子树)
用右子树中最小的节点替换目标节点的值(右子树最小节点是最左侧的节点),然后删除右子树中这个最小节点(同理,这个节点也容易处理)。
核心总结:
先定向找到目标节点,再按 "叶子直接删、有左找左最大、仅右找右最小" 的规则替换 + 删冗余,全程不破坏 BST 有序性。
代码实现:
javascript
// 删除值为n的节点
function deleteNode(root, n) {
// 根节点为空,直接返回
if (!root) {
return null;
}
// 找到目标节点
if (root.val === n) {
// 情况1:叶子节点(无左右子树)
if (!root.left && !root.right) {
root = null;
} else if (root.left) {
// 情况2:有左子树(无论是否有右子树)
// 找左子树最大节点
const maxLeft = findMax(root.left);
// 用最大值替换当前节点值
root.val = maxLeft.val;
// 递归删除左子树中这个最大节点
root.left = deleteNode(root.left, maxLeft.val);
} else {
// 情况3:只有右子树(无左子树)
// 找右子树最小节点
const minRight = findMin(root.right);
// 用最小值替换当前节点值
root.val = minRight.val;
// 递归删除右子树中这个最小节点
root.right = deleteNode(root.right, minRight.val);
}
} else if (root.val > n) {
// 目标在左子树,递归删除左子树
root.left = deleteNode(root.left, n);
} else {
// 目标在右子树,递归删除右子树
root.right = deleteNode(root.right, n);
}
return root;
}
// 寻找左子树最大值(最右侧节点)
function findMax(root) {
while (root.right) {
root = root.right;
}
return root;
}
// 寻找右子树最小值(最左侧节点)
function findMin(root) {
while (root.left) {
root = root.left;
}
return root;
}
思考与拓展:
1.目标节点有左子树且有右子树时,也可以用右子树里的最小值来替换目标节点的值,再去右子树里删除这个最小值节点,那为什么情况2与情况3不能合并?
情况 2 的条件是 "有左子树"(可能同时有右子树),情况 3 的条件是 "只有右子树"(无左子树),两者的处理逻辑不同:
- 情况 2 需要从左子树找最大值(左子树存在的前提下);
- 情况 3 只能从右子树找最小值(因为左子树不存在)。
如果合并,比如统一用右子树最小值处理,当节点只有左子树时(无右子树),就会出现 "右子树为 null" 的错误。
举个例子:若目标节点是值为 3 的节点(左子树有 1,无右子树),此时只能用左子树最大值(1)替换;若强行找右子树最小值,会因右子树为 null 报错。因此必须分开处理~
2. 可不可以让左子树存在时优先用右子树最小值?
当然可以!只要保证替换后依然满足 BST 规则即可。此时情况划分变为:
- 情况 1:叶子节点→直接删;
- 情况 2:有右子树(无论有没有左子树)→用右子树最小值替换,再删该最小值节点;
- 情况 3:只有左子树(无右子树)→用左子树最大值替换,再删该最大值节点。
对应的代码实现:
javascript
function deleteNodeV2(root, n) {
if (!root) return null;
if (root.val === n) {
// 情况1:叶子节点
if (!root.left && !root.right) {
root = null;
} else if (root.right) {
// 情况2:有右子树(无论是否有左子树)
const minRight = findMin(root.right);
root.val = minRight.val;
root.right = deleteNodeV2(root.right, minRight.val);
} else {
// 情况3:只有左子树(无右子树)
const maxLeft = findMax(root.left);
root.val = maxLeft.val;
root.left = deleteNodeV2(root.left, maxLeft.val);
}
} else if (root.val > n) {
root.left = deleteNodeV2(root.left, n);
} else {
root.right = deleteNodeV2(root.right, n);
}
return root;
}
六、面试官会问什么?🤔
- 二叉搜索树的中序遍历有什么特点? 答:中序遍历结果是严格递增的有序序列,这是 BST 最核心的特性,也是判断一棵二叉树是否为 BST 的重要依据。
- 为什么二叉搜索树的最坏时间复杂度是 O (n)? 答:当 BST 退化为 "斜树"(所有节点都只有左子树或右子树)时,查找 / 插入 / 删除操作需要遍历所有节点,此时复杂度为 O (n)。因此实际应用中常使用平衡二叉树(如 AVL 树、红黑树)优化。
- 删除节点时,为什么要用左子树最大值或右子树最小值替换? 答:因为这两个值是最接近目标节点值的,替换后能保证左子树依然全小于新值、右子树依然全大于新值,维持 BST 的有序性。
- 如何验证一棵二叉树是二叉搜索树? 答:可以通过中序遍历,检查结果是否严格递增;也可以递归验证,确保每个节点的左子树最大值小于它,右子树最小值大于它。
七、结语 🌟
二叉搜索树是数据结构中的 "劳模",凭借 "左小右大" 的简单规则,实现了高效的查找、插入和删除操作。掌握它的核心逻辑 ------ 利用有序性减少无效遍历,理解删除操作的三种情况处理,不仅能应对面试,更能在实际开发中灵活运用(比如实现有序映射、索引等)。
希望这篇文章能帮你理清 BST 的脉络,下次遇到相关问题时,也能像 BST 的查找操作一样 "直击要害" 哦!💪