【数据结构】二叉搜索树--c++代码(含思路详解/递归的解释/不使用递归的思路及代码/函数返回值的解释)

目录

思路

代码实现

1.使用递归,二叉树的根节点定义为局部变量,插入函数有返回值

一些解释

2.未使用递归,root为局部变量,Insert无返回值

一些解释

&引用 

不使用递归的思路:


P27-P28,视频地址👇

http://www.bilibili.com/video/BV1Fv4y1f7T1?p=27&vd_source=02dfd57080e8f31bc9c4a323c13dd49c

思路

我们日常存储数据的时候,会先想到数组、链表,不但存储了数据还能对数据进行插入、删除这些基本操作,在之前链表的文章里,有提到过数组和链表做插入、删除这些操作的时间复杂度。

对于查找和删除,其二者所需时间复杂度是比较高的。于是为了降低时间复杂度,对于用数组存储的数据,出现了二分查找法,之前在算法部分也有提到,这里附上二分的核心思想。

二分查找算法思想:通过改变查找区间,不断更新mid的值,根据mid与目标值的大小关系,选择搜索区间的左半部分或右半部分继续搜索,直到其等于目标值或者确定目标值不存在。

通过每次将查找区间缩小一半来降低时间复杂度为logn 。

而二分的条件是数组中的数据本身是有序的。根据这个思想,二叉搜索树出现了。

二叉搜索树是二叉树的一种,其左子树上的所有节点的值都比该节点值要小,其右子树上所有节点都比该节点值大

这样就直接实现了数组中二分查找的思想,左边都是<root的,右边都是>root的,对于每一棵子树都是这样,那么我们想查找某一元素时,比较大小关系,每次都会舍弃一半

但是会存在最坏的情况,就是该二叉搜索树像一个链表一样,只存在一边有树,如下图

对于n个节点来说,最小的高度就是logn。最大高度是n-1。这里就是高度为n-1的情况,因此我们尽量保持树的高度小一点来保证树尽可能的平衡(左右子树高度差<1),这样会使我们各种操作的时间复杂度降低。

感觉提到二分就亲切了很多,之前接触过hhh像遇到了老朋友。

那么思想明白了之后,就要考虑如何进行代码的实现?

 首先作为一个二叉树,最多只能有两个孩子,在之前提到过我们通过创建一个包含三个元素(左子树地址、本节点数据、右子树地址)的结构体作为节点,就像链表一样,这是实现二叉树常用的方式。

然后按照之前链表的思路,我们会定义根节点,通过根节点连接到其他任意节点。之前都是定义为全局变量,这里我按定义为局部变量来写(老师可能是考虑到全局变量要谨慎使用)。我将定义为全局变量和局部变量都写过了,好像在这里区别不大,因为将其定义为全局变量就是为了哪里都能使用它,但是在这里我们有递归的应用,每次都需要传入需要递归的子树根节点,好像并没有简便很多。

接着思考插入函数。二叉树为空时,我们直接将该节点赋值为根节点即可;当二叉树不为空,我们知道根据我们上面所说的,左边应该存放<root的元素,右边放>root的元素,当我们分别处理这两种情况的时候,发现每次插入数据都是这几个步骤,于是我们想到树的递归属性。 

!!!!注意

提一下这里判断是进行左子树还是右子树执行递归的操作是可以通过三目运算符来实现的。也比较简便。

(data<=root->data)?Insert(root->left,data):Insert(root->right,data);

是可以用三目运算符来实现,只是要注意完整的写法

void Insert(Node *&root, int data)
{
    if (root == NULL)
    {
        root = getnew(data);
    }
    else{
        (data <= root->data) ? Insert(root->left, data) : Insert(root->right, data);
    }
}

我想强调的是,这里必须用if-else语句,代表非此即彼的执行要求。如果三目运算符直接写并不包含在else语句之内,那么倘若root为空,root->data会访问出错。(问就是在后面用到三目运算符写法的时候没有包含在else语句之内,检查很久才发现居然是这里/(ㄒoㄒ)/)

代码实现

1.使用递归,二叉树的根节点定义为局部变量,插入函数有返回值

//递归实现二叉搜索树
#include<iostream>
using namespace std;
struct Node{
    int data;
    Node* left;
    Node* right;
};
//创建新节点
Node* GetNewNode(int data)
{
    Node* newNode=new Node();
    newNode->data=data;
    newNode->left=newNode->right=NULL;
    return newNode;
}
//插入节点
Node* Insert(Node* root,int data)//这里传入的每一个root都作为局部变量,函数执行完就会销毁
{
    if(root==NULL)
    {
        root=GetNewNode(data);
    }
    else if(data>root->data)
    {
        root->right=Insert(root->right,data);
    }
    else{
        root->left=Insert(root->left,data);
    }
    return root;//所以该函数需要返回值来更改主函数中二叉树的结构
}
bool Search(Node* root,int num)
{
    if(root == NULL) {
		return false;
	}
	else if(root->data == num) {
		return true;
	}
	else if(num <= root->data) {
		return Search(root->left,num);
	}
	else {
		return Search(root->right,num);
	}
}
int main()
{
    //创建一个树根,这里作为局部变量
    Node* root = NULL;
	//向树中插入数据
	root = Insert(root,15);	
	root = Insert(root,10);	
	root = Insert(root,20);
	root = Insert(root,25);
	root = Insert(root,8);
	root = Insert(root,12);
	//输入想查找的数据
	int number;
	cout<<"Enter number be searched\n";
	cin>>number;

	if(Search(root,number) == true) cout<<"Found\n";
	else cout<<"Not Found\n";
    return 0;
}

一些解释

这里解释一下,使用这种方式书写可能会产生的疑问:将二叉树的根节点定义为局部变量,我们知道一棵二叉树只有一个根节点,在写插入函数的时候,为了使新节点真正插入到了主函数所定义的二叉树里,插入函数需要两个参数:一个是本树根节点,一个是新插入的数据。注意在这个写法中,我们传入的root指针是作为Insert插入函数的局部变量,为了保存该函数对二叉树的结构更改,我们需要将root的更改作为返回值,那么主函数就要对应的用root本身进行接收

这里再说清楚一点,避免有朋友产生root好像被更改了很多次的错觉(是我的错觉hhh),在我们的插入函数中,除了当树为空的时候直接对插入的第一个节点返回做了整棵树的根节点,其余情况都执行了递归,而递归时我们传入的都是对于子树来说的"根节点",我们通过递归找到了当前树最下层且符合其属性(大小关系)的"空树"进行"root"赋值真正插入数据,才开始由递归函数不断的返回到第一层的Insert插入函数,从而实现了对树的结构的更改,既然返回到了第一层函数,那么返回值当然还是本身传入的唯一的树的根节点了。大家这点不要误解了哈

2.未使用递归,root为局部变量,Insert无返回值

//不使用递归实现
#include<iostream>
using namespace std;
struct Node{
    int data;
    Node* left;
    Node* right;
};
Node* Getnode(int data)
{
    Node* temp=new Node();
    temp->data=data;
    temp->left=temp->right=NULL;
    return temp;
}
void Insert(Node* &root,int data)
{
    if(root==NULL)
    {
        root=Getnode(data);
    }
    Node* temp=root;
    Node* parent=NULL;
    //temp用来找到往哪个子树上插
    while(temp)//当temp为NULL的时候才会跳出循环,所以我们需要定义parent指针提前记录下即将插入的子树地址
    {
        parent=temp;//存储该节点的双亲结点
        temp=data<=temp->data?temp->left:temp->right;
    }
    if(data<=parent->data)parent->left=Getnode(data);
    else parent->right=Getnode(data);
}
bool Search(Node* root,int x)
{
    Node* temp=root;
    if(root->data==x)return 1;//由于后面while循环之后会再次判断树是否为空,所以这里就不再多余判断一次了

    while(temp!=NULL && x!=temp->data)//只有当这两个条件有一个满足的时候才会退出循环 注意两个条件的顺序,要先检查是否为空
    {
        temp=x<=temp->data?temp->left:temp->right;
    }
    if(temp==NULL)return 0;
    return 1;
}
int main()
{
    Node* root=NULL;
    Insert(root,5);
    Insert(root,50);
    Insert(root,15);
    Insert(root,0);
    int x;
    cin>>x;
    bool ans=Search(root,x);
    cout<<ans;
    return 0;
}

一些解释

&引用 

先解释这里Insert()没有返回值的问题,我们从上面那种写法可以知道,Insert()有返回值是为了接收每次树的结构的变化。而这里我们使用了c++语法中的 引用&操作。关于语法中的引用有很多应用,而这里我们用到的是它的其中一个功能:

函数参数传递:引用可用于函数参数传递,通过引用传递参数可以避免不必要的复制,同时能够修改原始数据。这被称为引用传递。

我的理解就是有点像指针一样,传参的时候加上这个符号,就能够更改主函数中该参数的值

其他的关于引用的知识点这里就不提了,感兴趣的可以搜一搜有关的文章。

不使用递归的思路:

我们使用递归就是为了层层递进。

对于插入函数:递归是为即将插入的新节点 找到没有孩子的符合数据大小关系的 位置(子空树)进行插入,而通过while循环来找到一个节点是我们在链表中就使用过的。所以这里也不难想到(其实没咋想到直接看热心网友的代码了[不好意思])。

对于搜索函数:递归是为了查找树中是否存在与目标值相等的数据,其结果无非是找到或者树都遍历完了也没找到。于是while的出口条件也就找到了。出了循环之后,再判断更新的temp值是否为空就知道返回什么了。

这里有两点需要注意:

1.插入函数中我们在处理树不为空的两种情况时,创建了一个parent节点和temp节点。temp节点很好理解,作为向下索引的头兵,但我们注意到while循环的出口正是temp为空,所以我们意识到temp会被赋值为空,所以我们需要创建一个新的节点来记录temp为空之前的父节点,这个父节点才是我们最后一步要执行插入操作的节点。对于temp节点不断更新的依据仍然是数据的大小关系,仍然可以使用三目运算符简化代码。

当然这里我也想过能不能不设这个parent节点,通过判断temp的下一个节点(左右子树)是否为空来判断。如下是chatGPT给出的代码。

Node* temp = root;

while (temp->left && data <= temp->data) {
    temp = temp->left;  // 移动到左子树
}

while (temp->right && data > temp->data) {
    temp = temp->right;  // 移动到右子树
}

// 此时,temp 指向要插入新节点的位置

我们发现相比定义两个节点来说,这种写法是比较麻烦的,且前者更经常用。

2.搜索函数中,对于树不为空的情况循环条件的书写,"temp!=NULL && x!=temp->data"要注意这两个条件的顺序。当temp为空的时候,倘若先判断数据,temp由于已经是空了,不存在temp->data,再访问会出错,所以temp!=NULL,应该写在前。


有问题欢迎指出,非常感谢!!!

也欢迎交流和建议哦。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值