数据结构——二叉搜索树(干货满满)

Posted 两片空白

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了数据结构——二叉搜索树(干货满满)相关的知识,希望对你有一定的参考价值。

一.定义

二叉搜索树是一种队排序和查找都很有用的特殊二叉树。又称为二叉排序树和二叉查找树。其定义如下:
1.二叉查找树为空树
2.非空左子树所有结点的值小于根节点的值
3.非空右子树所有结点的值大于根节点的值
4.左右子树都是二叉搜索树

事例:如图就是一颗二叉搜索树,根节点值为5,其左子树值均小于5,右子树的值均大于5,并且在左子树,右子树中均符合这种情况。
在这里插入图片描述

二.基本操作

树信息:

typedef int Elementtype;

typedef struct TreeNode{
	Elementtype val;
	struct TreeNode *left;
	struct TreeNode *right;
}Tree;

Tree *FindNode(Tree *obj, Elementtype x);//查找
int MaxData(Tree *obj);//找树的最大值
int MinData(Tree *obj);//找树的最小值
Tree *InsertNode(Tree **obj,Elementtype x);//插入,
Tree *DeletNode(Tree **obj, Elementtype x);//删除

这里的删除与插入为什么传入二级指针,而查找不需要二级指针?
因为插入与删除改变的是树的结构,要往树里插入或删除结点,而我在主函数里定义的是一个树指针变量初始化为空(如下图),改变树结构要将地址作为参数传递(传址)。而查找并没有改变树结构,所以只要传参。
如果,你是封装一个函数创建一树节点,即动态申请内存,将树信息值初始化后返回一地址,就不需要将定义的树指针的地址作为参数传入函数了。树指针变量里存的就是结点地址。
验证代码:

#include"searchtree.h"


int main(){
	Tree *s = NULL;
	InsertNode(&s, 5);
	InsertNode(&s, 1);
	InsertNode(&s, 3);
	InsertNode(&s, 7);
	InsertNode(&s, 6);
	InsertNode(&s, 4);
	InsertNode(&s, 2);
	DeletNode(&s, 6);
	//int max = MaxData(s);
	//printf("%d\\n", max);
	//int min = MinData(s);
	//printf("%d\\n", min);
	//Tree *t = FindNode(s, 8);
	system("pause");
	return 0;
}

1.查找

查找某一结点

在二叉搜索树中找到值为x的结点,并返回地址。

思想:由于二叉搜索树的定义与性质,过程如下:
1.	如果树为空树,返回空,查找失败
2.	如果查找值x比根结点值大,往右子树找。
3.	如果查找值x比根结点值小,往左子树找
4.	如果查找值x等于根节点值,返回地址,查找成功。

代码如下:

//这里注意是先return,再递归函数,要将最后的找到的值一直返回。
Tree *FindNode(Tree *obj, Elementtype x){//递归
	if (obj == NULL){
		return obj;
	}
	if (obj->val > x){
		return FindNode(obj->left, x);
	}
	else if (obj->val < x){
		return FindNode(obj->right, x);
	}
	else{

	}
	return obj;
}

由于递归效率一般比非递归的效率低,一般使用非递归迭代的方式实现查找。

//找某一树结点值为x的结点
//迭代方法,循环,当树不为空时,根据二叉搜索树的性质
//如果结点值比要找的值x大
//往左找,如果小往右找,退出循环时两种条件,为空和找到了
Tree *FindNode(Tree *obj, Elementtype x){//迭代
	while (obj){
		if (obj->val > x){
			obj = obj->left;
		}
		else if (obj->val < x){
			obj = obj->right;
		}
		else{
			break;
		}
	}
	return obj;//如果树为空,返回空,没找到,不为空返回地址。
}

查找树的最大值:

在这里插入图片描述

思路:最大值肯定在右边,并且时一直往右子树走,直到结点的右子树为空,当前结点的值就是树的最大值。
int MaxData(Tree *obj){//递归
	if (obj == NULL){
		return 0;//obj等于空,返回0
	}
	if(obj->right){
		return MaxData(obj->right);
	}
	return obj->val;
}

迭代:

//根据二叉树的性质,一直往右找,最大的那个结点的右边肯定为空(退出条件)
int MaxData(Tree *obj){//迭代
	if (obj == NULL){
		return 0;//obj等于空,返回0
	}
	while (obj->right){
		obj = obj->right;
	}
	return obj->val;
}

查找树的最小值:

在这里插入图片描述

思路:最小值肯定在左子树,并且时一直往左子树走,直到结点的左子树为空,当前结点的值就是树的最小值。

递归:

int MinData(Tree *obj){//递归
	if (obj == NULL){
		return 0;
	}
	if (obj->left){
		return MinData(obj->left);
	}
	return obj->val;

}

迭代:

//根据二叉树的性质,一直往左找,最小的那个结点的左边肯定为空(退出条件)
int MinData(Tree *obj){//迭代
	if (obj == NULL){
		return 0;
	}
	while (obj->left){
		obj = obj->left;
	}
	return obj->val;

}

上面两个函数不仅可以实现找到树的最大值与最小值,还能实现找到左子树的最大值,右子树的最小值。
对于为什么要实现找树的最大值与最小值?在后面删除时会用到。

2.插入

根据二叉树的性质,插入的结点一定会作为叶节点插入,不会插入到两个结点中间,我实现的插入,如果结点存在的话,就不进行插入。

思路:(与查找的思路差不多,但是返回不一样,将结点返回给上一节点)
**插入的点一定是查找失败的点,并且一定是空节点**。
1. 为空直接创建新节点插入
2.	如果插入值x比根结点值大,往右子树找插入点
3.	如果插入值x比根结点值小,往左子树找插入点
4.	如果插入值x等于根节点值,说明节点已经存在,不进行插入。
5.	如果没有值与插入值x相等并且,走到节点为空,说明查找失败,即为插入点。

要插入树节点,先创造一树节点

static Tree *TreeNodecreate(Elementtype x){//建立树节点
	Tree *treenode = (Tree *)malloc(sizeof(Tree));
	treenode->left = NULL;
	treenode->right = NULL;
	treenode->val = x;
	return treenode;
}

插入:

//插入的结点肯定会作为叶节点插入
//跟查找差不多,只是要找到插入位置(肯定为空结点),然后建一个结点返回。如果不是空结点说明存在这个结点。
//注意这里不是一直return,是将子节点结点的地址赋给父节点后再返回,相当于一层一层返回。
Tree *InsertNode(Tree **obj, Elementtype x){
	if (*obj == NULL){
		*obj = TreeNodecreate(x);
	}
	else{
		if ((*obj)->val > x){
			(*obj)->left = InsertNode(&((*obj)->left), x);//不是return,而是返回给上一节点
		}
		else if ((*obj)->val < x){
			(*obj)->right = InsertNode(&((*obj)->right), x);
		}
		else{
			printf("已存在\\n");
		}
	}
	return *obj;

}

3.删除

删除比其它操作复杂一点,但是思路与之前一样,只是考虑情况比较多。

思路:先找到要删除的点,再进行删除。

删除时要考虑四种情况:

情况1:删除节点为叶节点
	这种情况最简单,直接将其置空,释放,然后返回给父节点即可。
情况2:删除节点有左子树没有右子树
	先保存该节点的地址(方便释放),将该节点直接等于左子树地址,也就是将该节点指向左子树。
	相当于该节点存的就是左子树的地址,将原来节点地址覆盖了。然后释放。

在这里插入图片描述

	情况3:删除节点有右子树没有左子树
	与情况二处理相同,只是将左子树换为右子树。

在这里插入图片描述

情况4:既有左子树又有右子树
可以有两种解决办法:
1.找到左子树中的最大值,将值赋给给节点,然后将左子树最大值这个节点删除(删除可以用递归实现)
2.找到左子树中的最小值,将值赋给给节点,然后将右子树最小值这个节点删除
当时这样会有个弊端:当一直删除时,会导致树高度失衡,导致一边高,一边低,解决这样的办法可以删除左右子树最大最小节点交替实行。
或者记录一高度,主要删除,左子树或者右子树高的那一边。

代码如下:

//删除首先要找到要删除结点的位置,我一开始用的循环迭代的方法,当时发现如果删除的结点为叶节点时,不知道前面一个结点,不好删除
//于是想到递归,将要删除的这个结点地址返回给父节点,为叶节点时,直接将这个结点置空。
//找到结点后要考虑几种情况,
//1.左右还有子节点 2.左边有子节点,右边没有 3.左边没有子节点,右边有 4.左右都没有子节点(叶节点)
Tree *DeletNode(Tree **obj, Elementtype x){

	if ((*obj)->val > x){//找节点
		(*obj)->left = DeletNode(&((*obj)->left), x);
	}
	else if ((*obj)->val < x){
		(*obj)->right = DeletNode(&((*obj)->right), x);
	}
	else{


		if (obj == NULL){//没找到
			printf("结点不存在\\n");
		}
		else{//找到
			if ((*obj)->left != NULL && (*obj)->right != NULL){
				int x = MaxData((*obj)->left);
				(*obj)->left = DeletNode(&((*obj)->left), x);//要返回给左指针,不返回,左指针还是指向原来地址,
				(*obj)->val = x;                             //如果左边要删除的不是就是下一个还好,如果就是下一个,不返回做指针存的还是原来地址
			                                                 //所以要返回
			}
			else if ((*obj)->left != NULL && (*obj)->right == NULL){
				Tree *temp = (*obj);
				(*obj) = (*obj)->left;
				free(temp);
			}
			else if ((*obj)->left == NULL && (*obj)->right != NULL){
				Tree *temp = (*obj);
				(*obj) = (*obj)->right;
				free(temp);
			}
			else{
				Tree *temp = (*obj)->right;
				(*obj) = NULL;
				free(temp);
			}
		}

	}
	return (*obj);
}

注意:
为什么情况2/3直接等于左/右子树地址就使得上一个节点指向左/右子树,因为递归返回给了上一节点。

三.性质

二叉搜索树一个实用的性质:对二叉查找树中序遍历,遍历结果是有序的
我们直到二叉搜索树查找数值与树的高度有关,如果调整二叉查找树的形态,使得每个节点尽量有两个节点,那么二叉搜索树的高度就会很低,是得树的高度会在log(N)级别,N为节点数,能实现这一要求的数为平衡二叉树。

以上是关于数据结构——二叉搜索树(干货满满)的主要内容,如果未能解决你的问题,请参考以下文章

#yyds干货盘点# 二叉搜索树

#yyds干货盘点# 解决剑指offer:二叉搜索树与双向链表

#yyds干货盘点# LeetCode 腾讯精选练习 50 题:二叉搜索树中第K小的元素

#yyds干货盘点# leetcode算法题:验证二叉搜索树

#yyds干货盘点# LeetCode程序员面试金典:二叉搜索树序列

#yyds干货盘点# LeetCode 热题 HOT 100:验证二叉搜索树