文章目录
🚩前言:为什么要学二叉搜索树?
我们先看一个核心痛点:日常开发中,我们需要一个既能快速查找,又能高效动态插入 / 删除的数据结构。对比常用结构:
| 数据结构 | 查找效率 | 插入效率 | 删除效率 | 核心痛点 |
|---|---|---|---|---|
| 无序数组 | O(n) | O(1) | O(n) | 查找极慢 |
| 有序数组 | O(logn) | O(n) | O(n) | 插入删除需移动大量元素 |
| 单链表 | O(n) | O(1) | O(1) | 查找必须遍历 |
| 哈希表 | O(1) | O(1) | O(1) | 无序、不支持范围查询、有哈希冲突 |
而二叉搜索树 (Binary Search Tree, BST) 完美解决了这个矛盾:平均情况下,查找、插入、删除的时间复杂度均为 O (logn),同时天然支持有序遍历和范围查询。
一、基础认知篇:二叉搜索树的核心定义与性质🪁
1.1 前置知识:二叉树基础快速回顾
二叉树是每个节点最多有 2 个子节点的树形结构,核心术语:
- 根节点:树的顶层节点,无父节点
- 叶子节点:无左右子节点的节点
- 左 / 右孩子:节点的左 / 右子节点
- 子树:以某个节点为根的完整树结构
- 高度:从当前节点到最远叶子节点的边数
- 深度:从根节点到当前节点的边数
通俗比喻:把二叉树比作家族族谱,根节点是老祖宗,每个节点最多两个儿子(左儿子、右儿子),每个儿子又可以有自己的后代。
1.2 二叉搜索树的严格定义✒️
二叉搜索树(也叫二叉查找树、有序二叉树),要么是一棵空树,要么必须同时满足以下 3 条核心性质:
- 左子树规则:若节点的左子树不为空,则左子树上所有节点的值 均严格小于该节点的值
- 右子树规则:若节点的右子树不为空,则右子树上所有节点的值 均严格大于该节点的值
- 递归规则:节点的左、右子树,本身也必须是二叉搜索树
🚨 易踩坑点:只判断节点的直接左 / 右孩子是否符合规则,忽略了所有子树节点。比如根节点 5,左孩子 3,左孩子的右孩子 6,虽然 3<5、6>3,但 6>5,违反了左子树所有节点必须小于根节点的规则,这不是一棵合法的 BST。
1.3 合法 / 非法 BST 示例
- 合法 BST 示例
8 # 根节点
/ \
3 10 # 3<8,10>8,符合规则
/ \ \
1 6 14 # 1<3,6>3;14>10,符合规则
/ \ /
4 7 13 # 4<6,7>6;13<14,符合规则
# 中序遍历结果:1 3 4 6 7 8 10 13 14(天然升序)
- 非法 BST 示例
5
/ \
3 7
/ \
2 6 # 错误点:6>根节点5,但位于根的左子树,违反BST核心规则
1.4 BST 的核心特性
- 有序性:左子树所有节点值 < 根节点值 < 右子树所有节点值。
- 中序遍历特性:对 BST 做中序遍历,结果一定是严格递增序列。
- 二分查找特性:所有操作都基于二分思想,平均时间复杂度 O (logn)。
- 递归结构:任意一颗子树本身也满足BST规则,天然适合递归实现。
- 前驱后继天然存在:任意节点都能快速找到中序前驱(前一个更小值)和中序后继(后一个更大值)。
- 动态扩展性:插入删除无需移动大量元素,仅需修改指针指向
- 范围查询友好:天然支持查找 [a,b] 区间内的所有元素,这是哈希表无法实现的核心优势
二、核心原理篇:BST 的核心操作算法拆解🎯
2.1 查找操作🔎
-
核心原理
从根节点出发:- 目标值 == 当前节点值:找到目标,返回节点
- 目标值 < 当前节点值:目标一定在左子树,去左子树查找
- 目标值 > 当前节点值:目标一定在右子树,去右子树查找
- 遍历到空节点:树中无目标值,查找失败
-
扩展查找:最值、前驱、后继
- 查找最小值:一路向左,直到左孩子为空的节点,就是 BST 的最小值
- 查找最大值:一路向右,直到右孩子为空的节点,就是 BST 的最大值
- 中序后继:比当前节点大的最小节点(右子树的最小值)
- 中序前驱:比当前节点小的最大节点(左子树的最大值)
-
查找步骤可视化
以上文合法 BST 为例,查找值为 6 的节点:
步骤1:根节点8,6<8 → 进入左子树
步骤2:节点3,6>3 → 进入右子树
步骤3:节点6,6==6 → 查找成功,返回节点
2.2 插入操作
-
核心原理
插入的核心是找到符合 BST 规则的叶子节点空位,插入的新节点一定是叶子节点,不会修改原有树的结构(仅修改父节点的指针),步骤:- 从根节点出发,按照查找规则,找到合适的父节点
- 若目标值已存在:按需求处理(禁止重复 / 计数累加)
- 若目标值小于父节点值:挂载到父节点的左孩子
- 若目标值大于父节点值:挂载到父节点的右孩子
-
插入步骤可视化
以上文合法 BST 为例,插入值为 2 的节点:
步骤1:根节点8,2<8 → 左子树
步骤2:节点3,2<3 → 左子树
步骤3:节点1,2>1 → 右子树
步骤4:节点1的右孩子为空 → 挂载新节点2到此处
插入后的树:
8
/ \
3 10
/ \ \
1 6 14
\ / \ /
2 4 7 13
2.3 删除操作
删除操作的难点在于:删除节点后,必须保持 BST 的核心性质不变。我们按节点的子节点数量,分为 3 种场景,逐个拆解,用「员工离职交接」的比喻辅助理解。
场景 1:待删除节点是叶子节点(无左右孩子)
- 比喻:普通员工离职,无下属,直接取消岗位即可
- 操作:直接将父节点对应的左 / 右指针置空,释放节点内存
场景 2:待删除节点只有一个孩子(左或右)
- 比喻:主管离职,只有一个下属,直接让下属接替主管的位置
- 操作:将父节点对应的指针,指向待删除节点的孩子,释放节点内存
场景 3:待删除节点有左右两个孩子(核心难点)
-
比喻:部门经理离职,有两个完整的团队,不能直接拆分,必须找一个完美的接班人
-
核心思路:找替身交接,用待删除节点的「中序后继」(右子树的最小值)或「中序前驱」(左子树的最大值)作为替身
-
替身的特性:中序后继是右子树的最小值,一定没有左孩子(否则就不是最小值),所以删除替身最多只会触发场景1或2,不会循环嵌套
-
操作步骤:
- 步骤 1:找到待删除节点的中序后继
- 步骤 2:将后继节点的值复制到待删除节点
- 步骤 3:删除原来的后继节点(此时后继节点最多只有一个右孩子,用场景1或2 处理)
-
-
删除步骤可视化(场景 3)
以上文插入后的 BST 为例,删除值为 3 的节点(有左右两个孩子):
# 原树
8
/ \
3 10
/ \ \
1 6 14
\ / \ /
2 4 7 13
步骤1:找到节点3的中序后继(右子树的最小值)→ 节点4
步骤2:将节点3的值替换为4,树变为:
8
/ \
4 10
/ \ \
1 6 14
\ / \ /
2 4 7 13
步骤3:删除原来的节点4(叶子节点,场景1处理),最终树:
8
/ \
4 10
/ \ \
1 6 14
\ \ /
2 7 13
# 中序遍历结果:1 2 4 6 7 8 10 13 14,依然保持升序,BST性质不变
Q:删除两个孩子的节点,为什么一定要用中继后序❓
A:中序后继是比当前节点大的最小节点,替换后不会破坏BST的升序性质,且后继节点一定没有左孩子,删除时不会出现嵌套的复杂场景,是最优的替身选择。
2.4 遍历操作
遍历是指按固定顺序访问树中所有节点,BST 的遍历核心是中序遍历。
| 遍历方式 | 访问顺序 | BST 特性 | 核心应用场景 |
|---|---|---|---|
| 中序遍历 | 左子树 → 根节点 → 右子树 | 得到严格升序的序列 | 验证 BST、有序输出、范围查询 |
| 前序遍历 | 根节点 → 左子树 → 右子树 | 保留树的结构,可用于序列化 | 树的复制、序列化存储 |
| 后序遍历 | 左子树 → 右子树 → 根节点 | 先处理孩子再处理根 | 树的销毁、子树统计 |
| 层序遍历 | 按层级从上到下、从左到右 | 按层次访问节点 | 求树的高度、最短路径 |
三、代码实现篇🚀
3.1 基础结构定义🖋️
首先定义 BST 的节点结构和通用枚举、返回值定义。
#include <stdio.h>
#include <stdlib.h>
#include <stdbool.h>
//BST操作错误码枚举
typedef enum {
BST_OK = 0, // 操作成功
BST_ERROR_NULL = -1, // 空指针异常
BST_ERROR_MALLOC = -2, // 内存分配失败
BST_ERROR_NOT_EXIST = -3, // 节点不存在
BST_ERROR_DUPLICATE = -4 // 重复值(禁止重复模式下)
} BST_Error;
/**
* @brief 二叉搜索树节点结构定义
* @param data 节点存储的整型数据
* @param count 重复值计数,避免重复节点破坏BST性质
* @param left 左孩子节点指针
* @param right 右孩子节点指针
*/
typedef struct BSTNode {
int data;
int count;
struct BSTNode *left;
struct BSTNode *right;
} BSTNode;
3.2 基础工具函数实现
3.2.1 节点创建函数
/**
* @brief 创建一个新的BST节点
* @param data 节点要存储的整型数据
* @return 成功返回新节点的指针,失败返回NULL
* @note 节点的左右孩子初始化为NULL,count初始化为1,需手动挂载到树上
*/
BSTNode* BST_CreateNode(int data) {
// 分配节点内存
BSTNode *newNode = (BSTNode*)malloc(sizeof(BSTNode));
if (newNode == NULL) {
perror("BST_CreateNode: malloc failed");
return NULL;
}
// 初始化节点所有字段
newNode->data = data;
newNode->count = 1; // 初始计数为1
newNode->left = NULL;
newNode->right = NULL;
return newNode;
}
3.2.2 树的销毁函数
/**
* @brief 递归销毁BST的所有节点,释放内存
* @param root 二叉搜索树的根节点指针的地址(二级指针,用于置空根节点)
* @return BST_Error 错误码,成功返回BST_OK,空指针返回BST_ERROR_NULL
* @note 采用后序遍历,先销毁左右子树,再销毁根节点,避免内存泄漏
* @warning 必须传入二级指针,否则根节点会变成野指针
*/
BST_Error BST_Destroy(BSTNode **root) {
if (root == NULL) {
printf("BST_Destroy: root pointer is NULL\n");
return BST_ERROR_NULL;
}
// 递归终止条件:当前节点为空,无需处理
if (*root == NULL) {
return BST_OK;
}
// 后序遍历:先销毁左子树,再销毁右子树
BST_Destroy(&(*root)->left);
BST_Destroy(&(*root)->right);
// 最后销毁当前节点
free(*root);
*root = NULL;
return BST_OK;
}
3.3 查找操作实现
3.3.1 递归版本查找
/**
* @brief 递归方式查找BST中指定值的节点
* @param root 二叉搜索树的根节点指针
* @param target 要查找的目标值
* @return 找到返回对应节点的指针,未找到/入参为空返回NULL
* @note 递归实现,代码简洁,但树退化成链表时可能栈溢出
*/
BSTNode* BST_Search_Recursive(BSTNode *root, int target) {
// 递归终止条件:根为空(未找到)或找到目标值
if (root == NULL || root->data == target) {
return root;
}
// 目标值小于当前节点,递归查找左子树
if (target < root->data) {
return BST_Search_Recursive(root->left, target);
}
// 目标值大于当前节点,递归查找右子树
else {
return BST_Search_Recursive(root->right, target);
}
}
3.3.2 迭代版本查找
/**
* @brief 迭代方式查找BST中指定值的节点
* @param root 二叉搜索树的根节点指针
* @param target 要查找的目标值
* @return 找到返回对应节点的指针,未找到/入参为空返回NULL
* @note 迭代实现,无递归栈溢出风险,无函数调用开销
*/
BSTNode* BST_Search_Iterative(BSTNode *root, int target) {
// 入参非空检查
if (root == NULL) {
return NULL;
}
// 定义当前遍历节点,从根节点开始
BSTNode *curr = root;
// 循环遍历,直到节点为空或找到目标
while (curr != NULL) {
// 找到目标节点,直接返回
if (target == curr->data) {
return curr;
}
// 目标值更小,去左子树
else if (target < curr->data) {
curr = curr->left;
}
// 目标值更大,去右子树
else {
curr = curr->right;
}
}
// 遍历完成,未找到目标节点
return NULL;
}
3.3.3 最值查找函数
/**
* @brief 查找BST中的最小值节点
* @param root 二叉搜索树的根节点指针
* @return 成功返回最小值节点指针,空树返回NULL
* @note 一路向左遍历,直到左孩子为空的节点就是最小值
*/
BSTNode* BST_FindMin(BSTNode *root) {
if (root == NULL) {
return NULL;
}
// 迭代遍历,左孩子不为空就继续向左
while (root->left != NULL) {
root = root->left;
}
return root;
}
/**
* @brief 查找BST中的最大值节点
* @param root 二叉搜索树的根节点指针
* @return 成功返回最大值节点指针,空树返回NULL
* @note 一路向右遍历,直到右孩子为空的节点就是最大值
*/
BSTNode* BST_FindMax(BSTNode *root) {
if (root == NULL) {
return NULL;
}
// 迭代遍历,右孩子不为空就继续向右
while (root->right != NULL) {
root = root->right;
}
return root;
}
3.4 插入操作实现
3.4.1 递归版本插入
/**
* @brief 递归方式向BST中插入节点
* @param root 二叉搜索树的根节点指针
* @param data 要插入的数据
* @return 插入后的根节点指针(插入第一个节点时根节点会变化)
* @note 重复值会自动累加count计数,不会创建新节点
*/
BSTNode* BST_Insert_Recursive(BSTNode *root, int data) {
// 递归终止条件:找到空位,创建新节点返回
if (root == NULL) {
return BST_CreateNode(data);
}
// 插入值小于当前节点,递归插入左子树
if (data < root->data) {
root->left = BST_Insert_Recursive(root->left, data);
}
// 插入值大于当前节点,递归插入右子树
else if (data > root->data) {
root->right = BST_Insert_Recursive(root->right, data);
}
// 插入值已存在,累加计数,无需创建新节点
else {
root->count++;
}
// 返回当前节点,保持树结构不变
return root;
}
3.4.2 迭代版本插入
/**
* @brief 迭代方式向BST中插入节点
* @param root 二叉搜索树的根节点指针的地址(二级指针,处理根节点为空的情况)
* @param data 要插入的数据
* @return BST_Error 错误码,成功返回BST_OK
* @note 重复值累加count计数,无递归栈溢出风险
*/
BST_Error BST_Insert_Iterative(BSTNode **root, int data) {
if (root == NULL) {
return BST_ERROR_NULL;
}
// 空树:直接创建根节点
if (*root == NULL) {
*root = BST_CreateNode(data);
return (*root == NULL) ? BST_ERROR_MALLOC : BST_OK;
}
// 定义当前遍历节点和父节点
BSTNode *curr = *root;
BSTNode *parent = NULL;
// 循环找到插入位置
while (curr != NULL) {
parent = curr;
// 插入值已存在,累加计数,直接返回
if (data == curr->data) {
curr->count++;
return BST_OK;
}
// 插入值更小,去左子树
else if (data < curr->data) {
curr = curr->left;
}
// 插入值更大,去右子树
else {
curr = curr->right;
}
}
// 此时curr为空,parent是插入节点的父节点,创建新节点
BSTNode *newNode = BST_CreateNode(data);
if (newNode == NULL) {
return BST_ERROR_MALLOC;
}
// 挂载到父节点的对应位置
if (data < parent->data) {
parent->left = newNode;
} else {
parent->right = newNode;
}
return BST_OK;
}
3.5 删除操作实现
3.5.1 递归版本删除
/**
* @brief 递归方式删除BST中指定值的节点
* @param root 二叉搜索树的根节点指针
* @param data 要删除的数据
* @return 删除后的根节点指针
* @note 重复值先递减count,count为0时才删除节点
*/
BSTNode* BST_Delete_Recursive(BSTNode *root, int data) {
// 递归终止条件:节点为空,未找到要删除的节点
if (root == NULL) {
return NULL;
}
// 目标值小于当前节点,递归删除左子树
if (data < root->data) {
root->left = BST_Delete_Recursive(root->left, data);
}
// 目标值大于当前节点,递归删除右子树
else if (data > root->data) {
root->right = BST_Delete_Recursive(root->right, data);
}
// 找到目标节点,执行删除逻辑
else {
// 处理重复值:count>1时,递减计数,不删除节点
if (root->count > 1) {
root->count--;
return root;
}
// 场景1:叶子节点(无左右孩子),直接释放
if (root->left == NULL && root->right == NULL) {
free(root);
return NULL;
}
// 场景2:只有一个孩子,返回孩子节点,替换当前节点
else if (root->left == NULL) { // 只有右孩子
BSTNode *temp = root->right;
free(root);
return temp;
} else if (root->right == NULL) { // 只有左孩子
BSTNode *temp = root->left;
free(root);
return temp;
} else {
// 场景3:有两个孩子,核心难点
// 步骤1:找到右子树的最小值(中序后继)
BSTNode *successor = BST_FindMin(root->right);
// 步骤2:将后继节点的值和计数复制到当前节点
root->data = successor->data;
root->count = successor->count;
// 步骤3:递归删除后继节点(后继节点最多只有一个右孩子)
root->right = BST_Delete_Recursive(root->right, successor->data);
// 重置后继节点的计数为1,确保删除
successor->count = 1;
}
}
return root;
}
3.5.2 迭代版本删除
/**
* @brief 迭代方式删除BST中指定值的节点
* @param root 二叉搜索树的根节点指针的地址(二级指针,处理根节点删除)
* @param data 要删除的数据
* @return BST_Error 错误码,成功返回BST_OK,节点不存在返回BST_ERROR_NOT_EXIST
* @note 无递归栈溢出风险,重复值先递减count
*/
BST_Error BST_Delete_Iterative(BSTNode **root, int data) {
if (root == NULL || *root == NULL) {
return BST_ERROR_NULL;
}
// 定义当前节点和父节点
BSTNode *curr = *root;
BSTNode *parent = NULL;
// 步骤1:找到要删除的节点和其父节点
while (curr != NULL && curr->data != data) {
parent = curr;
if (data < curr->data) {
curr = curr->left;
} else {
curr = curr->right;
}
}
// 未找到目标节点
if (curr == NULL) {
return BST_ERROR_NOT_EXIST;
}
// 处理重复值:count>1时,递减计数,不删除节点
if (curr->count > 1) {
curr->count--;
return BST_OK;
}
// 步骤2:分场景处理删除
// 场景1+2:待删除节点最多只有一个孩子
if (curr->left == NULL || curr->right == NULL) {
// 找到要替换的孩子节点
BSTNode *child = (curr->left != NULL) ? curr->left : curr->right;
// 待删除节点是根节点
if (parent == NULL) {
*root = child;
}
// 挂载到父节点的对应位置
else if (parent->left == curr) {
parent->left = child;
} else {
parent->right = child;
}
// 释放节点内存
free(curr);
}
// 场景3:待删除节点有两个孩子
else {
// 找到中序后继(右子树的最小值)和后继的父节点
BSTNode *succParent = curr;
BSTNode *successor = curr->right;
// 一路向左找最小值
while (successor->left != NULL) {
succParent = successor;
successor = successor->left;
}
// 复制后继节点的值和计数到当前节点
curr->data = successor->data;
curr->count = successor->count;
// 删除后继节点:后继节点最多只有一个右孩子
if (succParent == curr) {
succParent->right = successor->right;
} else {
succParent->left = successor->right;
}
// 释放后继节点内存
free(successor);
}
return BST_OK;
}
3.6 遍历操作实现
/**
* @brief 递归中序遍历打印BST节点
* @param root 二叉搜索树的根节点指针
* @return BST_Error 错误码,成功返回BST_OK
*/
BST_Error BST_InOrder_Print_Recursive(BSTNode *root) {
if (root == NULL) {
return BST_OK;
}
// 中序遍历:左子树 → 根节点 → 右子树
BST_InOrder_Print_Recursive(root->left);
// 直接打印节点值和计数,重复值会显示count
printf("%d(count:%d) ", root->data, root->count);
BST_InOrder_Print_Recursive(root->right);
return BST_OK;
}
四、性能分析与优化方案🔧
4.1 时间复杂度分析
| 操作 | 平均时间复杂度 | 最坏时间复杂度 | 说明 |
|---|---|---|---|
| 查找 | O(logn) | O(n) | 最坏情况:BST 退化成单链表 |
| 插入 | O(logn) | O(n) | 最坏情况:有序序列插入,退化成链表 |
| 删除 | O(logn) | O(n) | 最坏情况:退化成链表,删除叶子节点 |
| 遍历 | O(n) | O(n) | 必须访问所有节点,无优化空间 |
核心问题:BST 的性能完全取决于树的高度,当插入有序序列时,BST 会退化成单链表,高度为 n,性能暴跌到 O (n)。
4.2 优化方案
4.2.1 解决退化问题:平衡二叉搜索树
这是解决 BST 退化的根本方案,核心是让树的左右子树高度尽可能平衡,保证树的高度始终是 O (logn)。
- AVL 树:严格平衡二叉树,左右子树高度差不超过 1,查找性能最优,插入删除有旋转开销
- 红黑树:近似平衡二叉树,工业级最常用,Linux 内核、C++ STL map/set、Java TreeMap 的底层实现,插入删除旋转次数更少,性能更稳定
- B 树 / B + 树:多路平衡搜索树,数据库索引的核心实现,减少磁盘 IO 次数,适合外存存储
4.2.2 工程性能优化
- 重复值计数优化:用 count 字段处理重复值,避免创建重复节点,减少树的高度
- 节点复用:删除的节点不直接释放,放入空闲链表,插入时优先从空闲链表获取,减少内存分配开销
- 迭代优先原则:所有核心操作优先使用迭代实现,避免递归栈溢出,同时减少函数调用开销
- 内存池优化:避免频繁 malloc/free 节点,预分配一块连续的内存池,节点从内存池分配,减少内存碎片和系统调用开销
五、实战刷题篇:力扣经典 BST 题目详解⚔️
5.1 力扣 700. 二叉搜索树中的搜索
- 题目描述
给定二叉搜索树(BST)的根节点 root 和一个整数值 val。
你需要在 BST 中找到节点值等于 val 的节点。 返回以该节点为根的子树。 如果节点不存在,则返回 null 。
示例 :
- 解题思路
- 解法1:递归法
终止条件:若当前节点为空(未找到)或当前节点值等于 val(找到),直接返回当前节点;
递归逻辑:
- 若 val < 当前节点值:目标在左子树,递归搜索左子树;
- 若 val > 当前节点值:目标在右子树,递归搜索右子树。
- 解法2:迭代法
用循环 + 指针代替递归栈,避免递归的栈空间开销:初始化指针 curr 指向根节点;
循环遍历:
- 若 curr 为空,退出循环(未找到);
- 若 curr 若 val == curr->val,找到目标,返回 curr;
- 若 curr 若 val < curr->val,curr 指向左子树;
- 若 curr 若 val > curr->val,curr 指向右子树;
- 若 curr 循环结束未找到,返回 NULL。
- 代码实现
/**
* Definition for a binary tree node.
* struct TreeNode {
* int val;
* struct TreeNode *left;
* struct TreeNode *right;
* };
*/
// 递归解法
struct TreeNode* searchBST(struct TreeNode* root, int val) {
if (root == NULL || root->val == val) {
return root;
}
return (val < root->val) ? searchBST(root->left, val) : searchBST(root->right, val);
}
// ========================================================================
// 迭代解法
struct TreeNode* searchBST(struct TreeNode* root, int val) {
// 定义当前遍历节点,从根节点开始
struct TreeNode *curr = root;
// 循环遍历,直到节点为空或找到目标
while (curr != NULL) {
if (val == curr->val) {
return curr;
}
curr = (val < curr->val) ? curr->left : curr->right;
}
return NULL;
}
- 复杂度分析
-
时间复杂度
- 递归与迭代一致:O(h),其中 h 为二叉搜索树的高度;
- 最好情况(树为完全平衡 BST):h=logn,时间复杂度 O(logn);
- 最坏情况(树退化成链表):h=n,时间复杂度 O(n)。
-
空间复杂度
- 递归解法:O(h),递归调用栈的深度等于树的高度;
- 迭代解法:O(1),仅使用常数个指针变量,无额外空间开销。
5.2 力扣 98. 验证二叉搜索树
- 题目描述
给你一个二叉树的根节点 root ,判断其是否是一个有效的二叉搜索树。
有效 二叉搜索树定义如下:
节点的左子树只包含 严格小于 当前节点的数。
节点的右子树只包含 严格大于 当前节点的数。
所有左子树和右子树自身必须也是二叉搜索树。
- 解题思路
- 解法 1:递归法(上下界约束)
核心思路:
给每个节点(除根节点)设置取值范围 [low, high]:
左孩子继承父节点的 low,上界更新为父节点值
右孩子继承父节点的 high,下界更新为父节点值
所有节点必须满足:low < val < high
- 解法 2:迭代法(中序遍历 + 栈)
核心思路:
二叉搜索树的中序遍历结果一定是严格递增序列,利用这个性质:
- 若 curr 用栈模拟中序遍历;
- 若 curr 记录前驱节点值;
- 若 curr 每次遍历当前节点,判断是否 > 前驱值;
- 若 curr 全程满足递增则为有效 BST。
- 代码实现
/**
* Definition for a binary tree node.
* struct TreeNode {
* int val;
* struct TreeNode *left;
* struct TreeNode *right;
* };
*/
// 递归解法
// 辅助函数:递归校验每个节点的取值范围 [low, high]
bool helper(struct TreeNode* root, long long low, long long high) {
if (!root) return true;
if (root->val <= low || root->val >= high) return false;
// 递归校验:
// 左子树:上界更新为当前节点值
// 右子树:下界更新为当前节点值
return helper(root->left, low, root->val) && helper(root->right, root->val, high);
}
// 力扣接口主函数:验证二叉搜索树
bool isValidBST(struct TreeNode* root) {
// 初始范围:long long 最小值 ~ 最大值,防止int边界值溢出
return helper(root, LLONG_MIN, LLONG_MAX);
}
// ========================================================================
// 迭代解法
bool isValidBST(struct TreeNode* root) {
struct TreeNode** stack = (struct TreeNode**)malloc(sizeof(struct TreeNode*) * 10000);
if (stack == NULL) return false;
int top = -1; // 栈顶指针,-1表示栈空
long long prev = LLONG_MIN; // 记录中序遍历的前驱节点值,long long防溢出
// 中序遍历循环:节点不为空 或 栈不为空
while (root != NULL || top != -1) {
// 第一步:一路向左,所有左节点入栈
while (root != NULL) {
stack[++top] = root;
root = root->left;
}
// 第二步:出栈,访问根节点
root = stack[top--];
// 核心判断:中序遍历必须严格递增,否则不是BST
if (root->val <= prev) {
free(stack);
return false;
}
// 更新前驱节点为当前节点
prev = root->val;
// 第三步:访问右子树
root = root->right;
}
free(stack);
return true;
}
- 复杂度分析
- 时间复杂度:O (n),需要访问所有节点
- 空间复杂度:O (n),最坏情况栈存储所有节点
5.3 力扣 450. 删除二叉搜索树中的节点
- 题目描述
给定一个二叉搜索树的根节点 root 和一个值 key,删除二叉搜索树中的 key 对应的节点,并保证二叉搜索树的性质不变。返回二叉搜索树(有可能被更新)的根节点的引用。
- 解题思路
找到目标节点后,分三种情况处理:
- 叶子节点(无左右孩子):直接删除,返回 NULL
- 若 curr 只有一个孩子节点:用唯一的孩子替代当前节点
- 若 curr 有左右两个孩子:
找右子树的最小值节点替换当前节点的值;
删除右子树中的最小值节点(转化为前两种简单场景);
选择右子树最小值:保证替换后依然满足 BST 性质(左子树所有节点<最小值< 右子树剩余节点)
- 代码实现
/**
* Definition for a binary tree node.
* struct TreeNode {
* int val;
* struct TreeNode *left;
* struct TreeNode *right;
* };
*/
// 辅助函数:查找最小值节点
struct TreeNode* findMin(struct TreeNode* root) {
if (root == NULL) return NULL;
while (root->left != NULL) {
root = root->left;
}
return root;
}
// 力扣接口函数
struct TreeNode* deleteNode(struct TreeNode* root, int key) {
if (root == NULL) return NULL;
// 目标值小于当前节点,递归删除左子树
if (key < root->val) root->left = deleteNode(root->left, key);
// 目标值大于当前节点,递归删除右子树
else if (key > root->val) root->right = deleteNode(root->right, key);
// 找到目标节点,执行删除
else {
// 场景1:叶子节点,直接释放
if (root->left == NULL && root->right == NULL) {
free(root);
return NULL;
}
// 场景2:只有一个孩子
else if (root->left == NULL) { // 只有右孩子
struct TreeNode *temp = root->right;
free(root);
return temp;
} else if (root->right == NULL) { // 只有左孩子
struct TreeNode *temp = root->left;
free(root);
return temp;
} else {
// 场景3:当前节点 有左右两个孩子(最复杂场景)
// 步骤1:找到右子树的最小节点
struct TreeNode* minNode = findMin(root->right);
// 步骤2:用最小节点的值 替换当前节点的值
root->val = minNode->val;
// 步骤3:递归删除右子树中的最小节点(转化为简单场景)
root->right = deleteNode(root->right, minNode->val);
}
}
return root;
}
- 复杂度分析
- 时间复杂度:平均 O (logn),最坏 O (n)
- 空间复杂度:平均 O (logn),最坏 O (n),递归栈开销
六、避坑指南📜
6.1 基础概念避坑
💥坑 1:混淆 “严格比较” 与 “非严格比较”
-
错误表现:认为 BST 允许左子树≤根≤右子树,直接插入重复值节点
-
致命危害:破坏 BST 的唯一性,导致查找、删除逻辑混乱(重复值无法准确定位),甚至出现死循环
-
正确做法:
- 标准 BST 采用严格小于 / 大于规则(左 < 根 < 右)
- 必须支持重复值时,在节点中增加
count计数字段,禁止创建重复节点 - 计数法删除时,先减计数,计数为 0 再真正释放节点
💥坑 2:只验证直接左右孩子,忽略子树全局约束
-
错误表现:验证 BST 时,仅判断
node->left->val < node->val && node->right->val > node->val -
致命危害:会将非法树误判为合法(如根 5→左 3→右 6,6>5 违反全局规则)
-
正确做法:
- 递归法:传递上下界参数,每个节点必须满足
low < val < high - 迭代法:中序遍历验证严格升序
- 递归法:传递上下界参数,每个节点必须满足
💥坑 3:中序后继 / 前驱只考虑有子树的情况
-
错误表现:认为中序后继一定是右子树最小值,中序前驱一定是左子树最大值
-
致命危害:当节点没有右 / 左子树时,无法找到正确的后继 / 前驱,导致删除、范围查询出错
-
正确做法:
-
中序后继完整逻辑:
- 有右子树 → 右子树最小值
- 无右子树 → 向上找第一个 “左孩子的父节点”
-
中序前驱完整逻辑:
- 有左子树 → 左子树最大值
- 无左子树 → 向上找第一个 “右孩子的父节点”
-
💥坑 4:混淆 “高度” 与 “深度” 的定义
-
错误表现:将高度定义为 “节点数”,深度定义为 “边数”,或反之
-
致命危害:平衡二叉树(AVL)的高度差计算错误,导致旋转逻辑完全失效
-
正确做法:
- 深度:从根节点到当前节点的边数(根节点深度为 0)
- 高度:从当前节点到最远叶子节点的边数(叶子节点高度为 0)
💥坑 5:认为空树不是合法 BST
- 错误表现:验证 BST 时,空树返回 false
- 致命危害:插入第一个节点失败,递归终止条件错误,导致栈溢出
- 正确做法:空树是合法的 BST,所有递归操作的终止条件都是
node == NULL
6.2 代码实现避坑
💥坑 6:内存安全三大致命错误
-
💥子坑 6.1:销毁树时只释放根节点
- 错误表现:
free(root); return; - 危害:子树节点全部泄漏,长期运行导致内存耗尽
- 正确做法:后序遍历递归销毁(先销毁左右子树,再销毁当前节点)
- 错误表现:
BST_Error BST_Destroy(BSTNode **root) {
if (root == NULL) {
printf("BST_Destroy: root pointer is NULL\n");
return BST_ERROR_NULL;
}
// 递归终止条件:当前节点为空,无需处理
if (*root == NULL) {
return BST_OK;
}
// 后序遍历:先销毁左子树,再销毁右子树
BST_Destroy(&(*root)->left);
BST_Destroy(&(*root)->right);
// 最后销毁当前节点
free(*root);
*root = NULL;
return BST_OK;
}
-
💥子坑 6.2:不检查 malloc 返回值
- 错误表现:
BSTNode* new_node = (BSTNode*)malloc(sizeof(BSTNode)); - 危害:内存不足时 malloc 返回 NULL,后续赋值操作导致段错误
- 正确做法:检查 malloc 返回值,失败时返回错误码
- 错误表现:
BSTNode* BST_CreateNode(int data) {
BSTNode *newNode = (BSTNode*)malloc(sizeof(BSTNode));
if (newNode == NULL) {
perror("BST_CreateNode: malloc failed");
return NULL;
}
newNode->data = data;
newNode->count = 1;
newNode->left = NULL;
newNode->right = NULL;
return newNode;
}
-
💥子坑 6.3:释放节点后不置空指针
- 错误表现:
free(node);不置空父节点对应指针 - 危害:产生野指针,后续访问导致未定义行为
- 正确做法:释放节点后,立即将父节点的左 / 右指针置空
- 错误表现:
💥坑 7:递归实现的栈溢出风险
-
错误表现:增删查、遍历等核心操作仅用递归实现,BST 退化成链表仍递归实现
-
危害:当 BST 退化成单链表时,递归深度为 n,超过系统栈大小导致程序崩溃
-
正确做法:
递归仅适用于小规模数据,添加递归深度校验,超限则终止递归或切换迭代
💥坑 8:插入 / 删除时根节点更新错误
-
错误表现:递归插入 / 删除时,传递节点指针而不是指针的指针,或不返回新的根节点
-
危害:根节点被修改时(如删除根节点),外部指针仍指向旧地址,导致树结构丢失
-
正确做法:
- 递归版本:返回新的根节点,外部接收返回值
- 迭代版本:传递根节点的指针的指针,或单独处理根节点情况
💥坑 9:删除有两个孩子的节点时直接拼接子树
- 错误表现:删除有两个孩子的节点时,将左子树挂到右子树的最小值节点上
- 危害:增加树的高度,导致性能下降,且逻辑复杂易出错
- 正确做法:用中序后继 / 前驱的值替换待删除节点,再删除后继 / 前驱节点(最多只有一个孩子)
6.3 算法刷题避坑
💥坑 10:验证 BST 时使用 INT_MIN/INT_MAX 作为初始上下界
-
错误表现:
bool isValidBST(TreeNode* root) { return helper(root, INT_MIN, INT_MAX); } -
危害:当节点值等于 INT_MIN 或 INT_MAX 时,会被误判为非法
-
正确做法:
- 使用
long long类型的上下界,初始值为LLONG_MIN和LLONG_MAX - 或使用迭代中序遍历法(无边界问题)
- 使用
💥坑 11:求第 k 小元素时不做边界检查
- 错误表现:直接中序遍历取第 k 个元素,不检查 k 是否合法,如输入 k = 0 / k = -5(负数 / 零,无意义)、
树只有 3 个节点,输入 k = 5(超出节点总数)
这些情况都会让遍历走到空节点,直接访问空指针导致程序崩溃。 - 危害:k > 节点数时,访问空指针导致段错误
- 正确做法:遍历过程中计数,计数达到 k 时返回;遍历结束未找到则返回
NULL
七、🏁结尾
以上就是本次内容的全部分享,感谢你在茫茫人海中点开这篇文章,如果觉得这篇博客对你有帮助,欢迎点赞👍→收藏🔖→关注👀,后续我会继续更新AVL树等数据结构的详细讲解……

附录:完整测试代码
完整可编译运行的 BST 测试代码
#include <stdio.h>
#include <stdlib.h>
#include <stdbool.h>
#include <limits.h>
typedef enum {
BST_OK = 0, // 操作成功
BST_ERROR_NULL = -1, // 空指针异常
BST_ERROR_MALLOC = -2, // 内存分配失败
BST_ERROR_NOT_EXIST = -3, // 节点不存在
BST_ERROR_DUPLICATE = -4 // 重复值(禁止重复模式下)
} BST_Error;
/**
* @brief 二叉搜索树节点结构定义
* @param data 节点存储的整型数据
* @param count 重复值计数,避免重复节点破坏BST性质
* @param left 左孩子节点指针
* @param right 右孩子节点指针
*/
typedef struct BSTNode {
int data;
int count;
struct BSTNode *left;
struct BSTNode *right;
} BSTNode;
// ==================== 函数实现 ====================
BSTNode* BST_CreateNode(int data) {
BSTNode *newNode = (BSTNode*)malloc(sizeof(BSTNode));
if (newNode == NULL) {
perror("BST_CreateNode: malloc failed");
return NULL;
}
newNode->data = data;
newNode->count = 1;
newNode->left = NULL;
newNode->right = NULL;
return newNode;
}
BST_Error BST_Destroy(BSTNode **root) {
if (root == NULL) {
printf("BST_Destroy: root pointer is NULL\n");
return BST_ERROR_NULL;
}
if (*root == NULL) {
return BST_OK;
}
BST_Destroy(&(*root)->left);
BST_Destroy(&(*root)->right);
free(*root);
*root = NULL;
return BST_OK;
}
BSTNode* BST_Search_Iterative(BSTNode *root, int target) {
if (root == NULL) {
return NULL;
}
BSTNode *curr = root;
while (curr != NULL) {
if (target == curr->data) {
return curr;
} else if (target < curr->data) {
curr = curr->left;
} else {
curr = curr->right;
}
}
return NULL;
}
BSTNode* BST_FindMin(BSTNode *root) {
if (root == NULL) {
return NULL;
}
while (root->left != NULL) {
root = root->left;
}
return root;
}
BSTNode* BST_FindMax(BSTNode *root) {
if (root == NULL) {
return NULL;
}
while (root->right != NULL) {
root = root->right;
}
return root;
}
BST_Error BST_Insert_Iterative(BSTNode **root, int data) {
if (root == NULL) {
return BST_ERROR_NULL;
}
if (*root == NULL) {
*root = BST_CreateNode(data);
return (*root == NULL) ? BST_ERROR_MALLOC : BST_OK;
}
BSTNode *curr = *root;
BSTNode *parent = NULL;
while (curr != NULL) {
parent = curr;
if (data == curr->data) {
curr->count++;
return BST_OK;
} else if (data < curr->data) {
curr = curr->left;
} else {
curr = curr->right;
}
}
BSTNode *newNode = BST_CreateNode(data);
if (newNode == NULL) {
return BST_ERROR_MALLOC;
}
if (data < parent->data) {
parent->left = newNode;
} else {
parent->right = newNode;
}
return BST_OK;
}
BST_Error BST_Delete_Iterative(BSTNode **root, int data) {
if (root == NULL || *root == NULL) {
return BST_ERROR_NULL;
}
BSTNode *curr = *root;
BSTNode *parent = NULL;
while (curr != NULL && curr->data != data) {
parent = curr;
if (data < curr->data) {
curr = curr->left;
} else {
curr = curr->right;
}
}
if (curr == NULL) {
return BST_ERROR_NOT_EXIST;
}
if (curr->count > 1) {
curr->count--;
return BST_OK;
}
if (curr->left == NULL || curr->right == NULL) {
BSTNode *child = (curr->left != NULL) ? curr->left : curr->right;
if (parent == NULL) {
*root = child;
} else if (parent->left == curr) {
parent->left = child;
} else {
parent->right = child;
}
free(curr);
} else {
BSTNode *succParent = curr;
BSTNode *successor = curr->right;
while (successor->left != NULL) {
succParent = successor;
successor = successor->left;
}
curr->data = successor->data;
curr->count = successor->count;
if (succParent == curr) {
succParent->right = successor->right;
} else {
succParent->left = successor->right;
}
free(successor);
}
return BST_OK;
}
BST_Error BST_InOrder_Print_Recursive(BSTNode *root) {
if (root == NULL) {
return BST_OK;
}
BST_InOrder_Print_Recursive(root->left);
printf("%d(count:%d) ", root->data, root->count);
BST_InOrder_Print_Recursive(root->right);
return BST_OK;
}
// ==================== 测试主函数 ====================
int main() {
BSTNode *root = NULL;
printf("===== 二叉搜索树测试 =====\n");
// 1. 批量插入节点
int insertData[] = {8, 3, 10, 1, 6, 14, 3};
int len = sizeof(insertData) / sizeof(int);
printf("\n1. 插入数据: ");
for (int i = 0; i < len; i++) {
printf("%d ", insertData[i]);
BST_Insert_Iterative(&root, insertData[i]);
}
// 2. 中序遍历(验证有序性)
printf("\n2. 中序遍历: ");
BST_InOrder_Print_Recursive(root);
// 3. 查找节点
printf("\n\n3. 查找节点 6:");
BSTNode *found = BST_Search_Iterative(root, 6);
found ? printf("存在,count=%d", found->count) : printf("不存在");
// 4. 查询最值
printf("\n4. 最小值:%d,最大值:%d", BST_FindMin(root)->data, BST_FindMax(root)->data);
// 5. 删除节点并验证
printf("\n\n5. 删除节点 3:");
BST_Delete_Iterative(&root, 3);
printf("成功\n 删除后遍历: ");
BST_InOrder_Print_Recursive(root);
// 6. 释放内存
printf("\n\n6. 销毁树:");
BST_Destroy(&root);
printf("成功\n");
printf("\n===== 测试结束 =====\n");
return 0;
}

1003

被折叠的 条评论
为什么被折叠?



