二叉搜索树
在动态查找表中,数据元素会频繁地插入和删除,此时顺序查找效率太低,折半查找又依赖全表有序------二叉搜索树(又称二叉排序树)恰好解决了这一问题。它是一种特殊的二叉树,通过定义节点值的大小关系,让查找、插入、删除操作都能在树的高度范围内完成,兼顾了动态性和效率。无论是通讯录的动态管理,还是数据库的索引结构,二叉搜索树都有着广泛的应用。
1. 二叉搜索树的定义与结构
二叉搜索树是一种空树,或者是具有以下特性的二叉树:
- 若它的左子树不为空,则左子树上所有节点的值均小于它的根节点的值;
- 若它的右子树不为空,则右子树上所有节点的值均大于它的根节点的值;
- 它的左、右子树也分别是二叉搜索树。
这种特性保证了二叉搜索树的"有序性"------从根节点出发,左子树始终是"小值区域",右子树始终是"大值区域",就像一本按规则排列的字典,每个节点都是其左子树的"最大值"和右子树的"最小值"。
我们用一个具体的二叉搜索树示例(节点值为整数)来直观理解其结构,用mermaid绘制如下:
60 40 70 20 50 10 30 65 80
在这棵树中:
- 根节点60的左子树所有节点(40、20、50、10、30)均小于60,右子树所有节点(70、65、80)均大于60;
- 节点40的左子树(20、10、30)均小于40,右子树(50)大于40;
- 每个子树都满足同样的规则,例如节点20的左子树10小于20,右子树30大于20。
2. 二叉搜索树的查找操作
二叉搜索树的查找过程利用了其"左小右大"的特性,类似折半查找的"缩小范围"思想,但通过树的分支实现。具体步骤如下:
- 从根节点开始,将目标值
target与当前节点的值比较; - 若
target等于当前节点的值,查找成功,返回当前节点; - 若
target小于当前节点的值,说明目标可能在左子树,继续在左子树中查找; - 若
target大于当前节点的值,说明目标可能在右子树,继续在右子树中查找; - 若当前节点为空(即到达叶子节点的子树),则查找失败,返回空指针。
以之前的二叉搜索树为例,查找target=50的过程用mermaid绘制如下:
查找target=25的过程则是:
从根节点60→左子树40→左子树20→右子树30(25 < 30)→左子树为空,最终查找失败。
二叉搜索树查找的C语言实现(迭代版本,避免递归栈开销)如下:
c
// 二叉搜索树节点结构
typedef struct BSTNode {
int key; // 关键字
struct BSTNode *lchild, *rchild; // 左右孩子
} BSTNode;
// 查找关键字为target的节点,返回节点指针(失败返回NULL)
BSTNode* BST_Search(BSTNode *root, int target) {
while (root != NULL) { // 节点非空则继续查找
if (root->key == target) return root; // 找到目标,返回节点
else if (target < root->key) root = root->lchild; // 左子树查找
else root = root->rchild; // 右子树查找
}
return NULL; // 查找失败
}
代码说明:通过循环遍历树,根据目标值与当前节点值的大小关系,不断转向左或右子树,直到找到节点或遍历至空树,时间复杂度取决于树的高度。
3. 二叉搜索树的插入操作
插入操作的核心是"找到合适的位置并保持二叉搜索树的特性",新节点始终作为叶子节点插入(不破坏已有结构)。具体步骤如下:
- 若树为空,直接将新节点作为根节点;
- 若树非空,从根节点开始,类似查找过程:
1)比较新节点值key与当前节点值:- 若
key小于当前节点值,且当前节点左子树为空,则将新节点插入为左孩子; - 若
key大于当前节点值,且当前节点右子树为空,则将新节点插入为右孩子; - 若子树非空,则继续在对应子树中查找插入位置(注意:二叉搜索树通常不允许重复值,若
key与当前节点值相等,可视为插入失败或覆盖值)。
- 若
我们以"依次插入60、40、70、20、50、10、30、65、80"为例,展示二叉搜索树的构造过程(即上述示例树的形成过程),用mermaid分步绘制如下:
插入60 插入40 插入70 插入20 插入50 插入10 插入30 插入65 插入80 树为空,60作为根节点 40 < 60,插入60左子树 70 > 60,插入60右子树 20 < 40,插入40左子树 50 > 40,插入40右子树 10 < 20,插入20左子树 30 > 20,插入20右子树 65 < 70,插入70左子树 80 > 70,插入70右子树 插入60 插入40 插入70 插入20 插入50 插入10 插入30 插入65 插入80
每一步插入都遵循"左小右大"的规则,最终形成的树保持了二叉搜索树的特性。插入操作的C语言实现如下:
c
// 插入关键字为key的新节点,返回根节点(处理空树情况)
BSTNode* BST_Insert(BSTNode *root, int key) {
if (root == NULL) { // 树为空,创建新节点作为根
root = (BSTNode*)malloc(sizeof(BSTNode));
root->key = key;
root->lchild = root->rchild = NULL;
return root;
}
if (key < root->key) // 插入左子树
root->lchild = BST_Insert(root->lchild, key);
else if (key > root->key) // 插入右子树
root->rchild = BST_Insert(root->rchild, key);
// 若key相等,此处不处理(视为不插入重复值)
return root;
}
代码说明:采用递归实现,当找到空位置时创建新节点,否则递归向左或右子树插入,确保新节点作为叶子节点插入,维持树的特性。
4. 二叉搜索树的删除操作
删除操作是二叉搜索树中最复杂的操作,需要在删除节点后仍保持"左小右大"的特性。根据被删除节点的子树情况,分为三种情况:
(1)被删除节点是叶子节点(无左、右子树)
直接删除该节点,将其双亲节点的对应指针(左或右孩子)设为NULL即可。例如,在示例树中删除节点10(叶子节点),只需将节点20的左指针设为NULL,树的其他结构不变。
(2)被删除节点只有一棵子树(左子树或右子树)
用子树替代被删除节点的位置:若节点只有左子树,则将左子树连接到其双亲节点的对应指针;若只有右子树,则将右子树连接过去。例如,删除节点20(左子树10,右子树30),假设节点20是节点40的左孩子,则将节点40的左指针指向节点20的左子树(10)或右子树(30)------实际中需判断哪棵子树存在,此处节点20左右子树均存在,不适用该情况,仅举例说明逻辑。
(3)被删除节点有两棵子树(左、右子树均非空)
这种情况最复杂,需找到"替代节点"来填补被删除节点的位置,替代节点需满足:值大于左子树所有节点,且小于右子树所有节点(即保持树的有序性)。通常选择"中序前驱"或"中序后继"作为替代节点:
- 中序前驱:被删除节点左子树中值最大的节点(左子树的最右节点);
- 中序后继:被删除节点右子树中值最小的节点(右子树的最左节点)。
替代后,删除该替代节点(替代节点必为叶子节点或只有一棵子树,可用前两种情况处理)。
以示例树中删除根节点60(有左右子树)为例,步骤如下:
- 找中序后继:右子树70的最左节点65(右子树中值最小);
- 用65替代60的位置,此时根节点变为65;
- 删除原节点65(它是节点70的左孩子,且为叶子节点),将70的左指针设为NULL。
删除后的树结构用mermaid绘制如下:
65 40 70 20 50 10 30 NULL 80
删除操作的C语言实现(核心逻辑)如下:
c
// 删除关键字为key的节点,返回根节点
BSTNode* BST_Delete(BSTNode *root, int key) {
if (root == NULL) return NULL; // 树空,无需删除
if (key < root->key) // 向左子树查找删除
root->lchild = BST_Delete(root->lchild, key);
else if (key > root->key) // 向右子树查找删除
root->rchild = BST_Delete(root->rchild, key);
else { // 找到待删除节点
if (root->lchild == NULL) { // 无左子树(含叶子节点)
BSTNode *temp = root->rchild;
free(root);
return temp;
} else if (root->rchild == NULL) { // 无右子树
BSTNode *temp = root->lchild;
free(root);
return temp;
}
// 有两棵子树,找中序后继(右子树最左节点)
BSTNode *temp = root->rchild;
while (temp->lchild != NULL) temp = temp->lchild;
root->key = temp->key; // 用后继值替代
root->rchild = BST_Delete(root->rchild, temp->key); // 删除后继
}
return root;
}
代码说明:递归查找待删除节点,根据子树情况处理:无左子树则用右子树替代,无右子树则用左子树替代,有两棵子树则用中序后继替代并删除后继节点,确保删除后仍为二叉搜索树。
5. 二叉搜索树的中序遍历特性
二叉搜索树的中序遍历(左子树→根节点→右子树)具有特殊意义------遍历结果是一个递增的有序序列。这是由其"左小右大"的特性决定的,例如示例树的中序遍历结果为:10, 20, 30, 40, 50, 60, 65, 70, 80,恰好是递增序列。
这一特性可用于验证二叉搜索树的正确性:若一棵二叉树的中序遍历结果是有序的,则它是二叉搜索树(反之亦然)。同时,中序遍历也为查找"第k小元素""前驱/后继节点"等操作提供了便捷方式。
6. 二叉搜索树的查找效率分析
二叉搜索树的查找、插入、删除操作的时间复杂度均取决于树的高度h,即O(h)。树的高度与节点插入顺序密切相关:
- 最好情况 :树接近平衡(左右子树高度差较小),此时
h ≈ log₂n(n为节点数),时间复杂度为O(logn),与折半查找相当; - 最坏情况 :节点按有序序列插入(如10,20,30,40),树退化为单支树(斜树),此时
h = n,时间复杂度退化至O(n),与顺序查找相同; - 平均情况 :若节点插入顺序随机,树的高度约为
logn,平均时间复杂度为O(logn)。
例如,插入序列为60,40,70,20,50(随机顺序),树的高度为3(log₂5≈2.32),接近平衡;而插入序列为10,20,30,40,50(递增顺序),树的高度为5,退化为斜树。
这种效率的不稳定性是二叉搜索树的主要缺陷,后续的平衡二叉树、红黑树等结构正是为了解决这一问题,通过维护树的平衡性来保证高效的操作性能。
综上,二叉搜索树通过"左小右大"的特性实现了动态查找表的高效操作,查找、插入、删除均能在树高范围内完成。其核心优势是动态性------无需像折半查找那样依赖全表有序,插入删除只需调整少量指针;但缺点是效率受树的形态影响较大,最坏情况下性能较差。理解二叉搜索树的操作逻辑和特性,是学习更复杂平衡树结构的基础,也为实际应用中选择合适的查找结构提供了依据。