m数据结构 day20 查找(三)二叉查找树:动态查找中最重要的数据结构

引入

之前两篇关于查找的博文讲的都是静态查找表,一般都是线性表,查找和修改很方便,但是插入和删除就不方便了,插入或删除数据后要保证查找表仍然有序,对于静态查找表是不好做到的,因为要涉及大量元素的移动(数组实现线性表)或者一些指针的改动和大量比较操作。

插入和删除操作频繁的查找表,应该用动态查找表(需要在查找时插入或者删除的查找表)实现。静态查找表一般使用便于随机访问的线性表数据结构实现,而动态查找表则使用便于插入和删除的非线性数据结构实现,比如用二叉树,注意一般不会用m元树(m>2)。

本文的主角,二叉查找树,也叫做二叉排序树,就是第一种要介绍的动态查找表。

定义(递归)

在这里插入图片描述在这里插入图片描述

从第三点性质可以看到,二叉查找树也用递归实现定义。
在这里插入图片描述
虽然名字叫做二叉排序树,但是主要目的并不是为了排序哦,所以大名是二叉查找树,小名才是二叉排序树,毕竟确实也实现了排序,就把排序当做一个副产品好啦

用一个例子来比较线性表和二叉树的插入操作的难易

在这里插入图片描述
如果用线性表,第三个元素58小于62,如果用数组实现线性表,则需要把62和88往后移动;如果用链表,这里即头插。但是后面每一个元素都需要和前面已有的所有元素一一对比,比较操作太多了,不过链表的插入倒也不算特别麻烦,但是肯定是不可能用数组的····
在这里插入图片描述
用二叉树,第一个元素62作为根节点,第二个元素88大于62,于是作为右孩子,第三个元素58小于62,于是作为左孩子······,新元素的插入非常方便,比较操作又好实现又好理解,并且比较操作的数量很少——等于待查找结点在二叉查找树的层数。所以比较次数最少为1次(待查找结点就是根节点),次数最多为树的深度(待查找结点是最深的叶子结点)。
在这里插入图片描述
对二叉查找树进行中序遍历,就可以得到从小到大的排列顺序的线性序列。

代码

二叉树的结点,二叉树

这是表示二叉树的结点的结构体,BiTree是指向二叉树结点的指针,所以二叉树实际上是用二叉链表实现的哦,要知道本质

typedef struct BiTNode
{
	ElemType data;
	struct BiTNode * lchild, * rchild;
}BiTNode, *BiTree;

查找操作(递归)

/*在二叉查找树T中查找关键字key*/
/*f指向T的父亲,初次调用时其值是NULL,因为初次调用时T指向根节点,根节点没有父亲*/
/*T指向当前查找子树的根节点*/
/*如果找到了key,则p指向该元素所在结点的地址;否则,p指向查找路径上访问的最后一个结点*/
bool SearchBST(BiTree T, BiTree f, int key, BiTree * p)
{
	if (!T)//当前查找的子树为空,查找失败,返回
	{
		*p = f;//查找路径的最后一个结点是当前查找子树T的父亲
		return false;
	}
	if (key == T->data)//如果T指向的数据刚好就是关键字,查找成功
	{
		*p = T;
		return true;
	}
	if (key < T->data)//关键字小于T指向的数据,则递归查找T的左子树
		return SearchBST(T->lchild, T, key, p);
    if (key > T->data)
		return SearchBST(T->rchild, T, key, p);
}

插入操作(基于查找找到的位置)

基于查找,先找找是否已经存在,不存在才插入。

p是查找路径的最后一个结点,它决定了新结点的插入位置

/*不存在关键字Key,则插入并返回true;否则返回false*/
bool InsertBST(BiTree *T, int key)
{
	BiTree * p = NULL;//p是查找路径的最后一个结点,它决定了新结点的插入位置
	if (SearchBST(*T, NULL, key, p))//已经存在
		return false;
	//建立新结点
	BiTree s;
	s = (BiTree)malloc(sizeof(BiTNode));
	s->data = key;
	//把结点插入到正确位置
	if (*p)//p是空指针,则这棵树是空树,插入为根结点
		*T = s;
	else if (key < p->data)//插为p指向结点的左孩子
		p->lchild = s;
	else if (key > p->data)
		p->rchild = s;
	return true;
}

删除操作(难咯)

难在删除之后留下的二叉树,仍然是一棵二叉查找树。

删除叶子结点(最简单)

因为删除叶子并不会影响其他部分的结构
在这里插入图片描述

待删除结点只有左子树,或者只有右子树(较简单)

直接把左子树或右子树搬到待删除结点的位置,独子承父业
在这里插入图片描述

待删除结点既有左子树又有右子树(难):用待删除结点的直接前驱或直接后继替代它,可报二叉树其他部分的结构不变

在这里插入图片描述

选一个子树代替待删除结点,把另一棵子树的所有结点依次重新插入到二叉树中(不好)

如果待删除结点有两个孩子,那么我们最直观想到的办法就是:随便选一个子树来代替待删除结点,然后把另一棵子树的所有结点依次重新插入到二叉树中。
在这里插入图片描述
这么做,确实可以实现目的,但绝对不是什么好办法,因为另一棵子树的结点也许很多,更重要的是,这样做有可能会加深树的高度!!!这是我们绝对不想看到的后果,后面我们还要专门研究平衡二叉树,目的就是要让树的高度尽量小,让树尽量平衡,而不要出现左重右轻或者右重左轻的情况(最极端的是左斜树和右斜树)

用直接前驱或直接后继替代(好)

观察发现,用左子树的37,或者右子树的48直接替代待删除结点,可以保证二叉树其他部分结构不变,还是一棵二叉排序树,并且深度只可能降低或者不变,绝不对增高。
在这里插入图片描述
在这里插入图片描述在这里插入图片描述

其实,对二叉排序树进行中序遍历得到的线性序列中,左子树的37正是待删除结点的直接前驱,右子树的48正是待删除结点的直接后继。

在这里插入图片描述

需要注意的是,直接前驱也许不是叶子结点,而是有左子树的结点(一定没有右子树),所以拿他们替换待删除结点时,相当于要删除他们自己,所以要把左子树移动到直接前驱结点处。同样的,直接后继也可能有右子树,要拿右子树替换直接后继结点。

怎么找一个结点的直接前驱:从左子树根节点出发,一路向右直到尽头

那么怎么找一个结点的直接前驱呢?

肯定不需要把整棵树中序遍历一次。

找直接前驱:
由于直接前驱比自己小,在左子树中,但是它一定是左子树中最大的元素,所以从左子树的根节点出发,一路沿着右边往下找,直到尽头,就是直接前驱。

找直接后继:
同理,直接后继是右子树中最小的元素,所以从右子树的根结点出发,一路沿着左边走,直到尽头。

这也是下面的代码的关键。

删除操作的代码

综合三种情况

/*如果有key,则删除并返回true;否则返回false*/
bool DeleteBST(BiTree T, int key)
{
	BiTree * p = NULL;
	if (!SearchBST(T, NULL, key, p))//没找到
		return false;
	//如果找到了,则p一定不是空指针,且指向待删除结点
	DeleteNode(p);
}
bool DeleteNode(BiTree * p)
{
/*删除结点p*/
	//待删除结点是叶子,或者待删除结点只有一个子树
	BiTree s;
	if (!p->lchild)
	{
		s = *p;//存住待删除结点的地址
		*p = (*p)->rchild;
		free(s);
		return true;
	}
	if (!p->rchild)
	{
		s = *p;
		*p = (*p)->lchild;
		free(s);
		return true;
	}
	//两个子树均非空,则从左子树找待删除结点的直接前驱(或者从右子树找直接后继)
	BiTree q;
	if ((*p)->lchild && (*p)->rchild)
	{
		s = (*p)->lchild;
		q = *p;
		while (s->rchild)
		{
			q = s;
			s = s->rchild;
		}
		//此时s指向待删除结点的直接前驱,q是s的父亲
		(*p)->data = s->data;
		if (q==*p)//待删除结点的直接前驱的父亲就是待删除结点,则重接q的左子树
			q->lchild = s->lchild;
		else 
			q->rchild = s->lchild;//重接q的右子树
		free(s);
		return true;
	}	
}

其中第一个函数也可以这么写:

bool DeleteBST(BiTree T, int key)
{
	if (!T)
		return false;
	if (key == T->data)//查找成功
		return DeleteNode(T);
	if (key < T->data)
		return DeleteBST(T->lchild, key);
	if (key > T->data)
		return DeleteBST(T->rchild, key);
}

这样就和查找的代码几乎一样,就不调用查找函数,自己查找了。

二叉查找树的查找性能不稳定,引出平衡二叉树的设计需求

前面说了,二叉查找树中比较次数最少是1,最多是树的深度。听起来感觉好像还行,但是如果二叉树是一个极端的右斜树或者左斜树,这个最坏就坏到了极致,直接差到和顺序查找一样了!所以我们希望树不要走极端,尽量平衡一些,以减小树的深度,从而优化二叉查找树的查找效率。

二叉查找树的查找性能取决于二叉树的形状,可二叉树的形状不是确定的!所以查找性能也不稳定
在这里插入图片描述在这里插入图片描述
再举一个例子(图来自B站)

在这里插入图片描述
再比如(图来自B站)
在这里插入图片描述
可以看到,同样一组数据,用不同的顺序建树,得到的树的形状不同,查找效率也不同。所以我们当然要想办法,尽量去建出一个AVL更小的树,这样的树其实就是比较平衡的树,即下一篇博文的主角,平衡二叉树。
在这里插入图片描述

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值