提示: 居上位而不骄,在下位而不忧
文章目录
本文会主要写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;
}
可执行代码运行图
部分重复功能未填入 名字几乎都是一样可直接替换 我都执行过, 应该没有什么问题
测试用树
测试结果
我来问你 作者写这么多可容易?所以还不点一个赞 你的点赞是对作者一种莫大的鼓励