C语言二叉树的实现

2018年01月02日 10:41 | 3164次浏览

二叉排序树简介 

    二叉排序树(Binary Sort Tree,简称BST),又称二叉查找树,是红黑树、AVL树等的基础。它或是一棵空树,或者是具有下列性质的一棵二叉树:

   1、若它的左子树不空,则左子树上所有节点的值均小于它的根节点的值;

   2、若它的右子树不空,则右子树上所有节点的值均大于它的根节点的值;

   3、它的左右子树也分别为二叉排序树。

   下面的一棵树即为二叉排序树:

很明显,对二叉排序树进行中序遍历,便可得到一个有序序列,该有序序列中的各元素按照从小到大的顺序排列,因此一个无序序列可以通过构造一棵二叉排序树而变成一个有序序列。


二叉排序树相关操作

    二叉排序树通常有查找、插入、删除等操作。查找操作很简单,无非就是递归查找,有点类似二叉树遍历的过程。插入操作也不难,一般是先在二叉排序树pTree中查找,看是否存在有等于给定值的元素,如果查找不到,则将给定值插入到该二叉排序树中,但是要保证插入后的树依然是二叉排序树。这样,新节点插入的位置便是唯一的,而且新插入的节点一定是一个新添加的叶子节点,并且是查找不成功时查找路径上访问的最后一个节点的左孩子或右孩子。正是由于其在查找过程中插入节点的特性,二叉排序树是一种动态树。


    在给出各操作实现的具体代码前,要详细看下二叉排序树的删除操作,删除操作相比于二叉排序树的其他操作要难些,但也只是相对于其本身的其他操作而已,真正理解了也就很容易了。闲话少说,下面就来具体分析二叉排序树的删除操作。


    我们假设在二叉排序树中要被删除的节点为p(即p指向的节点,下同),其父节点为f,当然节点p可能是节点f的的左孩子或右孩子,但在下面各种情况的分析中,你会发现,无论是左孩子还是右孩子,都不影响删除操作的通用性。很明显,删除操作要分为如下3种情况:


    1、若待删节点p为叶子节点,则删除p后,并不会破坏整棵树的结构,因此只需令p=NULL即可。


    2、若待删节点p只有左子树或右子树,则只需将左子树或右子树重接到p的父节点上即可,即执行如下操作:p=p->lchild或p=p->rchild。


    3、若待删节点p既有左子树又有右子树,显然就不如上面两种情况那么简单了。我们要使节点p被删除后,二叉排序树的结构不变,就需要对它的子树做一些操作,而且只需操作一个子树即可,操作左子树和操作右子树的思路相似,我们这里以操作左子树为例来实现节点p的删除操作,并结合下图做具体分析(图中三角形代表节点的左子树或右子树)。

我们这里将图a展开为更详细的图b进行分析,则在删除节点p前,中序遍历该二叉排序树的结果为:...CL C...QL Q SL S P PR F ...,删除节点p后,我们要保持其他元素在该序列中的先后顺序不变,观察图b,我们可以采取如下两种做法:


    1)将节点p的左子树直接接到其父节点f上,作为f的左子树,将节点p的右子树接到节点s上,作为s的右子树(这里s为p的前驱节点,即p在有序序列中紧接在s的前面),而后删除节点p。采用这种方法删除节点p后,得到的二叉排序树的形状如下图中的图c所示:

采取该方法删除节点的实现代码如下:

/*
采用第一种算法从二叉排序树中删除指针p所指向的节点,
并在保持二叉排序树有序的情况下,将其左右子树重接到该二叉排序树中.
该函数主要用来被后面的删除函数调用
*/
void delete_Node1(BSTree &p)
{ 
	BSTree q,s;
	if(!p->lchild)	
	{	//如果左子树为空,则只需重接其右子树
		//这里包含了左右子树均为空的情况
		q = p;
		p = p->rchild ;
		free(q);
	}
	else if(!p->rchild)
	{	//如果右子树为空,则只需重接其左子树
		q = p;
		p = p->lchild;
		free(q);
	}
	else
	{	//如果左右子树都不为空,我们采取第一种方法来重接左右子树,
		//我们这里采取修改左子树的方法,也可以修改右子树,方法类似
		s = p->lchild;		//取待删节点的左节点

		//一直向右,最终s为待删节点的前驱节点
		//如果将各节点元素按从小到大顺序排列成一个序列,
		//则某节点的前驱节点即为序列中该节点的前面一个节点
		while(s->rchild)
			s = s->rchild;
		s->rchild = p->rchild;	//将p的右子树接为s的右子树
		q = p;
		p = p->lchild;		//将p的左子树直接接到其父节点的左子树上
		free(q);
	}
}

2)将节点s的右子树重接到其父节点上,作为其父节点的右子树,而后用s替换掉带删节点p。采取这种方法删除节点p后,得到的二叉排序树的形状如上图中的图d所示。采用该方法删除节点的实现代码如下:

/*
采用第二种算法从二叉排序树中删除指针p所指向的节点,
并在保持二叉排序树有序的情况下,将其左右子树重接到该二叉排序树中.
该函数主要用来被后面的删除函数调用
*/
void delete_Node2(BSTree &p)
{
	BSTree q,s;		
	if(!p->lchild)	
	{	//如果左子树为空,则只需重接其右子树
		//这里包含了左右子树均为空的情况
		q = p;
		p = p->rchild ;
		free(q);
	}
	else if(!p->rchild)
	{	//如果右子树为空,则只需重接其左子树
		q = p;
		p = p->lchild;
		free(q);
	}
	else
	{	//如果左右子树都不为空,我们采取第二种方法来重接左右子树,
		//我们这里采取修改左子树的方法,也可以修改右子树,方法类似
		q = p;
		s = p->lchild;		//取待删节点的左节点
		while(s->rchild)		
		{	//一直向右,最终s为待删节点的前驱节点。
			//如果将各节点元素按从小到大顺序排列成一个序列,
			//则某节点的前驱节点即为序列中该节点的前面一个节点
			q = s;
			s = s->rchild;
		}
		//用s来替换待删节点p
		p->data = s->data;  
		//根据情况,将s的左子树重接到q上
		if(p != q)
			q->rchild = s->lchild;
		else
			q->lchild =s->lchild;
		free(s);
	}
}

完整源码

    上面重点分析了删除节点的思路和过程,下面给出二叉排序树各种操作实现的完整C代码(含测试代码并加有详细注释):

/*********************************
二叉排序树的相关操作实现

**********************************/
#include<stdio.h>
#include<stdlib.h>

typedef struct Node
{
	int data;
	struct Node *lchild;
	struct Node *rchild;
}NODE,*BSTree;


/*
在指针pTree所指的二叉排序树中递归查找关键字为key的元素,
若查找成功,则返回指向该元素节点的指针,否则返回NULL
*/
BSTree search(BSTree pTree,int key)
{
	if(!pTree || pTree->data == key)	//查找到时返回的pTree为该元素节点,没查找到时为NULL
		return pTree;
	else if(key < pTree->data)			//如果key小于当前节点的值,则在其左子树中递归查找
		return search(pTree->lchild,key);
	else								//如果key大于当前节点的值,则在其右子树中递归查找
		return search(pTree->rchild,key);
}


/*
在指针pTree所指的二叉排序树中递归查找关键字为key的元素,
若查找成功,则返回ture,并查找到的数据对应的节点指针保存在p中,
否则返回false,并将查找路径上访问的最后一个节点指针保存在p中。
这里的参数parent指向每次递归遍历的子树的根节点的父节点,即始终是参数pTree的父节点,
它的初始值为NULL,其目的是跟踪查找路径上访问的当前节点的父节点(即上一个访问节点)
该函数用来被后面的插入函数调用。
*/
bool search_BSTree(BSTree pTree,int key,BSTree parent,BSTree &p)
{
	if(!pTree)         //如果pTree为NULL,则查找不成功												
	{	//这里包含了树空,即pTree为NULL的情况
		p = parent;
		return false;
	}
	else             //否则,继续查找
	{								
		if(key == pTree->data)			//如果相等,则查找成功					
		{
			p = pTree;
			return true;
		}
		else if(key < pTree->data)		//在左子树中递归查找
			return search_BSTree(pTree->lchild,key,pTree,p);    
		else							//在右子树中递归查找
			return search_BSTree(pTree->rchild,key,pTree,p);
    }
}

/*
当在pTree所指向的二叉排序树中查找不到关键字为key的数据元素时,
将其插入该二叉排序树,并返回ture,否则返回false。
树空时插入会改变根节点的值,因此要传入引用。
*/
bool insert(BSTree &pTree,int key)
{
	BSTree p;
	if(!search_BSTree(pTree,key,NULL,p))		//如果查找失败,则执行插入操作
	{
		//为新节点分配空间,并对各域赋值
		BSTree pNew = (BSTree)malloc(sizeof(NODE));
		pNew->data = key;
		pNew->lchild = pNew->rchild = NULL;

		if(!p)						    //如果树空,则直接置pNew为根节点
			pTree = pNew;
		else if(key < p->data)			//作为左孩子插入p的左边
			p->lchild = pNew;	        //作为右孩子插入p的右边	
		else
			p->rchild = pNew;
		return true;
	}
	else
		return false;
}

/*
采用第一种算法从二叉排序树中删除指针p所指向的节点,
并在保持二叉排序树有序的情况下,将其左右子树重接到该二叉排序树中.
该函数主要用来被后面的删除函数调用
*/
void delete_Node1(BSTree &p)
{ 
	BSTree q,s;
	if(!p->lchild)	
	{	//如果左子树为空,则只需重接其右子树
		//这里包含了左右子树均为空的情况
		q = p;
		p = p->rchild ;
		free(q);
	}
	else if(!p->rchild)
	{	//如果右子树为空,则只需重接其左子树
		q = p;
		p = p->lchild;
		free(q);
	}
	else
	{	//如果左右子树都不为空,我们采取第一种方法来重接左右子树,
		//我们这里采取修改左子树的方法,也可以修改右子树,方法类似
		s = p->lchild;		//取待删节点的左节点

		//一直向右,最终s为待删节点的前驱节点
		//如果将各节点元素按从小到大顺序排列成一个序列,
		//则某节点的前驱节点即为序列中该节点的前面一个节点
		while(s->rchild)
			s = s->rchild;
		s->rchild = p->rchild;	//将p的右子树接为s的右子树
		q = p;
		p = p->lchild;		//将p的左子树直接接到其父节点的左子树上
		free(q);
	}
}

/*
采用第二种算法从二叉排序树中删除指针p所指向的节点,
并在保持二叉排序树有序的情况下,将其左右子树重接到该二叉排序树中.
该函数主要用来被后面的删除函数调用
*/
void delete_Node2(BSTree &p)
{
	BSTree q,s;		
	if(!p->lchild)	
	{	//如果左子树为空,则只需重接其右子树
		//这里包含了左右子树均为空的情况
		q = p;
		p = p->rchild ;
		free(q);
	}
	else if(!p->rchild)
	{	//如果右子树为空,则只需重接其左子树
		q = p;
		p = p->lchild;
		free(q);
	}
	else
	{	//如果左右子树都不为空,我们采取第二种方法来重接左右子树,
		//我们这里采取修改左子树的方法,也可以修改右子树,方法类似
		q = p;
		s = p->lchild;		//取待删节点的左节点
		while(s->rchild)		
		{	//一直向右,最终s为待删节点的前驱节点。
			//如果将各节点元素按从小到大顺序排列成一个序列,
			//则某节点的前驱节点即为序列中该节点的前面一个节点
			q = s;
			s = s->rchild;
		}
		//用s来替换待删节点p
		p->data = s->data;  
		//根据情况,将s的左子树重接到q上
		if(p != q)
			q->rchild = s->lchild;
		else
			q->lchild =s->lchild;
		free(s);
	}
}

/*
若pTree所指向的二叉排序树中查找到关键字为key的数据元素,
则删除该元素对应的节点,并返回true,否则返回false
如果要删除的恰好是根节点,则会改变根节点的值,因此要传入引用
*/
bool delete_BSTree(BSTree &pTree,int key)
{
	//不存在关键字为key的节点
	if(!pTree)
		return false;
	else
	{	
		if(key == pTree->data)       //查找到关键字为key的节点
		{
			delete_Node1(pTree);
//			delete_Node2(pTree);
			return true;			
		}
		else if(key < pTree->data)  //继续查找左子树
			return delete_BSTree(pTree->lchild,key);
		else                        //继续查找右子树
			return delete_BSTree(pTree->rchild,key);
	}
}

/*
根据所给的长为len的arr数组,按数组中元素的顺序构建一棵二叉排序树
*/
BSTree create_BSTree(int *arr,int len)
{
	BSTree pTree = NULL;
	int i;
	//按顺序逐个节点插入到二叉排序树中
	for(i=0;i<len;i++)
		insert(pTree,arr[i]);
	return pTree;	
}

/*
递归中序遍历二叉排序树,得到元素从小到大有序排列的序列
*/
void in_traverse(BSTree pTree)
{
	if(pTree)
	{
		if(pTree->lchild)
			in_traverse(pTree->lchild);
		printf("%d ",pTree->data);
		if(pTree->rchild)
			in_traverse(pTree->rchild);	
	}
}

/*
递归销毁二叉排序树
*/
void destroy_BSTree(BSTree pTree)
{
	if(pTree)
	{
		if(pTree->lchild)
			destroy_BSTree(pTree->lchild);
		if(pTree->rchild)
			destroy_BSTree(pTree->rchild);
		free(pTree);
		pTree = NULL;
	}
}

int main()
{
	int i;
	int num;
	printf("请输入节点个数:");
	scanf("%d",&num);

	//输入num个整数
	int *arr = (int *)malloc(num*sizeof(int));
	printf("请依次输入这%d个整数(必须互不相等):",num);
	for(i=0;i<num;i++)
		scanf("%d",arr+i);

	//中序遍历该二叉排序树,使数据按照从小到大的顺序输出
	BSTree pTree = create_BSTree(arr,num);
	printf("中序遍历该二叉排序树的结果:");
	in_traverse(pTree);
	printf("\n");

	//查找给定的整数
	int key;
	printf("请输入要查找的整数:");
	scanf("%d",&key);
	if(search(pTree,key))
		printf("查找成功\n");
	else 
		printf("查找不到该整数\n");

	//插入给定的整数
	printf("请输入要插入的整数:");
	scanf("%d",&key);
	if(insert(pTree,key))
	{
		printf("插入成功,插入后的中序遍历结果:");
		in_traverse(pTree);
		printf("\n");
	}
	else
		printf("插入失败,该二叉排序树中已经存在整数%d\n",key);

	//删除给定的整数
	printf("请输入要删除的整数:");
	scanf("%d",&key);
	if(delete_BSTree(pTree,key))
	{
		printf("删除成功,插入后的中序遍历结果:");
		in_traverse(pTree);
		printf("\n");
	}
	else
		printf("删除失败,该二叉排序树中不存在整数%d\n",key);

	return 0;
}

测试结果如下:


小说《我是全球混乱的源头》

感觉本站内容不错,读后有收获?小额赞助,鼓励网站分享出更好的教程