【数据结构】【学习笔记】二叉搜索树
我们可以把二叉查找树当作是二分查找算法思想的树形结构体现(二分查找复习)。
在查找数据或寻找适合添加数据的位置时,只要将其和现有的数据比较大小,就可以根据比较结果得知该往哪边移动了。
⚙️定义
二叉搜索树有两个性质:
- 每个结点的值均大于其左子树上任意一个结点的值;
- 每个结点的值均小于其右子树上任意一个结点的值。
🔎特性
比较的次数取决于树的高度。所以如果结点数为n,而且树的形状又较为均衡的话,比较大小和移动的次数最多就是 \(\log_2 n\) 。因此,时间复杂度为 \(O(\log n)\) 。
但是,如果树的形状朝单侧纵向延伸,树就会变得很高,此时时间复杂度也就变成了 \(O(n)\) 。
退化场景:连续顺序插入 [1, 2, 3, 4, 5] 时,BST 会退化为倾斜链表。
演进方向:为解决这一瓶颈,后续演进出了 AVL 树(严格平衡)与 红黑树(近似平衡,C++ std::map / C# SortedDictionary 的底层原理)。
中序遍历(In-order Traversal)结果必定严格递增。
这是做算法题(如验证 BST、求 BST 第 \(k\) 小元素、转换为双向链表等)时频次最高的考点。
🤓实现
添加数据
步骤如下:
先从二叉搜索树的顶端结点开始寻找添加数字的位置,将想要添加的数与该结点中的值进行比较:
- 若添加数小于结点值,则左移;
- 若添加数大于结点值,则右移;
删除结点
步骤如下:
- 当删除的结点没有子结点,直接删除即可;
- 当需要删除的结点只有一个子结点:
- 先删除目标结点;
- 把子结点移动到被删除结点即可。
- 当需要删除的结点有两个子结点:
- 先删除目标结点;
- 找到右子树的最小值(后继节点)或左子树的最大值(前驱节点);
- 将该节点的值复制/覆盖到当前要删除的节点;
- 转为递归删除那个后继/前驱节点(由于它是极值点,最多只有 1 个子节点,直接退化为前两种简单情况)。
查找结点
步骤如下:
- 从二叉搜索树的顶端结点开始往下查找;
- 把查找数和结点值进行比较:
- 小于该结点值则往左搜索;
- 大于该结点值则往右搜索;
❔题目
1. 二叉搜索树中的搜索
解题思路
根据上文实现中的描述,搜索只要注意以下这两点即可:
- 小于该结点值则往左搜索;
- 大于该结点值则往右搜索;
实现
function searchBST(root: TreeNode | null, val: number): TreeNode | null {
while(root !== null){
if(root.val === val){
return root;
}else if(root.val < val){
root = root.right;
}else{
root = root.left;
}
}
return null;
};
public class Solution {
public TreeNode SearchBST(TreeNode root, int val) {
while (root != null) {
if (root.val == val) {
return root;
} else if (root.val < val) {
root = root.right;
} else {
root = root.left;
}
}
return null;
}
}
class Solution {
public:
TreeNode* searchBST(TreeNode* root, int val) {
while (root != nullptr) {
if (root->val == val) {
return root;
} else if (root->val < val) {
root = root->right;
} else {
root = root->left;
}
}
return nullptr;
}
};
复杂度分析
- 时间复杂度:\(O(H)\)
其中 \(H\) 为二叉搜索树的高度。平均情况下树是平衡的,复杂度为 \(O(\log N)\);最坏情况下树退化为单链表,复杂度为 \(O(N)\)。 - 空间复杂度:\(O(1)\)
采用迭代写法,只需常数级额外空间(若使用递归写法,空间复杂度为递归栈开销 \(O(H)\))。
2. 二叉搜索树中的插入操作
解题思路
同样的,可以查看上文知道步骤如下:
- 若添加数小于结点值,则左移;
- 若添加数大于结点值,则右移;
实现
function insertIntoBST(root: TreeNode | null, val: number): TreeNode | null {
// 递归基:到达空位置,创建新节点返回
if (!root) return new TreeNode(val);
if (val < root.val) {
root.left = insertIntoBST(root.left, val); // 插入左子树并更新 left 指针
} else {
root.right = insertIntoBST(root.right, val); // 插入右子树并更新 right 指针
}
return root;
}
public class Solution {
public TreeNode InsertIntoBST(TreeNode root, int val) {
// 递归基:到达空位置,创建新节点返回
if (root == null) return new TreeNode(val);
if (val < root.val) {
root.left = InsertIntoBST(root.left, val); // 插入左子树并更新 left 指针
} else {
root.right = InsertIntoBST(root.right, val); // 插入右子树并更新 right 指针
}
return root;
}
}
class Solution {
public:
TreeNode* insertIntoBST(TreeNode* root, int val) {
TreeNode* newNode = new TreeNode(val);
if (!root) return newNode;
TreeNode* cur = root;
while(cur != nullptr){
if(val < cur->val){
if(cur->left == nullptr){
cur->left = newNode;
break;
}else{
cur = cur->left;
}
}else{
if(cur->right == nullptr){
cur->right = newNode;
break;
}else{
cur = cur->right;
}
}
}
return root;
}
};
class Solution {
public:
TreeNode* insertIntoBST(TreeNode* root, int val) {
if (!root) return new TreeNode(val); // 找到目标位置/原树为空,创建新节点返回
if (val < root->val) {
root->left = insertIntoBST(root->left, val); // 递归插入左子树并接住返回值
} else {
root->right = insertIntoBST(root->right, val); // 递归插入右子树并接住返回值
}
return root;
}
};
复杂度分析
- 时间复杂度:\(O(H)\),其中 \(H\) 为树的高度。
平均复杂度为 \(O(\log N)\),最坏退化为单链表时为 \(O(N)\)。 - 空间复杂度:
- 迭代法:\(O(1)\),只需常数级指针。
- 递归法:\(O(H)\),递归栈调用开销。
3. 删除二叉搜索树中的节点
解题思路
直接引用上文:
步骤如下:
- 当删除的结点没有子结点,直接删除即可;
- 当需要删除的结点只有一个子结点:
- 先删除目标结点;
- 把子结点移动到被删除结点即可。
- 当需要删除的结点有两个子结点:
- 先删除目标结点;
- 找到右子树的最小值(后继节点)或左子树的最大值(前驱节点);
- 将该节点的值复制/覆盖到当前要删除的节点;
- 转为递归删除那个后继/前驱节点(由于它是极值点,最多只有 1 个子节> 点,直接退化为前两种简单情况)。
使用递归法实现:
- 寻找目标节点:根据 BST 性质,小了去左子树,大了去右子树。
- 处理 4 种删除情况:
- 情况 1:没有子节点(叶节点)\(\rightarrow\) 直接返回 null。
- 情况 2:只有右孩子 \(\rightarrow\) 返回右孩子替换当前节点。
- 情况 3:只有左孩子 \(\rightarrow\) 返回左孩子替换当前节点。
- 情况 4:左右孩子都在 \(\rightarrow\) 找到右子树中的最小节点(后继节点),把它的值覆盖到当前节点,然后递归删除右子树中的那个后继节点。
实现
function deleteNode(root: TreeNode | null, key: number): TreeNode | null {
if (!root) return null;
// 1. 寻找目标节点
if (key < root.val) {
root.left = deleteNode(root.left, key);
} else if (key > root.val) {
root.right = deleteNode(root.right, key);
} else {
// 2. 找到目标节点
if (!root.left) return root.right;
if (!root.right) return root.left;
// 找到右子树最小值
let minNode = root.right;
while (minNode.left) {
minNode = minNode.left;
}
// 值覆盖
root.val = minNode.val;
// 递归删除右子树后继节点
root.right = deleteNode(root.right, minNode.val);
}
return root;
}
public class Solution {
public TreeNode DeleteNode(TreeNode root, int key) {
if (root == null) return null;
// 1. 寻找要删除的节点
if (key < root.val) {
// 去左子树删除
root.left = DeleteNode(root.left, key);
} else if (key > root.val) {
// 去右子树删除
root.right = DeleteNode(root.right, key);
} else {
// 2. 找到目标节点,分类讨论删除
// 情况 1 & 2:左子树为空,直接返回右子树
if (root.left == null) return root.right;
// 情况 3:右子树为空,直接返回左子树
if (root.right == null) return root.left;
// 情况 4:左右子树都不为空
// 找到右子树的最小值(后继节点)
TreeNode minNode = root.right;
while (minNode.left != null) {
minNode = minNode.left;
}
// 用后继节点的值覆盖当前节点
root.val = minNode.val;
// 转为递归删除右子树中的那个后继节点
root.right = DeleteNode(root.right, minNode.val);
}
return root;
}
}
class Solution {
public:
TreeNode* deleteNode(TreeNode* root, int key) {
if (!root) return nullptr;
// 1. 寻找要删除的节点
if (key < root->val) {
root->left = deleteNode(root->left, key);
} else if (key > root->val) {
root->right = deleteNode(root->right, key);
} else {
// 2. 找到目标节点
if (!root->left) return root->right;
if (!root->right) return root->left;
// 左右子树都不为空:找右子树最小值
TreeNode* minNode = root->right;
while (minNode->left) {
minNode = minNode->left;
}
// 值覆盖
root->val = minNode->val;
// 递归销毁右子树的后继节点
root->right = deleteNode(root->right, minNode->val);
}
return root;
}
};
复杂度分析
- 时间复杂度:\(O(H)\),其中 \(H\) 为树的高度。
平均复杂度为 \(O(\log N)\),最坏退化为单链表时为 \(O(N)\)。 - 空间复杂度:\(O(H)\)
递归调用的栈空间开销(最坏情况下为 \(O(N)\))。
引用
[1] 力扣探索模式
[2] 宫崎修一,石田保辉. 我的第一本算法书 [M]. 微信读书版
注:本文为个人学习与刷题笔记,部分文本结构与排版格式由 AI 辅助整理。

浙公网安备 33010602011771号