前言
二叉搜索树作为一种经典的数据结构,它既有链表的快速插入与删除操作的特点,又有数组快速查找的优势;所以应用十分广泛,例如在文件系统和数据库系统一般会采用这种数据结构进行高效率的排序与检索操作。今天让我们来手撕一个二叉搜索树。
首先我们先要对二叉搜索树结点中的数据大小关系有清晰的认知,如图:
注意观察结点数据的大小关系,不难发现,结点的元素一定大于左结点数据,小于右结点数据。
1.二叉搜索树结点定义
先是常见头文件的定义:
#include <stdio.h>
#include <stdlib.h>
然后是数据域元素类型的宏定义,再宏定义个bool类型(C语言没有bool的数据类型)方便后面函数的判断结果返回:
#define eletype int
#define bool int
然后再把二叉树结点的结构体写好:
typedef struct TreeNode {
eletype val;
struct TreeNode *left;
struct TreeNode *right;
} TreeNode;
2.二叉搜索树结构体定义
二叉树只有左右两个结点,故而只需要一个根结点root即可:
typedef struct BinarySearchTree {
TreeNode* root; //因为二叉树只有左右两个结点,所以只需要一个指针root就能找到所有结点
} BinarySearchTree;
3.二叉搜索树的创建
只需要将根结点置空即可:
void BSTCreat(BinarySearchTree* t) {
t->root=NULL;
}
4.二叉搜索树的销毁
从根节点开始,逐步释放每个结点的空间:
void BSTRemove(BinarySearchTree* t,eletype value); //先定义一下removeNode
//BSTDestoroy要用
void BSTDestroy(BinarySearchTree* t) {
while(t->root){
BSTRemove(t,t->root->val);
}
}
5.二叉搜索树结点的插入
结点的插入需要考虑多种情况。情况一,该二叉搜索树为空,则直接返回NULL。情况二,要插入的数据小于该结点数据域元素,则应该继续往左子树找合适的插入位置,只要反复调用函数即可。情况三,插入的数据大于该结点数据域元素,同理应该往右子树找合适的插入位置,操作方法与情况二一样:
TreeNode* insertNode(BinarySearchTree* t,TreeNode* node,eletype value){
if(node==NULL){
TreeNode* tn=(TreeNode*)malloc(sizeof(TreeNode)); //插入数据要分三种情况讨论
tn->left=NULL; //一是该结点为空
tn->right=NULL; //那么直接将数据插入到这个结点
tn->val=value;
return tn;
}
if(value<node->val){ //要插入的元素小于该结点的数据域
node->left=insertNode(t,node->left,value); //用递归的方法再次调用insertNode
} else if(value>node->val){ //要插入的元素大于该结点的数据域
node->right=insertNode(t,node->right,value); //同样用递归的方法
}
return node; //每次调用函数返回的是TreeNode*结点
}
void BSTInsert(BinarySearchTree* t,eletype value){ //注意看这里的参数表,与indsertNode
//相比少了node
t->root=insertNode(t,t->root,value); //是因为从根结点开始就可以找到每个元素了
}
6.二叉搜索树结点的删除
删除结点的思路框架与结点的插入类似,首先判断搜索树是否为空,接着比较删除的目标数据与某处结点数据域元素,来决定是往右子树还是左子树搜索(注意在删除元素与该结点数据域元素相等的时候,要讨论左右子树的存在情况):
TreeNode* removeNode(BinarySearchTree* t,TreeNode* node,eletype value){
if(node==NULL){ //若该二叉树是空的,则直接返回NULL
return NULL;
}
if(value<node->val){ //这里的思路与元素的插入相似
node->left=removeNode(t,node->left,value); //判断要插入元素与该结点数据域的大小关系
} else if(value>node->val){
node->right=removeNode(t,node->right,value);
} else {
if(node->left==NULL&&node->right==NULL){ //从这里开始是value==node->val的情况
free(node); //若该节点无左右结点,说明该结点是叶子结点,则直接将空间释放掉
node=NULL; //然后将该结点置空
return node;
} else if(node->left==NULL){ //如果只有左结点是空的,那么我要做的是将
//该结点的右结点代替该结点
//来接收该结点的右结点
TreeNode* rightchild=node->right; //我们先定义一个结点rightchild
free(node); //然后放心的释放掉该节点的空间
node=rightchild; //然后将node更新为rightchild
return node;
} else if(node->right==NULL){
TreeNode* leftchild=node->left; //同理我们可以完成只有右结点为空的情况
free(node);
node=leftchild;
} else { //若该结点的左右结点都不为空
TreeNode* replacement=node->right; //这里我们先明确二叉搜索树的元素大小关
//系,左结点数据一定小于右结点数据
while(replacement->left==NULL){ // 所以我们的想法是删除该节点后,在右子
//树中找一个最小的结点数据代替该结点
replacement=replacement->left; //具体操作是,定义一个replacement
//接收该结点的右子树
} //然后找到右子树中最小的结点数据,只要不
//断往左结点靠就行
node->val=replacement->val; //然后将找的数据覆盖掉要删除的结点数据
node->right=removeNode(t,node->right,replacement->val); //而此时还要做的就是将
//右子树中找到的最小结点删除,调用removeNode即可
}
}
return node;
}
void BSTRemove(BinarySearchTree* t,eletype value){
t->root=removeNode(t,t->root,value);
}
在这里我们详细分析一下第三种情况(当比较时发现要删除的目标数据与比较的数据域元素相等):若该结点无左右结点,则说明该结点为叶子结点,那么只要将空间释放掉就可以了;如果只有左结点没有右结点,那么我们在将该结点空间释放前需要先将右结点保存起来,然后将该结点更新为预先存好的右结点;只有右结点的情况与前者操作相同;最后是最关键的一步,若左右结点都不为空,因为删除2该结点后依然要保持二叉搜索树的性质(即该结点数据始终大于左结点,小于右结点),而我们的思路是在该结点的右子树中找到最小的元素,将其赋值给要删除结点的数据域,然后再将刚找到的右子树的最小元素删除,从而达到删除该结点的目的。
7.二叉搜索树结点的查找
结点查找的逻辑比较简单,先判断是否为空树,然后通过比较元素大小来确定是继续往左子树还是与子树找:
void BSTRemove(BinarySearchTree* t,eletype value){
t->root=removeNode(t,t->root,value);
}
bool searchNode(BinarySearchTree* t,TreeNode* node,eletype value){
if(node==NULL){ //结点的查找比较简单
return NULL; //若为空则返回NULL
}
if(value<node->val){ //然后同样是常规的判断,比较查找的元素与该结点数据域元素大小关系
searchNode(t,node->left,value); //来决定是继续往左子树还是右子树找
} else if(value>node->val){
searchNode(t,node->right,value);
}
return 1; //注意返回值是布尔值,代表查找成功与否
}
bool BSTSearch(BinarySearchTree* t,eletype value){
return searchNode(t,t->root,value);
}
8. 二叉搜索树的中序遍历
void InOrder(BinarySearchTree* t,TreeNode* node){
if(node){
InOrder(t,node->left); //中序遍历在之前二叉树的实现中已经介绍过
printf("%d",node->val);
InOrder(t,node->right);
}
}
void BSTInOrderTraversal(BinarySearchTree* t){
InOrder(t,t->root);
printf("\n");
}
完整源码
#include <stdio.h>
#include <stdlib.h>
#define eletype int
#define bool int
typedef struct TreeNode {
eletype val;
struct TreeNode *left;
struct TreeNode *right;
} TreeNode;
typedef struct BinarySearchTree {
TreeNode* root; //因为二叉树只有左右两个结点,
//所以只需要一个指针root就能找到所有结点
} BinarySearchTree;
void BSTCreat(BinarySearchTree* t) {
t->root=NULL;
}
void BSTRemove(BinarySearchTree* t,eletype value); //先定义一下removeNode
//BSTDestoroy要用
void BSTDestroy(BinarySearchTree* t) {
while(t->root){
BSTRemove(t,t->root->val);
}
}
TreeNode* insertNode(BinarySearchTree* t,TreeNode* node,eletype value){
if(node==NULL){
TreeNode* tn=(TreeNode*)malloc(sizeof(TreeNode)); //插入数据要分三种情况讨论
tn->left=NULL; //一是该结点为空
tn->right=NULL; //那么直接将数据插入到这个结点
tn->val=value;
return tn;
}
if(value<node->val){ //要插入的元素小于该结点的数据域
node->left=insertNode(t,node->left,value); //用递归的方法再次调用insertNode
} else if(value>node->val){ //要插入的元素大于该结点的数据域
node->right=insertNode(t,node->right,value); //同样用递归的方法
}
return node; //每次调用函数返回的是TreeNode*结点
}
void BSTInsert(BinarySearchTree* t,eletype value){ //注意看这里的参数表,与indsertNode相比
//少了node
t->root=insertNode(t,t->root,value); //是因为从根结点开始就可以找到每个元素了
}
TreeNode* removeNode(BinarySearchTree* t,TreeNode* node,eletype value){
if(node==NULL){ //若该二叉树是空的,则直接返回NULL
return NULL;
}
if(value<node->val){ //这里的思路与元素的插入相似
node->left=removeNode(t,node->left,value); //判断要插入元素与该结点数据域的大小关系
} else if(value>node->val){
node->right=removeNode(t,node->right,value);
} else {
if(node->left==NULL&&node->right==NULL){ //从这里开始是value==node->val的情况
free(node); //若该节点无左右结点,说明该结点是叶子
//结点,则直接将空间释放掉
node=NULL; //然后将该结点置空
return node;
} else if(node->left==NULL){ //如果只有左结点是空的,那么我要做的是
//将该结点的右结点代替该结点
TreeNode* rightchild=node->right; //我们先定义一个结点rightchild来接收该
//结点的右结点
free(node); //然后放心的释放掉该节点的空间
node=rightchild; //然后将node更新为rightchild
return node;
} else if(node->right==NULL){
TreeNode* leftchild=node->left; //同理我们可以完成只有右结点为空的情况
free(node);
node=leftchild;
} else { //若该结点的左右结点都不为空
TreeNode* replacement=node->right; //这里我们先明确二叉搜索树的元素大小关
//系,左结点数据一定小于右结点数据
while(replacement->left==NULL){ // 所以我们的想法是删除该节点后,在右子
//树中找一个最小的结点数据代替该结点
replacement=replacement->left; //具体操作是,定义一个replacement接收该
//结点的右子树
} //然后找到右子树中最小的结点数据,只要不
//断往左结点靠就行
node->val=replacement->val; //然后将找的数据覆盖掉要删除的结点数据
node->right=removeNode(t,node->right,replacement->val); //而此时还要做的就是将
//右子树中找到的最小结点删除,调用removeNode即可
}
}
return node;
}
void BSTRemove(BinarySearchTree* t,eletype value){
t->root=removeNode(t,t->root,value);
}
bool searchNode(BinarySearchTree* t,TreeNode* node,eletype value){
if(node==NULL){ //结点的查找比较简单
return NULL; //若为空则返回NULL
}
if(value<node->val){ //然后同样是常规的判断,比较查找的元素与该结点数据
//域元素大小关系
searchNode(t,node->left,value); //来决定是继续往左子树还是右子树找
} else if(value>node->val){
searchNode(t,node->right,value);
}
return 1; //注意返回值是布尔值,代表查找成功与否
}
bool BSTSearch(BinarySearchTree* t,eletype value){
return searchNode(t,t->root,value);
}
void InOrder(BinarySearchTree* t,TreeNode* node){
if(node){
InOrder(t,node->left); //中序遍历在之前二叉树的实现中已经介绍过
printf("%d",node->val);
InOrder(t,node->right);
}
}
void BSTInOrderTraversal(BinarySearchTree* t){
InOrder(t,t->root);
printf("\n");
}
int main (){
BinarySearchTree bst;
BSTCreat(&bst);
BSTInsert(&bst,50);
BSTInsert(&bst,30);
BSTInsert(&bst,70);
BSTInsert(&bst,40);
BSTInsert(&bst,80);
BSTInsert(&bst,60);
BSTInsert(&bst,100);
printf("中序遍历:");
BSTInOrderTraversal(&bst);
BSTInsert(&bst,65);
printf("中序遍历:");
BSTInOrderTraversal(&bst);
return 0;
}