二叉搜索树详解



🚩前言:为什么要学二叉搜索树?

我们先看一个核心痛点:日常开发中,我们需要一个既能快速查找,又能高效动态插入 / 删除的数据结构。对比常用结构:

数据结构查找效率插入效率删除效率核心痛点
无序数组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 条核心性质:

  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 的核心特性

  1. 有序性:左子树所有节点值 < 根节点值 < 右子树所有节点值。
  2. 中序遍历特性:对 BST 做中序遍历,结果一定是严格递增序列
  3. 二分查找特性:所有操作都基于二分思想,平均时间复杂度 O (logn)
  4. 递归结构:任意一颗子树本身也满足BST规则,天然适合递归实现。
  5. 前驱后继天然存在:任意节点都能快速找到中序前驱(前一个更小值)和中序后继(后一个更大值)。
  6. 动态扩展性:插入删除无需移动大量元素,仅需修改指针指向
  7. 范围查询友好:天然支持查找 [a,b] 区间内的所有元素,这是哈希表无法实现的核心优势

二、核心原理篇:BST 的核心操作算法拆解🎯

2.1 查找操作🔎

  • 核心原理
    从根节点出发:

    1. 目标值 == 当前节点值:找到目标,返回节点
    2. 目标值 < 当前节点值:目标一定在左子树,去左子树查找
    3. 目标值 > 当前节点值:目标一定在右子树,去右子树查找
    4. 遍历到空节点:树中无目标值,查找失败
  • 扩展查找:最值、前驱、后继

    1. 查找最小值:一路向左,直到左孩子为空的节点,就是 BST 的最小值
    2. 查找最大值:一路向右,直到右孩子为空的节点,就是 BST 的最大值
    3. 中序后继:比当前节点大的最小节点(右子树的最小值)
    4. 中序前驱:比当前节点小的最大节点(左子树的最大值)
  • 查找步骤可视化

以上文合法 BST 为例,查找值为 6 的节点:

步骤1:根节点8,6<8 → 进入左子树
步骤2:节点3,6>3 → 进入右子树
步骤3:节点6,6==6 → 查找成功,返回节点

2.2 插入操作

  • 核心原理
    插入的核心是找到符合 BST 规则的叶子节点空位,插入的新节点一定是叶子节点,不会修改原有树的结构(仅修改父节点的指针),步骤:

    1. 从根节点出发,按照查找规则,找到合适的父节点
    2. 若目标值已存在:按需求处理(禁止重复 / 计数累加)
    3. 若目标值小于父节点值:挂载到父节点的左孩子
    4. 若目标值大于父节点值:挂载到父节点的右孩子
  • 插入步骤可视化

以上文合法 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. 替身的特性:中序后继是右子树的最小值,一定没有左孩子(否则就不是最小值),所以删除替身最多只会触发场景1或2,不会循环嵌套

    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 工程性能优化

  1. 重复值计数优化:用 count 字段处理重复值,避免创建重复节点,减少树的高度
  2. 节点复用:删除的节点不直接释放,放入空闲链表,插入时优先从空闲链表获取,减少内存分配开销
  3. 迭代优先原则:所有核心操作优先使用迭代实现,避免递归栈溢出,同时减少函数调用开销
  4. 内存池优化:避免频繁 malloc/free 节点,预分配一块连续的内存池,节点从内存池分配,减少内存碎片和系统调用开销

五、实战刷题篇:力扣经典 BST 题目详解⚔️

5.1 力扣 700. 二叉搜索树中的搜索

  1. 题目描述

给定二叉搜索树(BST)的根节点 root 和一个整数值 val
你需要在 BST 中找到节点值等于 val 的节点。 返回以该节点为根的子树。 如果节点不存在,则返回 null
示例 :

  1. 解题思路
  • 解法1:递归法

终止条件:若当前节点为空(未找到)或当前节点值等于 val(找到),直接返回当前节点;
递归逻辑

  • 若 val < 当前节点值:目标在左子树,递归搜索左子树;
  • 若 val > 当前节点值:目标在右子树,递归搜索右子树。
  • 解法2:迭代法

循环 + 指针代替递归栈,避免递归的栈空间开销:初始化指针 curr 指向根节点;
循环遍历

  • 若 curr 为空,退出循环(未找到);
  • 若 curr 若 val == curr->val,找到目标,返回 curr;
  • 若 curr 若 val < curr->val,curr 指向左子树;
  • 若 curr 若 val > curr->val,curr 指向右子树;
  • 若 curr 循环结束未找到,返回 NULL。
  1. 代码实现
/**
 * 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;
}
  1. 复杂度分析
  • 时间复杂度

    • 递归与迭代一致:O(h),其中 h 为二叉搜索树的高度;
    • 最好情况(树为完全平衡 BST):h=logn,时间复杂度 O(logn);
    • 最坏情况(树退化成链表):h=n,时间复杂度 O(n)。
  • 空间复杂度

    • 递归解法:O(h),递归调用栈的深度等于树的高度;
    • 迭代解法:O(1),仅使用常数个指针变量,无额外空间开销。

5.2 力扣 98. 验证二叉搜索树

  1. 题目描述

给你一个二叉树的根节点 root ,判断其是否是一个有效的二叉搜索树。
有效 二叉搜索树定义如下:
节点的左子树只包含 严格小于 当前节点的数。
节点的右子树只包含 严格大于 当前节点的数。
所有左子树和右子树自身必须也是二叉搜索树。

  1. 解题思路
  • 解法 1:递归法(上下界约束)

核心思路:
给每个节点(除根节点)设置取值范围 [low, high]:
左孩子继承父节点的 low,上界更新为父节点值
右孩子继承父节点的 high,下界更新为父节点值
所有节点必须满足:low < val < high

  • 解法 2:迭代法(中序遍历 + 栈)

核心思路:
二叉搜索树的中序遍历结果一定是严格递增序列,利用这个性质:

  • 若 curr 用栈模拟中序遍历;
  • 若 curr 记录前驱节点值;
  • 若 curr 每次遍历当前节点,判断是否 > 前驱值;
  • 若 curr 全程满足递增则为有效 BST。
  1. 代码实现
/**
 * 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;
}
  1. 复杂度分析
  • 时间复杂度:O (n),需要访问所有节点
  • 空间复杂度:O (n),最坏情况栈存储所有节点

5.3 力扣 450. 删除二叉搜索树中的节点

  1. 题目描述

给定一个二叉搜索树的根节点 root 和一个值 key,删除二叉搜索树中的 key 对应的节点,并保证二叉搜索树的性质不变。返回二叉搜索树(有可能被更新)的根节点的引用。

  1. 解题思路

找到目标节点后,分三种情况处理:

  1. 叶子节点(无左右孩子):直接删除,返回 NULL
  2. 若 curr 只有一个孩子节点:用唯一的孩子替代当前节点
  3. 若 curr 有左右两个孩子:
    找右子树的最小值节点替换当前节点的值;
    删除右子树中的最小值节点(转化为前两种简单场景);
    选择右子树最小值:保证替换后依然满足 BST 性质(左子树所有节点<最小值< 右子树剩余节点)
  1. 代码实现
/**
 * 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;
}
  1. 复杂度分析
  • 时间复杂度:平均 O (logn),最坏 O (n)
  • 空间复杂度:平均 O (logn),最坏 O (n),递归栈开销

六、避坑指南📜

6.1 基础概念避坑

💥坑 1:混淆 “严格比较” 与 “非严格比较”

  • 错误表现:认为 BST 允许左子树≤根≤右子树,直接插入重复值节点

  • 致命危害:破坏 BST 的唯一性,导致查找、删除逻辑混乱(重复值无法准确定位),甚至出现死循环

  • 正确做法

    1. 标准 BST 采用严格小于 / 大于规则(左 < 根 < 右)
    2. 必须支持重复值时,在节点中增加count计数字段,禁止创建重复节点
    3. 计数法删除时,先减计数,计数为 0 再真正释放节点

💥坑 2:只验证直接左右孩子,忽略子树全局约束

  • 错误表现:验证 BST 时,仅判断node->left->val < node->val && node->right->val > node->val

  • 致命危害:会将非法树误判为合法(如根 5→左 3→右 6,6>5 违反全局规则)

  • 正确做法

    1. 递归法:传递上下界参数,每个节点必须满足low < val < high
    2. 迭代法:中序遍历验证严格升序

💥坑 3:中序后继 / 前驱只考虑有子树的情况

  • 错误表现:认为中序后继一定是右子树最小值,中序前驱一定是左子树最大值

  • 致命危害:当节点没有右 / 左子树时,无法找到正确的后继 / 前驱,导致删除、范围查询出错

  • 正确做法

    • 中序后继完整逻辑:

      1. 有右子树 → 右子树最小值
      2. 无右子树 → 向上找第一个 “左孩子的父节点”
    • 中序前驱完整逻辑:

      1. 有左子树 → 左子树最大值
      2. 无左子树 → 向上找第一个 “右孩子的父节点”

💥坑 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:插入 / 删除时根节点更新错误

  • 错误表现:递归插入 / 删除时,传递节点指针而不是指针的指针,或不返回新的根节点

  • 危害:根节点被修改时(如删除根节点),外部指针仍指向旧地址,导致树结构丢失

  • 正确做法

    1. 递归版本:返回新的根节点,外部接收返回值
    2. 迭代版本:传递根节点的指针的指针,或单独处理根节点情况

💥坑 9:删除有两个孩子的节点时直接拼接子树

  • 错误表现:删除有两个孩子的节点时,将左子树挂到右子树的最小值节点上
  • 危害:增加树的高度,导致性能下降,且逻辑复杂易出错
  • 正确做法:用中序后继 / 前驱的值替换待删除节点,再删除后继 / 前驱节点(最多只有一个孩子)

6.3 算法刷题避坑

💥坑 10:验证 BST 时使用 INT_MIN/INT_MAX 作为初始上下界

  • 错误表现

    bool isValidBST(TreeNode* root) {
        return helper(root, INT_MIN, INT_MAX);
    }
    
  • 危害:当节点值等于 INT_MIN 或 INT_MAX 时,会被误判为非法

  • 正确做法

    1. 使用long long类型的上下界,初始值为LLONG_MINLLONG_MAX
    2. 或使用迭代中序遍历法(无边界问题)

💥坑 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;
}

在这里插入图片描述

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值