c语言(二叉查找树代码分析)

知识提要:

1.随机访问:对数组而言,可以使用数组下标直接访问该数组中的任意元素,这叫做随机访问.
2.顺序访问:对链表而言,必须从链表首节点开始,逐个节点移动到要访问的节点,这叫做顺序访问.
3.顺序查找:从列表的开头按顺序查找.
4.二分查找:(1)规定待查找的项为目标项;(2)假设列表中的各项按字母排序,比较列表中的中间项和目标项;(3)如果相等,则查找结束;如果目标项在列表中,中间项在目标项前面,则目标项一定在列表的后半部分…(4)再取后半部分的中间项,一直按这种思路取下去…
5.数组很好完成这种二分查找,但是链表不容易完成-------所以诞生了一种既支持频繁插入和删除项(链表轻松胜任)又支持频繁查找的(数组轻松胜任)数据形式------二叉查找树.
这个图辅佐代码分析
以C Prime Plus 第17章的代码------宠物店分析举例:
下面进行代码分析:(只分析二叉查找树的定义代码(不分析声明代码和具体实现代码))

一.数据结构的定义:

1.项的建立

/*---项的建立*/
#define SLEN 20//成员中的数组的长度

typedef struct item
{
	char petname[SLEN];
	char petkind[SLEN];
}Item;

2.节点的建立

/*---节点的建立*/
typedef struct trnode
{
	Item item;//项
	struct trnode* left;//左节点指针
	struct trnode* right;//右节点指针
}Trnode;

3.树的建立

/*---树的建立*/
typedef struct tree
{
	Trnode * root;//树指针
	int size;//树中节点数
}Tree;

二.二叉树函数的一些定义

1.几个简单的"属性类(反映一些树的信息)"函数

void InitializeTree(Tree* ptree)
{
	ptree->root = NULL;
	ptree->size = 0;
}
bool TreeIsEmpty(const Tree* ptree)
{
	if (ptree->size == 0)
		return true;
	else
		return false;
}
bool TreeIsFull(const Tree* ptree)
{
	if (ptree->size == MAXITEMS)
		return true;
	else
		return false;
}
int TreeItemCount(const Tree* ptree)
{
	return ptree->size;
}

(1)初始化:让指向树的指针(ptree->root)指向null,树中的项(ptree->size)为0;
(2)判断------树是否是空,满和树中的节点数:通过判断树中的项数(ptree->size)的情况判断.

2.复杂一点的"属性类"操作------判断具体的项是否在数中

bool InTree(const Item* pitem, const Tree* ptree)
{
	/*需要进行寻找--1.需要定义函数实现---以上*/

	return (SeekItem(pitem, ptree)).chlid == NULL ? false : true;//.chlid就是表示没找到  找到的话这个值不会为null(两种情况 1.还没开始找(就不存在树) 2.找了没找到(树里面没有))
}

这里用到了一个辅助函数SeekItem(pitem, ptree)

static Pair SeekItem(const Item* pitem, const Tree* ptree)
{
	//声明变量---pair结构
	Pair look;

	//初始化变量
	look.chlid = ptree->root;//从顶点开始寻找
	look.parent = NULL;

	//如果没有对应的树---开始让chlid节点指向树的根节点---没有顶点
	if (look.chlid == NULL)
		return look;//终止函数  提前返回

	//如果有对应的树---直到指针为空
	while (look.chlid != NULL)
	{
		/*向左边寻找*/
		if (ToLeft(pitem, &look.chlid->item))//带寻找项与树中的项相比  如果在树中项顺序的后边
		{
			//向左蔓延
			look.parent = look.chlid;
			look.chlid = look.chlid->left;
		}
		else if (ToRight(pitem, &look.chlid->item))
		{
			//向右蔓延
			look.parent = look.chlid;
			look.chlid = look.chlid->right;
		}
		else
			break;// 找到了 不用向下探寻了(没有重复项)  当前子节点就是找到的节点
	}

	return look;

}

这个辅助函数中定义了一种辅助工具(Pair look)来帮忙查找

typedef struct  pair
{
	Trnode* parent;//记录作用
	Trnode* chlid;//子节点为探照节点 与树中的节点进行信息比对
}Pair;

这个数据中由两个指向节点的指针构成.用来查找和记录位置使用

这个辅助函数中用到了其他的辅助函数ToLeft,ToRight

static bool ToLeft(const Item*item1, const Item*item2)
{
	//定义strcmp接受变量
	int cmp1, cmp2;
	//比较两个指针指向的字符串  ---如果第一个字符串在前面  就返回true   (strcmp函数返回<0)
	//---宠物名的比较(先比宠物名)
	if ((cmp1 = strcmp(item1->petname, item2->petname)) < 0)
		return true;
	//---宠物种类的比较(宠物名相同就比宠物种类)
	else if ((cmp1 == 0) && ((cmp1 = strcmp(item1->petkind, item2->petkind)) < 0))
		return true;
	else
		return false;

}

static bool ToRight(const Item*item1, const Item*item2)
{
	//定义strcmp接受变量
	int cmp1, cmp2;
	//比较两个指针指向的字符串  ---如果第一个字符串在前面  就返回true   (strcmp函数返回<0)
	//---宠物名的比较(先比宠物名)
	if ((cmp1 = strcmp(item1->petname, item2->petname)) > 0)
		return true;
	//---宠物种类的比较(宠物名相同就比宠物种类)
	else if ((cmp1 == 0) && ((cmp1 = strcmp(item1->petkind, item2->petkind)) > 0))
		return true;
	else
		return false;
}

这里只分析ToLeft函数,(ToRight函数同理)
这个函数比较比较项1和项2中的内容字母大小的排序(名字:strcmp(item1->petname, item2->petname))(名字相同就比种类(cmp1 = strcmp(item1->petkind, item2->petkind)) < 0)):结果是如果第一项小就返回true…
回到寻找节点函数SeekItem:现在分析这个函数的流程:

1.声明查找结构并进行初始化

2.判断是否有查找的主体(也就是确定树存不存在)

3.如果树存在就按照规定的方向查找(ToLeft,ToRight),这个就是二分查找的精髓

(1)将要查找的项先从根节点开始查找不停地用二分法查找
(2)每进行一个比对就进行查找数据的更新(父指针指向子指针,子指针指向子指针的查找方向)
(3)直到找到目标项或者找遍了所有节点.------找到了会返回查找数据的(.chlid就是指向目标节点)
在回到InTree函数:如果找到了(查找数据的.chlid成员变量不为空)就会返回true.

3.添加项函数

bool AddItem(const Item* pitem, Tree* ptree)
{
	//创建一个新的节点(添加项是通过添加节点实现)---这里并不是创建 而是提出一个概念(因为不一定能创建  看有没有违反要求) 真正的创建要进行分配空间和初始化  3.创建节点函数实现
	Trnode * new_node;
	//判断树的情况---满了是不能添加节点的
	if (TreeIsFull(ptree))
	{
		fprintf(stderr, "树是满的  不能添加项了 你的明白???\n");
		return false;
	}
	//如果要添加的项已经存在  就不能进行添加----不能有重复项
	if (SeekItem(pitem, ptree).chlid != NULL)
	{
		fprintf(stderr, "你添加的项在树中已经存在 莫添加了 ok?\n");
		return false;
	}
	//节点的正式创建---分配内存  初始化
	new_node = MakeNode(pitem);
	//如果创建失败---分配空间失败
	if (new_node == NULL)
	{
		fprintf(stderr, "哦豁,创建节点失败~\n");
		return false;
	}
	//成功创建节点

	//---树中的节点树增加
	ptree->size++;
	//---如果树是空的---------------------不能用TreeIsEmpty判断   因为这个函数是通过判断size来判断的  而现在的情况是 先初始化了size
	if (ptree->root==NULL)
		//------树的根节点就是这个新节点
		ptree->root = new_node;
	else//---如果树不是空的
	{
		//------执行添加节点函数-----创建4.添加节点函数
		AddNode(new_node, ptree->root);
	}
}

添加项目:

(1) 创建指向节点的指针

(2)如果创建成功(树中的节点没满),判断这个项是否存在(SeekItem)

(3)这个项如果不存在就进行初始化节点(MakeNode(pitem)😉----用到了辅助函数

static Trnode* MakeNode(const Item* pitem)
{
	//创建一个节点
	Trnode* new_node;
	//分配空间
	new_node = (Trnode*)malloc(sizeof(Trnode));
	//成员变量的初始化
	if (new_node != NULL)
	{
		new_node->item = *pitem;
		new_node->left = NULL;
		new_node->right = NULL;
	}
	return new_node;
}


(4)成功创建节点后,判断节点的位置(作为根节点还是子节点),如果是根节点(就让树的根节点指向这个新节点),如果是子节点则执行子节点操作函数-------辅助函数添加子节点

static void AddNode(Trnode* new_node, Trnode* root)
{
	/*要把新节点中的内容与树中节点进行比较后才能插入*/

	//到达一个节点时 如果新节点的内容比树节点的内容小
	if (ToLeft(&new_node->item, &root->item))
	{
		//--如果这个树节点的左侧是空的
		if (root->left == NULL)
		{
			//---就把新节点添加到树节点的左侧
			root->left = new_node;
		}

		else 
		{
			//--左侧不是空的
			//---继续调用这个函数寻找---这次跟树节点的左边指向的节点比
			AddNode(new_node, root->left);
		}
		
	}
	else if(ToRight(&new_node->item,&root->item))//如果发现这个新节点的内容比这个节点下面所有的节点内容都大
	{
		
		//--如果右侧是空的
		if (root->right == NULL)
		{
			//就把这个新节点放在此位置
			root->right = new_node;
		}
		else//--如果不是空的
		{
			//就调用这个函数继续寻找
			AddNode(new_node, root->right);
		}
	}
	else//如果发现新节点的内容不大不小  ---就是内容重复--输出错误
	{
		fprintf(stderr, "你添加的东西重复了 懂?\n");
		exit(EXIT_FAILURE);
	}


}

这个辅助函数使用了递归来添加,具体流程是:

(1)将要添加的节点的项的内容与树中节点的项的内容做对比(插入也要根据二分法插入)

(2)如果比树中的当前节点小

1.如果当前节点左边是空的(root->left == NULL)那么把要添加的项放入左边
2.如果不是空的,那么将对比的内容换成--------需要添加的项与此节点左指针指向的节点进行比较

(3)如果找不到就进行右边的寻找.

4.删除项函数

bool DeleteItem(const Item* pitem, Tree* ptree)
{
	/*在树中寻找到了目标项才能进行删除*/
	Pair look;


	//创建寻找体
	//执行寻找函数 将结果反馈到寻找体中
	look = SeekItem(pitem, ptree);
	//如果没有树(寻找体子成员为null)
	if (look.chlid == NULL)
		return false;
	//如果有树  找到了对应项---如果是根这个点
	if (look.parent == NULL)
	{
		//---删除根节点-----5.创建删除节点函数
		DeletNode(&ptree->root);
	}
	//如果是子节点
	else if (look.parent->left == look.chlid)//--左节点
		DeletNode(&look.parent->left);
	else
		DeletNode(&look.parent->right);//---右节点

	ptree->size--;
	return true;
}

流程是:先查找你要删除的项所在的节点,确认后执行删除节点函数.-------删除节点函数是个辅助函数

static void DeletNode(Trnode** ptree)
{
	//创建节点指针----要删除就是释放内存 释放的就是内容所对应的地址
	Trnode * ptemp;
	//如果要删除的节点左指针是空的  右指针不是空的---让它的右指针来接上
	if ((*ptree)->left == NULL)
	{
		//先把这个指针保存下来
		ptemp = *ptree;
		//让这个指针的右指针替换这个指针
		*ptree = (*ptree)->right;
		//释放替死鬼
		free(ptemp);
	}
	else if((*ptree)->right == NULL)//如果要删除的节点右指针是空的   左指针不是空的---让它的左指针来接上
	{
		ptemp = *ptree;
		*ptree = (*ptree)->left;
		free(ptemp);
	}
	else
	{
		/*如果要删除的节点  左右指针都不是空的 ---左指针接上  右指针去找寻离右指针最近的左指针指向节点的子节点的null  接上*/
		
		//寻找右节点能接上的位置----这里替死鬼先当了一回查找员  查找到符合条件的节点  自己复制它  方便右节点接上
		for (ptemp = (*ptree)->left;ptemp->right!=NULL;ptemp = ptemp->right)
		{
			continue;
		}
		//找到这个节点后---删除节点的右节点接到这个节点上(ptemp就是找到的节点 它的右节点是null)
		ptemp->right = (*ptree)->right;
		

		//让替死鬼指向要删除的节点
		ptemp = *ptree;

		//---本来就接上自己的左节点----要记住要先让替死鬼记录要删除节点的信息  在去让要删除节点的左指针上位
		*ptree = (*ptree)->left;


		//---释放替死鬼
		free(ptemp);

	}
}

删除节点函数的流程是:-----------------这里操作的是节点的地址(Trnode ptree)而不是直接操作节点----因为删除节点就是通过释放节点所在的地址来进行删除的**

(1)创建一个指向节点的指针(通过释放这个指针来释放内容)

(2)对要删除的节点进行判断:
1.如果这个节点的左边是空的((*ptree)->left == NULL),右边不是空的---------那么

  • 先将这个要删除的节点保存(ptemp = *ptree;)
  • 再将此节点的右指针指向的节点接到这个指针上(*ptree = (*ptree)->right;)新,这一步是保存信息
  • 现在可以安全的释放这个临时工指针来达到删除的目的(free(ptemp)😉

2.如果这个节点是右边空的,左边不是空的-----原理同上
3.如果这个节点左边,右边都不是空的--------这种情况,节点左边的节点树直接接上,右边的节点要接到左边节点树中(依次向下寻找过程中)那种节点的右指针为空的节点上,所以第一步要找出这种节点
for (ptemp = (*ptree)->left;ptemp->right!=NULL;ptemp = ptemp->right)-------这里这个临时节点充当符合条件的节点;然后让要删除节点的右边((*ptree)->right;)指向这个临时节点的右边(ptemp->right),从而完成对接成功.
4.最后让临时工重操旧业,让他去指向要删除的节点,通过释放临时工来进行删除操作.然后项数减1

5.用函数作用节点

void Traverse(const Tree* ptree, const void(*pfun)(Item item))
{
	//如果树存在
	if(ptree->root!=NULL)
	{
		//---按顺序作用每一个节点----创建按顺序作用的函数
		InOrder(ptree->root, pfun);
	}
}

函数作用也会按照顺序作用的,所以用到了一个辅助函数:InOrder

static void InOrder(const Trnode* ptree, const void(*pfun)(Item item))
{
	//如果树节点不为空
	if (ptree != NULL)
	{
		InOrder(ptree->left, pfun);//1.左边被作用2.改变节点---原节点变为原节点的左边
		(*pfun)(ptree->item);//这个节点的项被作用
		InOrder(ptree->right, pfun);//1.右边被作用 2.改变节点---原节点变为原节点的右边
	}
	/*相当于一只寻找这个节点的左边部分的节点 知道找到了那个节点左边指向为空的地方----------------注意递归的边界
	然后用函数作用这个节点的项
	然后去找这个节点(不是原节点)它右边的部分
	同样把这个节点看做新的节点  然后还是从左边找*/
}

这个函数也是通过递归来实现--------先作用它的左边,然后作用它的项,最后作用它的右边-------这个顺序是从最下面的节点开始作用(递归的特点).

6.最后是删除函数------删除所有节点

void DeleteAll(Tree* ptree)
{
	//如果树不是空的  就删除所有节点
	if (ptree != NULL)
		DeletAllNote(ptree->root);//------------------6.创建删除所有节点的函数
	//---树指针指向空
	ptree->root = NULL;
	//---项数为0
	ptree->size = 0;
}

这里使用了辅助函数DeletAllNote-----删除所有节点

void DeletAllNote(Trnode* pnode)
{
	//创建节点---替死鬼
	Trnode * pchange;
	//如果要删除的节点不是空的---删除分两步  一步保存  二步删保存的
	if (pnode != NULL)
	{
		//---根节点的右指针 指向替死鬼---先保存了右边---并没有删除
		pchange = pnode->right;
		//删除这个节点的左边---递归自己----这样就删了一半----实质上是删了所有节点的左节点(当然是从最底层开始) 通过释放内存 删除了右节点
		DeletAllNote(pnode->left);
		//--释放这个节点的内存
		free(pnode);
		//最后调用函数清除替死鬼
		DeletAllNote(pchange);//这里就是清除了另一半
	}


}

这里也是利用递归来实现--------递归这东西在二叉树中真的很常用(但也可以不用递归来实现)

具体操作:
1.设置"临时工"-------Trnode * pchange;通过释放它指向的节点来删除节点里的信息
2.进行递归:
(1)确定递归停止条件:树中节点为空的时候停止(pnode != NULL)

(2)让临时工保存信息(这里保存了右边的信息pchange = pnode->right;)(删除也是通过二分法来进行的,但是删除节点会删除节点的全部信息,这样会使得这个节点与其他节点的关系断裂,从而其他节点得不到操作),删除是一半一半的删.

(3)确定操作方向(pnode->left)-----删除节点的左边的东西(即使本来就为空,递归从最后面开始操作)

(4)释放这个节点的内容(free(pnode)😉

这里有些复杂(刚接触递归其实到现在都没整明白):我尽量表述的清楚一些:其实这个函数做了两件事:保存信息和释放内存
它先把原始点的右边部分保存起来,然后向这个点的左边开始寻找
而且边寻找边保存当前节点的右边部分的信息,直到找到一个节点(这个节点的左指针是空)
然后释放这个节点的地址(就是删除了这个节点)
然后又从当前(删掉的)节点的右指针指向的节点开始经过循环…
这样就相当于从第一项删除到了最后一项

没有做动画 将就着看吧
可以发现一个规律:每次对树进行操作,在项的处理封装一层(在最外面),在节点的处理封装一层(在里面),统筹性的工作比如删除树,会影响树的信息(树包括节点和节点数-------节点包括项和指针-------项就只包括基本数据,但是最直观的确实操作项)

  • 2
    点赞
  • 12
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 1
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

bussyman

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

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

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

打赏作者

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

抵扣说明:

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

余额充值