数据结构----二叉排序树(ai修改版)


说明:

  • ✏️ 原文 ------ 你草稿中的内容,保留原意,仅做了语句通顺上的微调
  • 📝 补充/修正 ------ 我添加的内容或指出的错误

所有代码均保持你原本的实现不变,错误处会单独标注并给出修正。


一、定义和性质

✏️ 原文:

二叉搜索树是在二叉树的基础上,增加了几个规则约束:

  1. 如果它的左子树不空,则左子树上所有结点的值均小于它的根结点的值。
  2. 如果它的右子树不空,则右子树上所有结点的值均大于它的根结点的值。
  3. 它的左、右子树也分别为二叉搜索树。

根据这个性质,我们可以用递归来对树进行各种操作。

📝 补充:

中序遍历有序性: 对二叉搜索树进行中序遍历(左 → 根 → 右),得到的结果是一个递增序列。这既是 BST 最重要的性质,也是验证一棵树是否为 BST 的常用手段。

为什么需要 BST?

数据结构 查找 插入 删除
顺序表 O(log n) 二分 O(n) O(n)
链表 O(n) O(1) O(1)
BST O(log n) 平均 O(log n) 平均 O(log n) 平均

BST 在查找和插入/删除之间取得了平衡,是动态数据结构的经典方案。


二、数据结构定义

✏️ 原文:

c 复制代码
typedef struct BSTNode {
    int data;
    struct BST* l;
    struct BST* r;
}BSTNode,*BSTree;

📝 ⚠️ 错误修正:

上面的定义中,struct BST* lstruct BST* r 指向的类型 BST 并不存在 ,结构体标签是 BSTNode。应该写成:

c 复制代码
typedef struct BSTNode {
    int data;
    struct BSTNode* l;
    struct BSTNode* r;
} BSTNode, *BSTree;

或者用更简洁的前向声明写法:

c 复制代码
typedef struct BSTNode {
    int data;
    struct BSTNode *l, *r;
} BSTNode, *BSTree;

三、创建新节点

✏️ 原文:

c 复制代码
BSTNode* CreateNode(int x) {
    BSTNode* s = (BSTNode*)malloc(sizeof(BSTNode));
    if (s == NULL) {
        printf("内存申请失败\n");
        return s;
    }
    s->data = x;
    s->l = s->r = NULL;
    return s;
}

📝 补充:

注意 malloc 可能返回 NULL,代码中做了防御性检查,这是一个好习惯。


四、查找操作

1. 递归实现 ------ Find1

✏️ 原文:

确定递归出口 root == NULL,如果遍历完整棵树都没有找到目标节点,则返回 NULL

如果根节点的数据等于目标数据,直接返回 root

  • 如果 x < root->data,说明目标数据在左子树中,递归进入左子树查找。
  • 如果 x > root->data,说明目标数据在右子树中,递归进入右子树查找。
c 复制代码
BSTNode* Find1(BSTree root, int x) {
    if (root == NULL) {
        return NULL;
    }
    if (x == root->data) {
        return root;
    }
    else if (x < root->data) {
        return Find1(root->l, x);
    }
    else {
        return Find1(root->r, x);
    }
}

2. 非递归实现 ------ Find2

✏️ 原文:

用指针 p 指向 root,通过 while 循环和 BST 特性遍历整棵树找到目标节点。在循环条件中先判断 p 是否为 NULL,再判断 p->datax 是否相等,防止访问空指针。如果找到则跳出循环返回 p,如果没找到最终 p == NULL,返回 NULL

c 复制代码
BSTNode* Find2(BSTree root, int x) {
    BSTNode* p = root;
    while (p != NULL && p->data != x) {
        if (x > p->data) {
            p = p->r;
        }
        else {
            p = p->l;
        }
    }
    return p;
}

📝 补充:递归 vs 非递归对比

维度 递归 (Find1) 非递归 (Find2)
代码简洁性 代码少,逻辑清晰 略长
性能 有函数调用开销 纯循环,略快
栈溢出风险 树高较大时有风险

时间复杂度:平均 O(log n),最坏 O(n)(退化成链表时)。


五、插入操作

1. 递归实现 ------ Insert1

✏️ 原文:

首先确定递归的出口:当根节点为空时找到了空位置,创建节点 s 并返回 s

当根节点不为空时,判断根节点数据和插入数据 x 的大小:

  • 如果 x < root->data,说明 x 应该插入在 root 的左子树中,递归调用 Insert1 本身。因为插入操作会改变子树的结构,所以将返回值赋给 root->l,从而把新创建的节点接入树中。
  • 如果 x > root->data,说明 x 应该插入在右子树中。

最后返回根节点 root

c 复制代码
BSTree Insert1(BSTree root, int x) {
    if (root == NULL) {
        BSTNode* s = CreateNode(x);
        return s;
    }
    if (x < root->data) {
        root->l = Insert1(root->l, x);
    }
    else {
        root->r = Insert1(root->r, x);
    }
    return root;
}

📝 补充:

递归版最巧妙的地方在于 root->l = Insert1(root->l, x) ------ 通过返回值将新节点(或修改后的子树)接回原树,不需要显式记录父节点。

2. 非递归实现 ------ Insert2

✏️ 原文:

如果这是一棵空树,则创建根节点并返回。

p 指针遍历树,pre 指针记录 p 的父节点。通过 while 循环找到插入位置:每次先更新 pre = p,然后根据 BST 特性决定往左还是往右。跳出循环后创建新节点 s,根据 xpre->data 的大小关系接入左或右。

最后返回根节点 root

c 复制代码
BSTree Insert2(BSTree root, int x) {
    if (root == NULL) {
        BSTNode* s = CreateNode(x);
        return s;
    }
    BSTNode* p = root;
    BSTNode* pre = NULL;
    while (p != NULL) {
        pre = p;
        if (x < p->data) {
            p = p->l;
        }
        else {
            p = p->r;
        }
    }
    BSTNode* s = CreateNode(x);
    if (x > pre->data) {
        pre->r = s;
    }
    else {
        pre->l = s;
    }
    return root;
}

📝 补充:关于重复值

上述插入代码中,当 x == root->data 时走 else 分支(插入右子树)。这意味着重复元素会被插入到右子树。这是一种常见做法,但在实际应用中通常不允许重复,或者需要额外处理(如计数或禁止插入)。


六、删除操作(重点 + 难点)

📝 补充:删除操作的核心思想

删除一个节点,根据它的(孩子数量)分为三种情况:

情况 处理方式
度 0(叶子节点) 直接删除,父节点对应指针置空
度 1(只有一个孩子) 让孩子顶替它的位置
度 2(有两个孩子) 找前驱(左子树最右节点)或后继(右子树最左节点)替换值,转化为删除度 0 或 1 的节点

为什么要对度 2 做转化?因为直接删除度 2 的节点会"留下两个空洞",无法简单处理。而用一个值来替换后,我们只需要去删除那个被拿走值的节点,它必然在树的最底层(度 0 或 1),就容易处理了。

1. 非递归实现 ------ Delete1

✏️ 原文:

先判断空树。

用指针 p 指向 rootpre 置空,类似于 Find 函数找到要删除的节点。跳出循环后判断 p 是否为空,即判断目标节点是否存在。

判断 p 的度:

  • 如果度为 2,找到中序遍历中 p前驱 (左子树的最右节点),把前驱的数据复制到 p,问题转化为删除前驱原节点(度必为 0 或 1)。
  • 此时被删除节点的度变为 0 或 1,判断其左孩子是否为空:左孩子不为空则度为 1,用 ch 指向左孩子;左孩子为空则度为 0,ch 指向右孩子(为空,统一处理)。
  • 如果 pre != NULL 说明 p 不是根节点,根据 ppre 的左/右孩子来决定 pre 的哪个指针指向 ch
  • 如果 pre == NULL 说明 p 是根节点,直接让 ch 成为新根。
  • 最后 free(p) 并置空。
c 复制代码
BSTree Delete1(BSTree root, int x) {
    if (root == NULL) {
        printf("空树\n");
        return root;
    }
    BSTNode* p = root;
    BSTNode* pre = NULL;
    BSTNode* ch = NULL;
    while (p != NULL && p->data != x) {
        pre = p;
        if (x < p->data) {
            p = p->l;
        }
        else {
            p = p->r;
        }
    }
    if (p == NULL) {
        printf("p不存在\n");
        return root;
    }
    if (p->l != NULL && p->r != NULL) {
        BSTNode* t = p;
        BSTNode* tf = NULL;
        while (t->r != NULL) {
            tf = t;
            t = t->r;
        }
        p->data = t->data;
        p = t;
        pre = tf;
    }
    if (p->l != NULL) {
        ch = p->l;
    }
    else {
        ch = p->r;
    }
    if (pre != NULL) {
        if (pre->l == p) {
            pre->l = ch;
        }
        else {
            pre->r = ch;
        }
    }
    else {
        root = ch;
    }
    free(p);
    p = NULL;
    return root;
}

📝 ⚠️ 错误修正(重要):

代码第 28 行 BSTNode* t = p;错误的 ,应该改为 BSTNode* t = p->l;

原因分析:

p 的度为 2 时,我们需要找的是 p 的前驱------即左子树中的最右节点 。所以 t 应该从 p->l(左孩子)开始,然后一路向右。

BSTNode* t = p; 是从 p 本身开始向右走,会进入右子树找到右子树的最大值(最右节点)。用这个值替换 p 后,会出现右子树中仍有比新根小的节点的情况,破坏 BST 性质。

举例说明:

markdown 复制代码
      8          删除 8(度 2)
    /   \
   3     10
  / \      \
 1   6      14
    / \    /
   4   7  13

错误写法:找到右子树最大值 14 → 8 变成 14 → 14 被删除 → 13 成为 10 的右孩子

markdown 复制代码
      14(原来是8)
    /    \
   3      10
  / \       \
 1   6       13   ← 13 < 14,违反 BST!
    / \
   4   7

正确做法:找到左子树前驱 7 → 8 变成 7 → 删除原节点 7

markdown 复制代码
       7
    /     \
   3      10
  / \       \
 1   6       14
    /       /
   4       13

修正后的代码片段:

c 复制代码
if (p->l != NULL && p->r != NULL) {
    // 找前驱:左子树的最右节点(最大值)
    BSTNode* t = p->l;   // ✏️ 从 p->l 开始,不是 p
    BSTNode* tf = NULL;
    while (t->r != NULL) {
        tf = t;
        t = t->r;
    }
    p->data = t->data;    // 前驱数据复制过来
    p = t;                // 转为删除前驱节点
    pre = tf;
}

2. 递归实现 ------ Delete2

✏️ 原文:

递归出口和之前相同。

  • 如果 x < root->data,在左子树中递归删除。
  • 如果 x > root->data,在右子树中递归删除。
  • 如果 x == root->dataroot 就是我们要删除的节点:
    • 如果度为 2,这次我们找中序遍历的后继 (右子树的最左节点),用后继数据替换 root 的数据,然后去 root 的右子树中删除后继节点。
    • 如果度为 1 或 0,判断 root 有没有左孩子,有则用左孩子代替自己,否则用右孩子代替。
c 复制代码
BSTree Delete2(BSTree root, int x) {
    if (root == NULL) {
        printf("空树\n");
        return root;
    }
    if (x < root->data) {
        root->l = Delete2(root->l, x);
    }
    else if (x > root->data) {
        root->r = Delete2(root->r, x);
    }
    else {
        if (root->l != NULL && root->r != NULL) {
            BSTNode* t = root->r;
            while (t->l != NULL) {
                t = t->l;
            }
            root->data = t->data;
            root->r = Delete2(root->r, t->data);
        }
        else {
            BSTNode* p = root;
            if (root->l != NULL) root = root->l;
            else root = root->r;
            free(p);
            p = NULL;
        }
    }
    return root;
}

📝 补充:

Delete2 的实现是正确的。这里找的是后继(右子树的最左节点)。由于不涉及显式的父节点维护,递归版代码比非递归版简洁许多。

递归版的核心技巧root->l = Delete2(root->l, x) 通过返回值将修改后的子树接回来,和插入操作的递归版如出一辙。


七、中序遍历

✏️ 原文:

c 复制代码
void InOrder(BSTree root) {
    if (root == NULL) return;
    InOrder(root->l);
    printf("%d ", root->data);
    InOrder(root->r);
}

📝 补充:

对 BST 做中序遍历,输出的是升序序列。可以通过这个性质来验证树是否正确。


八、完整测试

✏️ 原文:

c 复制代码
int main() {
    BSTree root = NULL;
    int n, x;
    scanf("%d", &n);
    for (int i = 1; i <= n; i++) {
        scanf("%d", &x);
        root = Insert2(root, x);
    }
    InOrder(root);
    printf("\n");
    scanf("%d", &x);
    root = Delete2(root, x);
    InOrder(root);
    printf("\n");
    return 0;
}
/*
输入:
9
8 3 10 1 6 14 4 7 13
输出:
1 3 4 6 7 8 10 13 14
*/

📝 补充:

建树后的中序遍历结果应该为升序序列 1 3 4 6 7 8 10 13 14。 删除节点后再次中序遍历,结果仍然保持升序。


九、总结与延伸

📝 补充:

递归 vs 非递归

操作 递归 非递归
查找 代码简洁,理解直观 无函数调用开销
插入 通过返回值接回树,最优雅 需要手动维护父节点
删除 同样通过返回值,逻辑清晰 父节点 + 前驱/后继指针维护复杂

递归版的代码更简洁、更接近数学定义,但树高较大时有栈溢出风险。非递归版性能略优,但代码维护难度更高。

BST 的局限性

BST 在有序插入 时(如 1 2 3 4 5)会退化成一条链表,查找复杂度退化为 O(n)。

解决方案:平衡二叉树

类型 特点
AVL 树 严格平衡,左右子树高度差 ≤ 1
红黑树 近似平衡,插入/删除效率更高

这两种树在 BST 的基础上增加了自平衡机制,是工业界的标准方案(如 C++ STL 的 std::map 就是红黑树)。


全文完

相关推荐
iiiiyu2 小时前
集合进阶(Map集合)
java·大数据·开发语言·数据结构·编程语言
小江的记录本2 小时前
【Java基础】核心关键字:final、static、volatile、synchronized、transient(附《思维导图》+《面试高频考点清单》)
java·前端·数据结构·后端·ai·面试·ai编程
go不是csgo3 小时前
两个Redis数据结构搞定签到和UV统计:Bitmap与HyperLogLog实战
数据结构·redis·uv
悠仁さん3 小时前
数据结构 栈与队
数据结构
Plan-C-4 小时前
二叉树的遍历
java·数据结构·算法
历程里程碑4 小时前
54 深入解析poll多路复用技术
java·linux·服务器·开发语言·前端·数据结构·c++
一切皆是因缘际会5 小时前
AI Agent落地困局与突破:从技术架构到企业解析
数据结构·人工智能·算法·架构
qeen875 小时前
【算法笔记】各种常见排序算法详细解析(下)
c语言·数据结构·c++·笔记·学习·算法·排序算法
欢璃5 小时前
笔试强训练习
java·开发语言·jvm·数据结构·算法·贪心算法·动态规划