数据结构基础:P4.1-树(二)--->二叉搜索树

本系列文章为浙江大学陈越、何钦铭数据结构学习笔记,前面的系列文章链接如下
数据结构基础:P1-基本概念
数据结构基础:P2.1-线性结构—>线性表
数据结构基础:P2.2-线性结构—>堆栈
数据结构基础:P2.3-线性结构—>队列
数据结构基础:P2.4-应用实例—>多项式加法运算
数据结构基础:P2.5-应用实例—>多项式乘法与加法运算-C实现
数据结构基础:P3.1-树(一)—>树与树的表示
数据结构基础:P3.2-树(一)—>二叉树及存储结构
数据结构基础:P3.3-树(一)—>二叉树的遍历
数据结构基础:P3.4-树(一)—>小白专场:树的同构-C语言实现


前言

我们先来回忆一下前面提到过的查找问题

查找问题有两类,一类叫静态查找,就是我们要找的集合的元素是不动的。所以也就是说在一个集合上主要做的是find操作,而没有插入、删除等操作发生。另外一种查找就是我们要找的这个对象的集合本身会动态地发生变化,也就是说经常要发生插入删除操作。对于静态查找问题,我们前面提到过一个很好的方法就是二分查找,二分查找把一般的顺序查找的时间复杂性降到了 l o g 2 N {\rm{lo}}{{\rm{g}}_{\rm{2}}}{\rm{N}} log2N。为什么二分查找效率这么好,其中很重要的原因就是我们把要查找的数据事先进行的有效的组织。这样的话给定了n个数,我们的查找的顺序可以形成一个判定树的结构。所以这就把一个线性的查找过程变成是在一个树上的查找过程,而查找效率就是树的高度。


一、二叉搜索树及查找

从上面我们得出一个启示:

有没有可能直接把元素就放到树上,不要放在数组里面。放在一个树上的一个好处就是树的动态性比较强,插入和删除操作比在线性数组里面做要方便,这就是我们说的二叉查找树或者叫二叉搜索树


1.1 二叉搜索树的建立

二叉搜索树中元素的组织方式

从前面的判定树我们得到一个启示,我们有没有可能把数据按这种方式来组织:树上的任何一个结点的值比它的左子树的所有结点值都大,比右子树的所有结点值都小。这样我们的查找过程就变成对当前结点的一个判断,如果比当前结点的值要小,那么我们就到左边去找。如果比它大,到右边去找。如果相等,则找到了。这样把我们的查找范围一下子缩小了一大部分。

二叉搜索树的定义

二叉搜索树(BST,Binary Search Tree),也称二叉排序树或二叉查找树。可以为空,如果不为空则满足以下性质:
非空左子树的所有键值小于其根结点的键值。
非空右子树的所有键值大于其根结点的键值。
左、右子树都是二叉搜索树。
例子:我们来看下面这两棵树是不是二叉搜索树
在这里插入图片描述
可以看出第一个不是,因为10那个结点右边是5,小于10。第二个是,因为满足左小右大。


1.2 二叉搜索树的函数

二叉搜索树的函数

我们主要的目的是要做查找,另外还要做插入删除。对于查找操作来讲,有三种查找:一种是最普通的查找,给你一个键值,找到等于这个值的元素在哪里,把它的地址告诉我。另外两种就是在这个树里面找最小值或最大值。
①Position Find(ElementType X, BinTree BST):从二叉搜索树BST中查找元素X,返回其所在结点的地址;
②Position FindMin(BinTree BST):从二叉搜索树BST中查找并返回最小元素所在结点的地址;
③Position FindMax(BinTree BST) :从二叉搜索树BST中查找并返回最大元素所在结点的地址。
④BinTree Insert(ElementType X, BinTree BST):插入一个新的结点,它的值等于X
⑤BinTree Delete(ElementType X, BinTree BST):删除值等于 X 的结点


查找指定元素

查找的思路是很简单的,就是跟根结点去做比较。如果x比根结点值大,那么就到右边去找。x的值比根结值小,就到左边去找。如果又不小又不大就是相等了。当然如果树是空那么是直接返回NULL。
在这里插入图片描述

对应代码如下

//递归实现
Position Find( ElementType X, BinTree BST )
{
	if( !BST ) return NULL; /*查找失败*/
	if( X > BST->Data )
		return Find( X, BST->Right ); /*在右子树中继续查找*/
	else if( X < BST->Data )
		return Find( X, BST->Left ); /*在左子树中继续查找*/
	else /* X == BST->Data */
		return BST; /*查找成功,返回结点的找到结点的地址*/
}

//循环实现
Position IterFind( ElementType X, BinTree BST )
{
	while( BST ) {
		if( X > BST->Data )
			BST = BST->Right; /*向右子树中移动,继续查找*/
		else if( X < BST->Data )
			BST = BST->Left; /*向左子树中移动,继续查找*/
		else /* X == BST->Data */
			return BST; /*查找成功,返回结点的找到结点的地址*/
	}
	return NULL; /*查找失败*/
}

分析:

这里大家可以注意到递归实现的代码中,这个递归是一种伪递归,就是在程序分支的最后要返回的时候再出现递归。从遍历的角度来讲,尾递归都可以用循环来进行实现了,所以递归实现的方法效率不是很高。


最大查找与最小查找

根据查找树的特点(比左边的大比右边的小),可以得出最大元素一定是在最右边,最小元素一定是在最左边。也就是说从根结点开始一直往左边走到底就是最小值,往右边走到底就是最大值。
在这里插入图片描述

对应代码如下

//查找最小元素的递归函数
Position FindMin( BinTree BST )
{
	if( !BST ) 
		return NULL; /*空的二叉搜索树,返回NULL*/
	else if( !BST->Left )
		return BST; /*找到最左叶结点并返回*/
	else
		return FindMin( BST->Left ); /*沿左分支继续查找*/
}

//查找最大元素的迭代函数
Position FindMax( BinTree BST )
{
	if(BST )
		while( BST->Right ) 
			BST = BST->Right;
	/*沿右分支继续查找,直到最右叶结点*/
	return BST;
}

二、二叉搜索树的插入

我们看看给定二叉搜索树,我们要插入一个新的结点,同时保证我们插完之后这个树还是二叉搜索树。也就是说左子树的结点要比根结点小,右子树的结点要比根结点大。

我们来看一个具体的例子
在这里插入图片描述
这棵树的根结点是30,然后我想插入35,就把35跟30去做比较,发现比根结点大,就往右边走。然后再跟41去比较,发现比41小,就往左边走。然后再跟33比较,发现比33大,就往右边,往右边走发现是空的,所以这个时候我们知道要插入了。所以把35插在33的右边。
在这里插入图片描述
分析:
可以看出这个过程整个的框架应该是跟Find是一样的,但是跟Find也有个不一样的地方。
我们插入的时候,35这个结点要挂在33的右边,这个时候必须记住33的位置。如果你忘掉了,就不知道要挂哪里去了。我们有一种方法来处理这个事情。
①比方说从30开始,我把35跟30去做比较,发现比30大,所以我就往右边去递归去了。这个时候我们如果能要求递归函数给我返回一个插入在35之后的这个根结点的地址,然后我就把这个根结点挂在这个地方了。所以第一次跟30比较时,比30大,所以返回30的Right结点,也就是41那个结点。
②跟41对比,发现比41小,返回41的Left结点,也就是33那个结点。
③跟33对比,发现比33大,返回33的Right结点。可以看出是个空结点NULL,我们就把35插到这里,然后33的Right就不再是一个空结点了,就是35这个结点了。
我们看一个例子:以一年十二个月的英文缩写为键值,按从一月到十二月顺序输入,即输入序列为(Jan, Feb, Mar, Apr, May, Jun, July, Aug, Sep, Oct, Nov, Dec)
在这里插入图片描述

插入操作对应代码如下:

BinTree Insert( ElementType X, BinTree BST )
{
	if( !BST ){
		/*若原树为空,生成并返回一个结点的二叉搜索树*/
		BST = malloc(sizeof(struct TreeNode));
		BST->Data = X;
		BST->Left = BST->Right = NULL;
	}else /*开始找要插入元素的位置*/
		if( X < BST->Data )
			BST->Left = Insert( X, BST->Left);
	/*递归插入左子树*/
		else if( X > BST->Data )
			BST->Right = Insert( X, BST->Right);
	/*递归插入右子树*/
	/* else X已经存在,什么都不做 */
	return BST;
}

三、二叉搜索树的删除

怎么在查找树或者搜索树里面删除一个结点,同样的我们也一样要找到这个结点在哪里然后再进行删除,这里有几种情况:

要删除的结点是叶结点。如下面我们要删除35这个结点。
在这里插入图片描述
那就很容易删除,就把这叶结点拿掉,把它的父亲指向它的这个指针设为NULL就可以了。
要删除的结点只有一个儿子。如下面我们要删除33这个结点。
在这里插入图片描述
找到33的时候,知道33只有一个右儿子。在这个情况下直接把它删掉,并让它的儿子3535挂在41的左边就可以了。
在这里插入图片描述
比较复杂的是删除的结点左右两边都不空。如下面我们要删除41这个结点。
在这里插入图片描述
这个时候我们的一个策略是把复杂的情况简单化:我们知道没有儿子结点怎么删除了,只有一个儿子结点怎么删除了。当你有两个儿子结点的时候,我能不能把有两个儿子结点的情况转化成只有一个儿子或者没有儿子的结点。这样的一种情况是做得到的。
----3.1 在右子树里面找个最小的结点50来替代它,这就完成了删除过程。
在这里插入图片描述
----3.2 取左子树中的最大元素35替代它,然后将35那个结点删除。
在这里插入图片描述
这两种删除方法的好处是:左子树的最大值和右子树的最小值一定不是有两个儿子的结点。因为你是左子树的最大值,一定是在左子树的最右边,那么你就不可能再有右儿子。 右子树里面找一个最小值,它一定在右子树的最左边,那么也就说它没有左儿子。所以我们把这个删除的过程变成是左右两边找最小或者最大的过程,使得我们要删除一个有两个儿子的结点就变成了前面一种情况了:要么没儿子,要么只有一个儿子。

对应代码为:

BinTree Delete( ElementType X, BinTree BST )
{ 
	Position Tmp;
	if( !BST ) printf("要删除的元素未找到");
	else if( X < BST->Data )
		BST->Left = Delete( X, BST->Left); /* 左子树递归删除 */
	else if( X > BST->Data )
		BST->Right = Delete( X, BST->Right); /* 右子树递归删除 */
	else /*找到要删除的结点 */
		if( BST->Left && BST->Right ) { /*被删除结点有左右两个子结点 */
			Tmp = FindMin( BST->Right );
			/*在右子树中找最小的元素填充删除结点*/
			BST->Data = Tmp->Data;
			BST->Right = Delete( BST->Data, BST->Right);
	/*在删除结点的右子树中删除最小元素*/
		} else { /*被删除结点有一个或无子结点*/
			Tmp = BST;
			if( !BST->Left ) /* 有右孩子或无子结点*/
				BST = BST->Right;
			else if( !BST->Right ) /*有左孩子或无子结点*/
				BST = BST->Left;
			free( Tmp );
	}
	return BST;
}

小测验

1、已知一棵由1、2、3、4、5、6、7共7个结点组成的二叉搜索树(查找树),其结构如图所示,问:根结点是什么?
在这里插入图片描述

A. 1
B. 4
C. 5
D. 不能确定

答案:C

2、在上题的搜索树中删除结点1,那么删除后该搜索树的后序遍历结果是:

A. 243765
B. 432765
C. 234567
D. 765432

答案:A

3、若一搜索树(查找树)是一个有n个结点的完全二叉树,则该树的最大值一定在叶结点上(错误)

4、若一搜索树(查找树)是一个有n个结点的完全二叉树,则该树的最小值一定在叶结点上(正确)

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

知初与修一

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值