二叉搜索树 - 动态有序集合的经典实现
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删除算法的三种情况:
- 删除叶节点:直接移除
- 删除有一个子节点的节点:用子节点替代
- 删除有两个子节点的节点:用中序后继(右子树最小值)替代被删节点的值,然后删除中序后继
Why(何因)- 为什么发明?
要解决的问题:
- 数组实现的有序表:查找O(log n)但插入/删除O(n)(需要移动元素)
- 链表:插入/删除O(1)但查找O(n)(无法二分)
- 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删除算法
功能需求(用精确的中文描述)
-
插入:向BST中插入一个值,保持BST性质
- 输入:根节点指针的指针、整数值
- 操作:从根出发,小则向左、大则向右,到达NULL处插入新节点;忽略重复值
- 输出:无(就地修改)
-
查找:在BST中查找一个值
- 输入:根节点指针、目标值
- 操作:从根出发,相等返回,小向左,大向右
- 输出:找到返回节点指针,未找到返回NULL
-
删除(Hibbard算法):删除指定值的节点,保持BST性质
- 输入:根节点指针的指针、目标值
- 情况1(叶节点):直接释放,父指针置NULL
- 情况2(一个子节点):用子节点替代
- 情况3(两个子节点):用中序后继的值替代,再删除中序后继
- 输出:无(就地修改)
-
中序遍历:按升序遍历所有节点
- 输入:根节点指针、输出数组指针、计数器指针
- 操作:左-根-右递归遍历,依次填入数组
- 输出:有序数组和节点数
-
最小值/最大值:返回BST中最小/最大节点
- 最小:一直向左走
- 最大:一直向右走
-
树高度:计算BST的高度(空树为-1,单节点为0)
- 输入:根节点指针
- 操作:递归计算左右子树高度,取最大值+1
- 输出:整数高度
-
释放整棵树:释放所有节点内存
- 操作:后序遍历,先删子节点再删根
约束条件
- 重复插入同一值应被忽略(不改变树)
- 删除不存在的值应安全返回(不崩溃)
- 所有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)- 后序遍历释放所有节点