数据结构之二叉搜索树

本文详细介绍了二叉搜索树(BST)的基本概念和操作,包括创建、插入、删除和查找。文中提供了多种插入方法,包括递归和非递归实现,并讨论了插入时返回父节点的策略。此外,还详细阐述了删除节点的三种情况,确保树的有序性。最后,文章展示了代码实现和运行截图,帮助读者更好地理解和应用二叉搜索树。
摘要由CSDN通过智能技术生成

提示: 居上位而不骄,在下位而不忧


本文会主要写BST(二叉搜索树)的创建,插入,删除,查找
为了防止有人问我为什么叫BST 不能叫狗剩啥的 当然如果你愿意没有丝毫问题,
Bi 在英文种作为一种词根 是2的意思,
S=Sreach搜索
T=Tree 树
下文我们简称BST

前言

有个人问我,为什么这个引用类型里面传的是T,而且修改了为什么他的父亲也能感受到,这是因为呀,这里用的是引用类型,简单来说递归调用的时候是替换而不是新建,你可以将调用的T就看成是T->Lnext;相当于是对T->Lnext进行赋值
写到二叉搜索时 或者叫做二叉排序树,我想许多人 应该不会陌生 ,但是这里还是讲一下它的概念 简单来说就是一种特定的二叉树,左子树上的所有结点的关键字均小于根结点的关键字 :右子树上的所有结点的关键字均大于根结点的关键字,左子树和右子树又各是一个二叉排序树,
而且你可能听过中序遍历情况下是从小往大的,值是从小往大的,但是你考虑一下为什么会是这样的?,原因是中序遍历是先左子树,然后根结点,然后右子树,若是我们使用的是递归版本,也就是会把左子树解决掉才会解决根结点 ,在解决左子树 的过程中 又会先解决左子树的左子树 再解决左子树的根结点 所以第一个解决的是树中最左结点,然后解决此时最左子树的根结点 然后是最左子树的右节点,通过归纳总结 所以整棵树中序就是有序的,我们前面也说过中序遍历就是人的名 树的影 所以树的投影也就是从小往大递增的

二、常见操作

2.1 创建BST

我们若是插入一个结点在二叉树种,结点判断与此时结点的大小 到底往左走 还是往右 走 直到来到了 一个空位置 ,它做了上去 有点类似与 大家玩过的游戏 猜数字,大了!! 大了!! 创建其实就是不停地插入,,

void Insert(BiNTree* &T,int val){ //此时我们要找的是一个空位置,
	if(T==NULL){//若是空位置也就是我们要插入的位置
		T=(BiNTree*)malloc(sizeof(BiNTree));
		T->data=val; 
		T->Lchild=NULL;
		T->Rchild=NULL;
		return;
	}
	if(val>T->data){//若是大于此时子树的树根就深入左子树
		 Insert(T->Rchild,val); 
	}
	if(val<T->data){//若是小于此时就深入右子树
		Insert(T->Lchild,val); 
	} 
} 
void CreatTree(BiNTree* &T,vector<int> vec){ //创建一颗二叉树就是不停的插入操作 
	if(vec.empty()) {
		T=NULL;return;
	} 
	for(int i=0;i<vec.size();i++){ 
		Insert(T,vec[i]);
	}
}

2.2关于插入的思考

方式一

如上面创建中的一样

方式二

上面版本一 有时给人一种感觉 父子关系不是特别清楚 这里我们提供这样一种方式 其实也跟上面差不多
主要是这个和下面的return 怎么理解 相当于交给下面的任务完成了 我们一直向上提交就行了 因为每一次插入的是一个值,插入成功就会一直执行return 语句 注意理解这个“一直“的意思

BiNTree* Insert(BiNTree* &T,int val){ //此时我我们要找的是一个空位置但是要连接  
	if(T==NULL){//若是空位置也就是我们要插入的位置
		T=(BiNTree*)malloc(sizeof(BiNTree));
		T->data=val; 
		T->Lchild=NULL;
		T->Rchild=NULL;
		return T; 
	}
	if(val>T->data){//若是大于此时子树的树根就深入左子树
		T->Rchild=Insert(T->Rchild,val); 
		return T;
	}
	if(val<T->data){//若是小于此时就深入右子树
		T->Lchild=Insert(T->Lchild,val); 
		return T;
	} 
	
} 
void CreatTree(BiNTree* &T,vector<int> vec){ //创建一颗二叉树就是不停的插入操作 
	BiNTree* pre;pre->Lchild=T; pre->Rchild=T;pre->data=-1;
	if(vec.empty()) {
		T==NULL;return;
	} 
	for(int i=0;i<vec.size();i++){ 
		Insert(T,vec[i]);
	}
}

方式三 非递归版

版本一(带头结点):

有人可能会说 就是不懂递归,那你大部分树的题可不好做,但是绝对不包括这一题,这里我们也写一下非递归版本的,非递归版本使用的是什么其实也是双指针 ,这里的双指针有点类似于链表中的的双指针 ,我给树也加了一个头节点,pre指向待插入位置的父亲,cur指向待插入位置 这里不能直接在像之前递归的时候一样用T作为指针 若是用的话 T的指向一直改变 你就不是每一次插入都是从头可以查找 而是上一次插入时候 的末结点的位置。

void Insert(BiNTree* &T,int val,BiNTree* pre){ //此时我们要找的是一个空位置但是要连接  所以此时 
	cout<<pre->data<<" ";
	BiNTree* cur=T;//这里千万不能像前面一样用跟作为指针  要重新定义一个指针 
	while(cur!=NULL){//一直走直到它找到它的位置 
		if(val>cur->data){//右深入 
			pre=cur;cur=cur->Rchild; 
		}
		else{//左深入 
			pre=cur;cur=cur->Lchild;
		}
	}
	if(cur==NULL){
		cur=(BiNTree*)malloc(sizeof(BiNTree));
		cur->data=val;
		cur->Lchild=NULL;
		cur->Rchild=NULL;
		if(pre->data==-1){//将插入的是第一个结点
			T=cur;
			//pre->Lchild=cur;
			//pre->Rchild=cur;
			/*下面两句是不对的 我本来写的是这个 ,这里做一下标记
			 我原本想法是按照链表的形式来 头结点里面放第一个结点是一样的,
			 但是这里却不行 ,因为我们访问树  并不是从头洁点开始的,
			 我们是从首结点开始的,所以依然访问的是T 他就找不到刚才插入的cur, */
		}
		else if(pre->data>val){
			pre->Lchild=cur;
		}
		else{
			pre->Rchild=cur;
		}	
	}
	
} 
void CreatTree(BiNTree* &T,vector<int> vec){ //创建一颗二叉树就是不停的插入操作 
	BiNTree* pre;pre->Lchild=T; pre->Rchild=T;pre->data=-1;
	if(vec.empty()) {
		T==NULL;return;
	} 
	for(int i=0;i<vec.size();i++){ 
		Insert(T,vec[i],pre);
	}
}

无论带不带头节点,其中的第一个结点的插入,也就是当树为空树的时候,是需要特殊的拿出来讨论的,因为我们自认为这个Pre是指向这个cur的前驱的,我们甚至需要用Pre的值与这个新插入的结点进行比较,所以这个pre自然是不能为空的。

void Insert(BiNTree* &T,int val){ //此时我们要找的是一个空位置
	if(T==NULL){
		T=(BiNTree*)malloc(sizeof(BiNTree));
		T->Lchild=NULL;
		T->Rchild=NULL;
		T->data=val; 
	} 
	else{
		BiNTree* pre=T;BiNTree* cur=T;//定义两个指针 让他们先在一起  然后让cur一个多走一步不就可以了
		while(cur!=NULL){
			pre=cur;
			if(val>cur->data)cur=cur->Rchild;
			else cur=cur->Lchild;
		} 
		cur=(BiNTree*)malloc(sizeof(BiNTree));
		cur->Lchild=NULL;
		cur->Rchild=NULL;
		cur->data=val;
		if(pre->data>val) pre->Lchild=cur;
		else pre->Rchild=cur;	
	}
} 
void CreatTree(BiNTree* &T,vector<int> vec){ //创建一颗二叉树就是不停的插入操作 
	BiNTree* pre;pre->Lchild=T; pre->Rchild=T;pre->data=-1;
	if(vec.empty()) {
		T==NULL;return;
	} 
	for(int i=0;i<vec.size();i++){ 
		Insert(T,vec[i]);
	}
}

带头节点(单指针)

#include<bits/stdc++.h>
typedef struct BST{
	int data;
	BST* Lnext;
	BST* Rnext; 
}BST;
using namespace std;
void insert(BST* &Pre,int val){
	if(Pre->Lnext==NULL){//申请一个结点放入进去便可 
		Pre->Lnext=(BST*)malloc(sizeof(BST));
		Pre->Rnext=Pre->Lnext;
		Pre->Lnext->Lnext=NULL;
		Pre->Lnext->Rnext=NULL; 
		Pre->Lnext->data=val;
		cout<<"此时将根结点赋值为"<<Pre->Lnext->data<<endl;
		return;
	}
	BST* cur=Pre->Lnext;
	while(!((cur->Lnext==NULL&&val<cur->data)||(cur->Rnext==NULL&&val>cur->data))){
		if(val<cur->data){
			cur=cur->Lnext;
		}
		else{
			cur=cur->Rnext;
		}
	}
	if(cur->Lnext==NULL){
		cur->Lnext=(BST*)malloc(sizeof(BST));
		cur->Lnext->data=val;
		cur->Lnext->Lnext=NULL;
		cur->Lnext->Rnext=NULL; 
		cout<<"此时在"<<cur->data<<"的左子插入值"<<cur->Lnext->data<<endl; 
	}
	else{
		cur->Rnext=(BST*)malloc(sizeof(BST));
		cur->Rnext->data=val;
		cur->Rnext->Lnext=NULL;
		cur->Rnext->Rnext=NULL;
		cout<<"此时在"<<cur->data<<"的右子插入值"<<cur->Rnext->data<<endl; 
	}
}
int main(){
	cout<<"请输入你要创建的值"<<endl;
	int input;vector<int> V;BST* Pre=(BST*)malloc(sizeof(BST));
	BST* T=NULL;
	Pre->Lnext=T;Pre->Rnext=T;Pre->data=-1;
	while(scanf("%d",&input)!=EOF) V.push_back(input);
	for(int i=0;i<V.size();i++){
		cout<<"j"; 
		insert(Pre,V[i]);
	}  
	return 0;
}

运行截图

请添加图片描述
同样的道理不带头节点 并且单指针的我相信你也可以写出来

2.3BST的插入返回父节点的方式

但是给大家一个思考题 若是需要返回插入值的父节点大家怎么办?
其实我们可以有几种解法: 这里说一下个人的愚见 可以通过返回指针来搞 ,返回值来搞,

双指针法

其实双指针法是最有用的 自己再给树加上一个头节点,这个头节点存放值的是-1 两个指针域指向都是root ,这里说一下为什么有这个想法 ,类比想到的

BiNTree* Insert(BiNTree* &T,int val,BiNTree* pre){ //此时我我们要找的是一个空位置但是要连接  所以此时 
	if(T==NULL){//若是空位置也就是我们要插入的位置
		T=(BiNTree*)malloc(sizeof(BiNTree));
		T->data=val; 
		T->Lchild=NULL;
		T->Rchild=NULL; 
		return pre;
	}
	if(val>T->data){//若是大于此时子树的树根就深入左子树
		Insert(T->Rchild,val,T); 
	}
	if(val<T->data){//若是小于此时就深入右子树
		Insert(T->Lchild,val,T); 
	} 
} 
void CreatTree(BiNTree* &T,vector<int> vec){ //创建一颗二叉树就是不停的插入操作 
	BiNTree* pre;pre->Lchild=T; pre->Rchild=T;pre->data=-1;
	if(vec.empty()) {
		T==NULL;return;
	} 
	for(int i=0;i<vec.size();i++){ 
		Insert(T,vec[i],pre);
	}
}

2.4 查找

聪明如你, 我们这里就简单的写一个递归的 当然方法 我知道你肯定也不止一个
这里我就写一个 你肯定可以的

BiNTree* Search(BiNTree* T,int val){
	if(T==NULL){
		cout<<"未发现你要查找的值"<<endl; 
		return NULL;
	} 
	if(T->data>val){
		return Search(T->Lchild,val);
	} 
	else if(T->data<val){
		return Search(T->Rchild,val);
	}
	else{
		return T;
	}
}

这里注意这里返回的T是查找的T,因为这个T返回上一层,被上一层直接返回了,所以也就相当于一路成交上去的。

删除结点(重点)

为什么他是重点 因为它像较于插入比较难,因为插入的时候是在叶子结点的空结点上的操作,不涉及结构的改变 但是删除却有可能改变结构
相信许多博客中关于怎么删
这里我就顺便写一下,我们知道二叉搜索树中 它的投影是有序的, 、
(1)若是我们删除的是叶子结点 当然没有问题 直接删就好 不会涉及到序列递增的问题
(2)若是删除的是有一个子树的结点,此时待删除结点的下一个结点就是最接近它的结点,直接让它的子树来取代它的位置 此时投影的有序性依然是不会改变的,
(3) 若是删除的是有两个子树的的结点
此时为了保证有序性,让谁来取代它的位置 左子树的最右孩子 右子树的最左孩子 也就是与这个待删除结点值最接近的结点值来取代它的位置
但是这其中有一个代码技巧 就是 删除结点的时候 先用要取代的值来替换删除的值,然后删除左子树的右孩子 右子树的最左孩子便可
想必此时你心中已经有了主意 我们开始写代码

void Delete(BiNTree* &T,int val){
	BiNTree* pre=NULL;//用于指向待删除结点的父节点
	BiNTree* cur=T; 
	while(cur&&val!=cur->data){//用于遍历树来寻找待删除的结点 
		pre=cur;
		if(cur->data>val){
			cur=cur->Lchild;
		} 
		else{
			cur=cur->Rchild;
		}
	}
	if(cur==NULL||cur->data==val){
		if(cur==NULL){
			cout<<"你输入的值不正确"<<endl;
			return;
		}
		else{//此时就是根据待删除结点的孩子的情况 分成三种情况来处理
			if(T->Lchild==NULL&&T->Rchild==NULL){//为叶子结点 
				//若是叶子结点直接删除即可; 让其前驱指向后继的后继 
				//但是因为我们前驱定义的方式 使用根结点需要特殊考虑
				if(cur==T){
					T=NULL;
				} 
				else if(pre->Lchild&&pre->Lchild==cur){//要知道前驱与后继的关系 到底是左孩子还是右孩子 
					pre->Lchild=NULL;
				} 
				else{
					pre->Rchild=NULL;
				} 
			}
			else if(cur->Rchild!=NULL&&cur->Lchild!=NULL){//有两个结点 我们有两种方式 用左子树的最右结点或者右子树的最左结点来代替
			//找到要代替结点的值进行值覆盖  这个代替的结点可能是是叶子结点 也可能是有一个孩子的结点,这里我们选择右子树的最左孩子 
				 //首先要来找这个最左孩子  我们等一会还要删 所以这里也要使用双指针  但是这里不需要讨论是否cur 为根 因为我们使用的是值覆盖 
				 //删除结点是右子树的最左结点,它肯定有前驱 
				BiNTree* di_cur=cur->Rchild;
				BiNTree* di_pre=cur;
				while(di_cur->Lchild)di_pre=di_cur,di_cur=di_cur->Lchild;
				cout<<"此时找到了待删除结点是"<<cur->data<<endl; 
				cout<<"此时找到了待删除直接后继的前驱是"<<di_pre->data<<endl; 
				cout<<"此时找到了待删除的后继是结点"<<di_cur->data<<endl; 
				  //赋值给待删除结点 
				cur->data=di_cur->data; 
				  //来删除此时右子树的最左结点  但是这个最左结点依然有两种情况  有没有孩子  但是若是有孩子的话一定是右孩子 
				  //若是叶子结点 则我们需要知道与di_pre之间的关系   是它的左子树还是右子树
				if(di_pre->Lchild==di_cur){
					di_pre->Lchild=di_cur->Rchild;
				} 
				else{
					di_pre->Rchild=di_cur->Rchild;
				}
			}
			else{//若是有一个子树的话 不仅要判断上待删除结点的前驱与其的关系 而且还有待删除结点与其后继的关系 
				if(cur==T){//这种双指针的方式中 都是需要单独考虑根结点 的情况因为根结点没有前驱 
					if(cur->Lchild){
						T=cur->Lchild; 
					} 
					else{
						T=cur->Rchild;
					} 
				}
				else if(pre->Lchild&&pre->Lchild==cur){//若是其前驱的左孩子是待删除结点的话,同样的条件下需要讨论待删除结点到底是左孩子存在 还是右孩子存在 
					 if(cur->Lchild){
					 	pre->Lchild=cur->Lchild;
					 }
					 else{
					 	pre->Lchild=cur->Rchild;
					 }
				}
				else{//若是其前驱的右孩子是待删除结点的话 
					if(cur->Lchild){
						pre->Rchild=cur->Lchild;
					}
					else{
						pre->Rchild=cur->Rchild;
					}
				} 
			}
		}
	}
}

删除改进版本

有个人问我,为什么这个删除里面传的是T,而且修改了为什么他的父亲也能感受到,这是因为呀,这里用的是引用类型,简单来说递归调用的时候是替换而不是新建,你可以将调用的T就看成是T->Lnext;相当于是对T->Lnext进行赋值
9月17 改

#include<bits/stdc++.h>
typedef struct BST{
	int data;
	BST* Lnext;
	BST* Rnext; 
}BST;
using namespace std;
BST* insert(BST* &T,int val){
	if(T==NULL){
		T=(BST*)malloc(sizeof(BST));
		T->data=val;
		T->Lnext=NULL;
		T->Rnext=NULL;
		cout<<"此时就是根结点 并没有父结点"<<endl;;
		return NULL; 
	}
	if(T->data<val&&T->Lnext==NULL){
		T->Lnext=(BST*)malloc(sizeof(BST));
		T->Lnext->data=val;
		T->Lnext->Lnext=NULL;
		T->Lnext->Rnext=NULL;
		cout<<"此时返回的父节点是"<<T->data<<endl;
		return T;
	}
	else if(T->data>val&&T->Rnext==NULL){
		T->Rnext=(BST*)malloc(sizeof(BST));
		T->Rnext->data=val;
		T->Rnext->Lnext=NULL;
		T->Rnext->Rnext=NULL;
		cout<<"此时返回的父节点是"<<T->data<<endl;
		return T;
	}
	if(T->data<val){
		return insert(T->Lnext,val);
	}
	else{
		return insert(T->Rnext,val);
	}	
}
void Delete(BST* &T,int val){
	if(T->data==val){//此时T就是要删除的结点 
		//从此开始分三种情况
		if(T->Lnext==NULL&&T->Rnext==NULL){
			T=NULL;
		} 
		else if(T->Lnext==NULL||T->Rnext==NULL){//需要分两种情况 
			if(T->Lnext==NULL){
				T=T->Rnext; 
			}
			else{
				T=T->Lnext;
			}
		}
		else{//有两个子树
			BST* PreCur=T;
			BST* cur=T->Lnext;
			while(cur->Rnext){PreCur=cur,cur=cur->Rnext;};
			T->data=cur->data;
			//然后删除cur便可,但是需要判断cur与它父节点的关系 
			if(PreCur->Lnext==cur){
				PreCur->Lnext=cur->Lnext;
			}
			else{
				PreCur->Rnext=cur->Lnext;
			}
		}
	}
	else if(T->data>val){
		Delete(T->Rnext,val);
	}
	else{
		Delete(T->Lnext,val);
	}
}
void visit(BST* T){
	if(T==NULL) return;
	visit(T->Lnext);
	cout<<T->data<<" ";
	visit(T->Rnext);
}
int main(){
	cout<<"请输入你要创建的值"<<endl;
	int input;vector<int> V;BST* T=NULL;
	while(scanf("%d",&input)!=EOF) V.push_back(input);
	for(int i=0;i<V.size();i++){ 
		insert(T,V[i]);
	}
	while(1){
		cout<<"请输入你要删除的值"<<endl;
		cin>>input;
		Delete(T,input);
		visit(T);	
	}

	return 0;
}

可执行代码运行图

部分重复功能未填入 名字几乎都是一样可直接替换 我都执行过, 应该没有什么问题
测试用树
请添加图片描述
测试结果
请添加图片描述

我来问你 作者写这么多可容易?所以还不点一个赞 你的点赞是对作者一种莫大的鼓励

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值