动态查找之二叉排序树


之前讲过二分、插值和斐波那契查找方式,这三种查找算法是针对有序线性表的,有序表的插入和删除操作比较复杂,它需要移动插入和删除元素之后的所有元素,所以在查找时通常不进行数据的插入和删除,也就是说表的数据不会发生变化,我们称之为 静态查找表。那有没有即可以高效的查找又可以高效的插入和删除呢?有,二叉排序树就可以实现。下面介绍一下二叉排序树。

简介

二叉排序树又称二叉查找树,它若不为空则具有如下性质:

  • 若左子树不为空,则左子树上所有节点的值均小于它根节点的值;
  • 若右子树不为空,则右子树上所有节点的值均大于它根节点的值;
  • 它左、右子树也分别为二叉排序树。
    比如我们现在有集合{62,88,58,47,35,73,51,99,37,93},试着将它构建成二叉排序树。我们把集合看成数组,从下标由小到大逐渐插入树,首先将第一个数字62定为根节点,然后第二个数字88大于62,将它定为62的右子树,然后58小于62,将它定为62的左子树。
    在这里插入图片描述
    然后下一个数字47小于58,是58的左子树,然后再下一个数字…,以此类推构建如下图二叉树。对其进行中序遍历就可得到一个有序序列{35,37,47,51,58,62,73,88,93,99}。
    在这里插入图片描述
    由上述定义可知,二叉排序树首先是一棵二叉树,采用递归方式定义,节点间满足一定次序关系。二叉排序树的目的不是为了排序,而是为了提高查找、插入和删除效率,因为无论如何,在一个有序集合中查找其效率总是高于无序集合,而且二叉排序树这种结构也有利于插入和删除操作。下面我们实现以下二叉排序树的查找、插入和删除操作。

二叉排序树的插入操作

二叉排序树的节点和普通二叉树一样。

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

根据上述分析得插入操作的代码如下:

void InsertBST(BiTree &T, int key)
{
	if (!T)
	{
		T = (BiTree)new BiTNode;
		T->data = key;
		T->lchild = T->rchild = NULL;
		return;
	}
	else
	{
		BiTree s = T;
		while (1)
		{
			if (s->data == key)
				return;
			else if (s->data > key)
			{
				if (s->lchild == NULL)
				{
					s->lchild = (BiTree)new BiTNode;
					s->lchild->data = key;
					s->lchild->lchild = s->lchild->rchild = NULL;
					return;
				}
				else
					s = s->lchild;
			}
			else
			{
				if (s->rchild == NULL)
				{
					s->rchild = (BiTree)new BiTNode;
					s->rchild->data = key;
					s->rchild->lchild = s->rchild->rchild = NULL;
					return;
				}
				else
					s = s->rchild;;
			}
		}
	}
}

我们通过逐个插入建立集合为{62,88,58,47,35,73,51,99,37,93}的二叉排序树,然后通过中序遍历方式升序打印集合,以下是测试代码及输出。

int main()
{
	int i;
	int a[] = { 62,88,58,47,35,73,51,99,37,93 };
	BiTree T = NULL;
	for (i = 0; i < 10; i++)
		InsertBST(T, a[i]);
	InOrderTraverse(T);//中序遍历
	
	getchar();
}
35 37 47 51 58 62 73 88 93 99

二叉排序树的查找操作

BiTree SearchBST(BiTree T, int key)
{
	if (!T)
		return nullptr;
	while (T)
	{
		if (T->data == key)
			return T;
		else if (T->data > key)
			T = T->lchild;
		else
			T = T->rchild;
	}
	return T;
}

查找函数若查找成功返回所查找节点地址,否则返回空。
我们通过查找函数查找93,调用函数SearchBST(T, 93),我们看一下调用过程。

1.首先T指向根节点,节点不为空进入while循坏,while循坏条件为T不为空;
2.第一次循环,节点值为62小于93,执行T = T->rchild,T指向88;
在这里插入图片描述
3.第二次循环,节点值为88小于93,执行T = T->rchild,T指向99;
在这里插入图片描述
4.第三次循环,节点值为99大于93,执行T = T->lchild,T指向93;
在这里插入图片描述
5.第四次循环,节点值为93,返回节点地址。

二叉排序树的删除操作

二叉排序树的插入和查找操作比较简单也易于实现,但所谓“请神容易送神难”,二叉排序树的删除操作就不太容易了,因为删除任意节点后树的结构和特性不能改变,所以我们需要考虑多种情况。

1.如果删除节点37,51,73,93,那是很容易的,因为这些节点为叶子节点,我们可以直接删除不用做任何其他修改。如下图所示。
在这里插入图片描述
2.如果删除的节点只有左子树或右子树也比较容易,只需将其左子树或右子树整个移动到删除节点的位置即可,比如删除节点35、99、58。
在这里插入图片描述
3.如果删除的节点既有左子树又有右子树怎么办呢?一个办法是我们先将左子树移动至删除节点,然后将右子树的所有节点逐个插入,但这个做法效率不高并可能会增加高度,所以不是一个好的做法。
比较好的做法是我们从树中找出一个节点s替换删除节点p,然后删除节点s。比如我们删除下列二叉排序树的节点47。
在这里插入图片描述
那我们应该用哪个节点替换47呢,答案是37和48,为什么是这两个数呢?我们将该集合进行中序遍历得到其升序序列{29,35,36,37,47,48,49,50,51,56,58,62,73,88,93,99},发现37和48是47的直接前驱和直接后继,这两个数是47节点的左子树的最右端和右子树的最左端,并且37和48两个节点要么是叶子节点要么只有一个子树,我们只需进行一次替换操作和一次删除操作。
代码如下:

Status Delete(BiTree &p)
{
	BiTree q, s;
	if (p->rchild == NULL)
	{
		q = p;
		p = p->lchild;
		free(q);
	}
	else if (p->lchild == NULL)
	{
		q = p;
		p = p->rchild;
		free(q);
	}
	else
	{
		q = p; s = p->lchild;
		while (s->rchild)
		{
			q = s; s = s->rchild;
		}
		p->data = s->data;
		if (q != p)
			q->rchild = s->lchild;
		else
			q->lchild = s->lchild;
		free(s);
	}
	return OK;
}

Status DeleteBST(BiTree &T, int key)
{
	if (!T)
		return ERROR;
	else
	{
		if (key == T->data)
			return Delete(T);
		else if (key < T->data)
			return DeleteBST(T->lchild, key);
		else
			return DeleteBST(T->rchild, key);
	}
}

删除节点47的调用过程如下:
1.Delete函数的前两个if和else if语句处理删除节点为叶子节点或只有左子树或右子树的情况。最后的else语句处理删除节点既有左子树也有右子树的情况。
2.将要删除的节点p赋给临时变量q,再将p的左孩子赋给临时变量s,此时q指向节点47,s指向节点35
在这里插入图片描述
3.循环使s指向节点35子树的最右端节点37,q指向s的双亲35。
在这里插入图片描述
4.将p节点值替换为s节点值37。
在这里插入图片描述
5.判断p和q指向不同,将s->lchild赋给q->rchild,否则将s->lchild赋给q->lchild。
在这里插入图片描述
6.删除节点s。
在这里插入图片描述

性能分析

二叉排序树采用链式存储结构,继承了链式存储结构便于插入和删除的优点,所以通常用于在查找时进行插入和删除操作,我们称为动态查找表。对于查找操作,最好的情况是遍历1次,即根节点就是查找节点。最坏是遍历树的深度,所以二叉排序树的查找性能取决于树的结构,但二叉排序树的结构是不确定的。例如集合{62,88,58,47,35,73,51,99,37,93},其二叉排序树的结构如下图所示。
在这里插入图片描述
如果集合是升序的如{35,37,47,51,58,62,73,88,93,99},其结构如下图。
在这里插入图片描述
在这两个图中查找99,一个需要两次,一个需要10次,可知如果二叉排序树是平衡的查找的时间复杂度为O(logn),近于二分查找,最坏情况为O(n)。所以我们构建二叉排序树时最好把它构建成平衡的二叉排序树。我们将在后续文章中介绍平衡二叉树。

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值