二叉排序树的C实现(以排序查找为主线,顺带介绍二叉树的实现)(面向新手,大佬勿喷)

本文深入探讨二叉树的实现与操作,包括构建、查找、遍历及删除等关键算法,重点讲解二叉排序树的特性及其在数据排序与查找中的应用。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

非线性结构

前面我已经写过线性结构,今天我们谈一谈非线性结构。

二叉树

首先我们第一个要知道的结构就是二叉树,但是对于一般的最抽象的二叉树你可能会有些疑惑,我们直接从一个应用角度出发,来探讨二叉树的各种性质。

首先,二叉树是一种非线性结构,它和单纯的数组和链表不同,它有更加花里胡哨的实现方式。

对于数据结构本身的一些概念性的东西我不想做过多的赘述,我只和大家讨论两件事情。
第一件事,二叉树是一种所谓以是非判断为主体的一种逻辑结构,一般来讲是代表向右寻找,否代表向左寻找,或者反过来,“二”的真正的意义就在于二值逻辑:true和false
第二件事,二叉树是递归定义,任何一个结点单拎出来都可以叫一个二叉树,成为子树,这为递归创造了良好的条件,下面我会从递归和非递归两种角度去分析一个算法。

首先是二叉树的实现,二叉树也是用结构体实现的。

#include<stdio.h>
typedef struct BitreeNode
{
	int data;
	struct BitreeNode* left;
	struct BitreeNode* right;//我比较懒,不喜欢写孩子
	struct BitreeNode* parent;//一些涉及所谓缝合的操作需要这个指针
}bnode;

bnode* root = NULL;//根节点,这是一棵树的根基所在
int main()
{
	
	return 0;
}

这是一棵树的定义,下面,我们直奔主题,从排序的角度来思考如何构建一个二叉树。顺便体会一下什么叫做二值逻辑。

对于一棵树,我们首先要做的是构建这棵树,下面就有一个问题:我们为什么要使用二叉树,单纯的想体验一下上下祖宗十八代的恢弘?当然没那么闲的dan疼,思考一下,对于之前数组的排序与查找操作,我们是不是借了一把随机存储的光?(没看快排和二分查找的可以补一补),让我们的算法复杂度降到了nlogn(排序)logn(查找)?但是这种好处只是因为我们可以随机寻址,可以通过下标就可以直接访问地址,但是链表呢?显然就不具备这个功能,但是我们往往还需要一种链式结构来实现高效的排序操作,怎么办?第一种办法就是使用二叉树来实现。(也有别的办法,以后我要是会了就给你们讲嗷)

二叉树在寻找一个值的时候,我们可以通过树的特殊结构来判断他的位置,比如,如果,我规定一个以n为根的树,他的左子树上所有的结点都必须小于n,而右边所有的数都大于等于n,这样,我们就可以直接通过这种关系,查找到n这个结点时,如果我想找的比n小,那就向左走,如果比n大,那就向右走,这就相当于做了一次二分,就比挨个找要快的多。
这种数据结构叫做二叉排序树,我直接从这种思想来思考这种数据结构以便于对这种数据结构具体化而非研究抽象的一个数学观念。

这就要求我们在最开始构建二叉树的时候就实现这样的逻辑。
下面来看非递归实现(insert函数递归会有很多麻烦,非递归实现比较靠谱)

void insert(int value)//向以root为根的树中插入一个结点value
//首先,root必须是全局变量,如果是局部变量,这里应该填一个参数,而且是传指针,具体操作不详述
{
	if(root == NULL)//一个很客观的问题,第一个怎么插进去?
	{
		root = (bnode*)malloc(sizeof(bnode));
		root->data = value;//初始化,如果说置空是一种习惯的话,这里置空是一种必须,而且注意如果使用parent的话记得加上parent
		root->left = NULL;
		root->right = NULL;
		root->parent = NULL;
	}
	else//后来的怎么插?
	{
		bnode* ptr = root;//整一个指针往后找空位置
		while(1)//无休止的循环,直到给value插进去为止,后边break
		{
			if (value < ptr->data)//小,往左转
				if (ptr->left == NULL)//找到空位了,插进去
				{
					ptr->left = (bnode*)malloc(sizeof(bnode));
					ptr->left->data = value;
					ptr->left->left = NULL;
					ptr->left->right = NULL;//还是初始化
					ptr->left->parent = ptr;
					break;
				}
				else
					ptr = ptr->left;//没找到接着找
			else// 不小于,往右转
				if (ptr->right == NULL)//给上边的拷贝改一改就完了
				{
					ptr->right = (bnode*)malloc(sizeof(bnode));
					ptr->right->data = value;
					ptr->right->left = NULL;
					ptr->right->right = NULL;//还是初始化
					ptr->right->parent = ptr;
					break;
				}
				else
					ptr = ptr->right;//没找到接着找	
		}
	}
}

代码很长,不过还比较好懂,递归算法你可以自己试一试。注意二重指针的问题

如果一直都是使用这种算法实现的一个一个插入,我们只需要按着这种规则再进行查找,就可以找到我想找到的结点
比如我写一个查找函数。

int BinarySearch(bnode* ptr, int value)//这里不存在传指针的问题,找到返回1,否则返回0
{
	if(ptr == NULL)//走死胡同了,山穷水尽了
		return 0;
	else if(value == ptr->data)//找到了,柳暗花明了
		return 1;
	else if(value < ptr->data)//没找到,但是还有希望
		return BinarySearch(ptr->left, value);
	else
		return BinarySearch(ptr->right, value);
}

当然我一直是不主张递归实现的算法的,可以尝试换成递推

int BinarySearch(int value)
{
	bnode* ptr = root;
	while(ptr != NULL)//要是还没走到山穷水尽,就放手一搏
	{
		if(value == ptr->data)//柳暗花明
			return 1;
		else if(value < ptr->data)//再接再厉
			ptr = ptr->left;
		else
			ptr = ptr->right;
	}
	return 0;//跳出循环了,没法子了
}

好像更简单了呢。。能用递推还是尽量使用递推吧。

然后就有一个问题,二叉树既然能按照这种规律查找,说明一个问题,他一定是有序的,如果知道set的同学一定想知道它为什么能自排序?他一定有一种办法可以将一串数排序,我们想看看一串数输进二叉树之后排好序的状态怎么办呢?

要想看这里面的顺序,无非是要遍历整个二叉树,但是怎么遍历就成了大学问,下面我们来设想,对于一个结点来说,所有比他小的结点都在它左边,说明他左边所有的结点都应该先于它输出,所以,程序最起码左孩子要先于它输出吧,同理,它的右子树的所有结点都要后于它输出,最起码是右孩子后于它输出。所以,我们有了一个基本的顺序,对于每一个结点而言,都要等待它左子树上所有的结点都输出完才能输出他自己,然后再输出它的右子树,这就叫所谓的中序遍历。
实现起来非常的银杏。递归的话很方便

void print(bnode* ptr)
{
	if (ptr->left)
		print(ptr->left);
	printf("%d ", ptr->data);
	if (ptr->right)
		print(ptr->right);
}

你也可以试一试递推实现,递归化递推需要用到栈的结构

这样我们就可以看到排好序的一棵树,当然,我们还有其他遍历方式,不过遍历之后看不到有序的状态。先序遍历即先遍历他自己,后序遍历就是最后遍历他自己,无非是将输出和输出上下的两个代码块换一下位置什么的,先序放在头前,后序放在最后。

最后是二叉树的删除操作,二叉树的删除是一个累活,我们先看最开心的全删掉。
还记得嘛,链表那一节我说过,动态内存要有借有还,二叉树是动态内存,所以二叉树要有借有还(三段论学的多好)。二叉树用完了应该扔进垃圾桶,不要随处乱放。
删除操作就是典型的后序遍历,设想一下,我只有先将边上的结点剪掉,再剪枝杈,如果先剪掉了枝杈,我的叶子就找不到了,形象的讲叫风刮跑了,这可不行,至少代码也是要讲卫生的,先剪叶子,采取的就是后序遍历

//调用的时候传root进去
void allremove(bnode** ptr)//终于还是要用二重指针了,一旦涉及二叉树(链表也是一个道理)的增,删操作的时候都需要二重指针来传地址,我们才能对地址进行实在的操作,要不然,我们只是对一个复制品进行无谓的操作。
{
	if((*ptr)->left) allremove(&((*ptr)->left));//传进指针的地址,相当于传进了一个指向指针的指针
	if((*ptr)->right) allremove(&((*ptr)->right));
	free(*ptr);
}

如果我只想删除一个结点呢,有几种办法,但是都比较麻烦,这里采取一种比较简单的办法。

我们先不要想那么复杂的问题,先设想,我们想删除一个结点,是不是首先需要先找到这个结点呢?所以在删除之前首先需要对这个需要删除的结点进行定址。定址操作参考上面查找,调整一下返回值即可。这里直接探讨如何删除一个已经找到的结点。
二叉树毕竟不是数组,同样的一种顺序可以有很多种不同的物理地址的组织格式。我们只需要在删除的过程中一直秉承着不破坏之前的顺序结构就可以(即不会出现比A小的数跳到A的右子树上)我们设想一下,如果我想删除的是叶子结点,那是不是很简单,剪掉就好了嘛,但是如果是非叶子节点呢,这让我不禁陷入沉思。

先给叶子结点剪掉

void remove(bnode** ptr)//还是二重指针
{
	if ((*ptr)->left == NULL && (*ptr)->right == NULL)
	{
		if((*ptr)->parent->left == *ptr)
			(*ptr)->parent->left = NULL;
		else
			(*ptr)->parent->right = NULL;//所谓的缝合操作,删除节点之后不能还有野指针在末稍,所以用到了parent,老实话并不喜欢parent但是也没找到更好的办法
		free(*ptr);
	}
}

然后再思考如果不是叶子节点怎么办。
如果这个结点(记为B)有右节点,说明它不是最大的,那我们就可以找到它右子树的最左结点,即比他大的最小结点B‘代替它的位置,(因为这么做不会破坏顺序性嘛),然后再考虑删除B’,为什么这么做呢?首先,我每这么做一次,就会让这个地址的位置接近最末稍的位置(这么做是肯定不会往根的方向查找的,只有可能更接近底层)

else if ((*ptr)->right != NULL)
{
	bnode* rcd = (*ptr)->right;//从右子树开始找
	while(rcd->left != NULL)//找最左侧的结点
		rcd = rcd->left;
	(*ptr)->data = rcd->data;//进行数据拷贝
	remove(&rcd);//删掉这个重复的结点,实现了递归
}

那要是。。。。
连右子树也没有。。但是有左子树呢?
这里提出一个比较简单的办法,但是实际这么操作并不严谨,就是直接拷贝右子树的操作模式,这里给出这种实现

else
{
	bnode* rcd = (*ptr)->left;
	while(rcd->right != NULL)
		rcd = rcd->right;
	(*ptr)->data = rcd->data;
	remove(&rcd);
}

这么操作可以实现功能,但是理论上破坏了左子树严格小于根节点的结构,这里不给出具体的实现,可以自己试一试。

评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值