061二叉搜索树

二叉搜索树 - 动态有序集合的经典实现

061二叉搜索树:动态有序的基石

📰 5W1H 发明者故事

Who(何人)- 发明者是谁?

发明者(插入/查找) :二叉搜索树作为概念在1950年代由多人独立发现,通常归功于P. F. Windley(1960)和Andrew Donald Booth(1960年代初)

删除算法发明者 :托马斯·H·希巴德(Thomas H. Hibbard),1962年发表"Some Combinatorial Properties of Certain Trees",提出用中序后继替换被删节点的删除算法

背景:

  • Hibbard在Bell实验室工作,研究符号计算和表处理的效率问题
  • 高德纳(Knuth)在TAOCP第三卷6.2.2节对BST及Hibbard删除算法进行了严格分析
  • Knuth指出Hibbard删除算法的一个微妙问题:重复删除+插入会使树逐渐向左偏斜

When(何时)- 什么时候发明的?

时间 :插入/查找概念约1955-1960年;Hibbard删除算法1962年

时代背景:

  • 链表(1956)刚发明,树作为链表的层次推广自然出现
  • 符号处理语言(LISP, IPL)开始广泛使用树状结构
  • 排序和查找是1960年代计算机科学的核心问题,高效的动态表迫切需要

Where(何地)- 在哪里发明的?

地点 :Hibbard在贝尔实验室(Bell Labs),纽泽西州默里山

环境:

  • 贝尔实验室是1960年代最顶级的计算机科学研究中心之一
  • C语言、Unix、晶体管均在此诞生,BST删除算法的研究环境得天独厚

What(何事)- 发明了什么?

数据结构 :二叉搜索树(Binary Search Tree, BST)

BST性质 :对每个节点N,N左子树所有值 < N的值 < N右子树所有值

Hibbard删除算法的三种情况:

  1. 删除叶节点:直接移除
  2. 删除有一个子节点的节点:用子节点替代
  3. 删除有两个子节点的节点:用中序后继(右子树最小值)替代被删节点的值,然后删除中序后继

Why(何因)- 为什么发明?

要解决的问题:

  1. 数组实现的有序表:查找O(log n)但插入/删除O(n)(需要移动元素)
  2. 链表:插入/删除O(1)但查找O(n)(无法二分)
  3. BST结合两者优点:平均查找O(log n),插入/删除O(log n)

Hibbard删除的挑战:

  • 删除有两个子节点的节点不能简单移除,必须维护BST性质
  • 用中序前驱或中序后继替代是保持BST性质的最优选择
  • 理论上,长期的随机插入+删除使BST期望高度为O(√n)而非O(log n)(Knuth指出此问题,后由Jonassen & Knuth 1978年证明)

How(何果)- 如何实现?有什么影响?

中序后继查找:

复制代码
右子树的最小值 = 一直向左走直到没有左孩子

历史影响:

  • BST是AVL树、红黑树、B树等所有平衡树的基础
  • 数据库索引(MySQL InnoDB的B+树)是BST的多路推广
  • C++ STL的std::set/std::map基于红黑树(BST的平衡变体)
  • 函数式语言中的不可变Map(如Haskell的Data.Map)基于平衡BST

今天的使用:

  • 编译器的符号表(作用域嵌套 + 有序遍历)
  • 操作系统的虚拟内存区域管理(Linux内核用红黑树管理VMA)
  • 数据库的内存索引(如Redis的ZSet用跳表+哈希,某些实现用BST)

名言:Knuth在TAOCP中指出:"二叉搜索树是最重要的数据结构之一。它的简单性是其最大优点,而其不平衡性是其最大缺点------这两点共同激励了AVL树、红黑树等平衡结构的发明。"


📝 自然语言需求定义

需求名称:实现完整的二叉搜索树,包含Hibbard删除算法

功能需求(用精确的中文描述)

  1. 插入:向BST中插入一个值,保持BST性质

    • 输入:根节点指针的指针、整数值
    • 操作:从根出发,小则向左、大则向右,到达NULL处插入新节点;忽略重复值
    • 输出:无(就地修改)
  2. 查找:在BST中查找一个值

    • 输入:根节点指针、目标值
    • 操作:从根出发,相等返回,小向左,大向右
    • 输出:找到返回节点指针,未找到返回NULL
  3. 删除(Hibbard算法):删除指定值的节点,保持BST性质

    • 输入:根节点指针的指针、目标值
    • 情况1(叶节点):直接释放,父指针置NULL
    • 情况2(一个子节点):用子节点替代
    • 情况3(两个子节点):用中序后继的值替代,再删除中序后继
    • 输出:无(就地修改)
  4. 中序遍历:按升序遍历所有节点

    • 输入:根节点指针、输出数组指针、计数器指针
    • 操作:左-根-右递归遍历,依次填入数组
    • 输出:有序数组和节点数
  5. 最小值/最大值:返回BST中最小/最大节点

    • 最小:一直向左走
    • 最大:一直向右走
  6. 树高度:计算BST的高度(空树为-1,单节点为0)

    • 输入:根节点指针
    • 操作:递归计算左右子树高度,取最大值+1
    • 输出:整数高度
  7. 释放整棵树:释放所有节点内存

    • 操作:后序遍历,先删子节点再删根

约束条件

  • 重复插入同一值应被忽略(不改变树)
  • 删除不存在的值应安全返回(不崩溃)
  • 所有malloc必须有对应的free
  • 中序遍历结果必须严格有序

验收标准(必须可验证)

编号 测试场景(自然语言描述) 预期结果 验证方式
1 插入5,3,7,1,4,6,8后中序遍历 1,3,4,5,6,7,8 断言数组各元素
2 查找存在的值4 返回非NULL节点 断言指针非NULL且data==4
3 查找不存在的值9 返回NULL 断言指针为NULL
4 删除叶节点1后中序遍历 3,4,5,6,7,8 断言数组
5 删除有一个子节点的节点3(只有右孩子4)后中序遍历 4,5,6,7,8 断言数组
6 删除有两个子节点的根节点5后中序遍历 4,6,7,8(中序后继6替代5) 断言数组
7 最小值和最大值 min=4,max=8 断言节点data
8 删除不存在的值99 树不变,不崩溃 断言中序遍历不变
9 空树查找任意值 返回NULL,不崩溃 断言

AI 生成提示

复制代码
基于以上需求和验收标准,用标准C语言实现完整的二叉搜索树(含Hibbard删除)。

要求:
1. 使用标准C99,gcc -Wall无警告
2. 节点结构体:int data, Node* left, Node* right
3. 完整实现insert/search/delete/min/max/height/inorder/free_tree
4. delete实现Hibbard三种情况(叶/一子/二子)
5. 完整测试框架:tests_passed/tests_failed计数
6. main最后返回 tests_failed > 0 ? 1 : 0

核心函数:
- bst_insert(&root, value) - 插入
- bst_search(root, value) - 查找,返回节点指针
- bst_delete(&root, value) - Hibbard删除
- bst_min(root) / bst_max(root) - 最小/最大值节点
- bst_height(root) - 树高度
- bst_inorder(root, arr, &cnt) - 中序遍历填数组
- bst_free(root) - 释放所有内存

💻 C语言实现文件

对应文件 : binary_search_tree.c

编译运行:

bash 复制代码
gcc -std=c99 -Wall -o binary_search_tree_test binary_search_tree.c
./binary_search_tree_test

# 内存泄漏检测
valgrind --leak-check=full ./binary_search_tree_test

核心函数:

  • bst_insert(&root, value) - 迭代插入,保持BST性质
  • bst_search(root, value) - 迭代查找,返回节点指针
  • bst_delete(&root, value) - Hibbard删除(叶/一子/二子三种情况)
  • bst_min(root) / bst_max(root) - 最小/最大值节点
  • bst_height(root) - 递归计算树高度
  • bst_inorder(root, arr, &cnt) - 中序遍历,输出有序数组
  • bst_free(root) - 后序遍历释放所有节点