二叉树学习笔记-实现

上一篇文章中,算是初步了解了二叉树是一种怎样的数据结构,也算是有了一个初步的印象。接下来用自己的代码去实现一个二叉搜索树(以下全叫二叉树)类,对外提供常用的接口,比如insert、erase、size、find等等。就像盖房一样,如果说二叉树是一座建筑,那么其中的节点就是一块块砖。要实现二叉树这个类,就必须先实现节点类,假设我们起名为treeNode。在STL标准库中,像一般的数据结构都是模板类,在这里为了方便起见,假设二叉树这个类中保存的数据全是int型。

这个节点类中,需要包含如下的一些成员:节点值value,指向左子节点的指针left,指向右子节点的指针right,指向父节点的指针parent。用代码实现如下:

class treeNode
{
public:
	int value;
	treeNode *left;
	treeNode *right;
	treeNode *parent;
	treeNode()
	{
		value = 0;
		left = NULL;
		right = NULL;
		parent = NULL;
	}
};

为了方便起见,我们将值初始化为0,指针全部初始化为NULL。

有了这个类以后,等于有了盖房子的基本原料。现在还需要实现二叉树类,目的就是按照一定的规则,将一个个节点添加进来,构成二叉树。与此同时还需要向外部用户提供友好的接口,而不必关心内部的细节。比如说,我现在想将值为12的一个节点插入二叉树,只需要调用insert(12)即可,而不是先生成一个节点的实例,赋值为12,再调整指针,插入二叉树,同时还要保证二叉树的结构特性。这些实现细节都需要用insert函数封装起来。

在二叉树这个类中,需要包含如下的成员,insert()、erase()、find()、size()成员函数,treeSize,根节点的指针root。当然还有一个构造函数。用代码实现如下:

class searchTree
{
public:
	searchTree(const int &value);
	int	insert(const int &value);
	int	erase(const int &value);
	treeNode* find(const int &value);
	int size() const;
private:
	int treeSize;
	treeNode* root;
};

在构造函数中,我们将二叉树的实例初始化为包含一个根节点,根节点值为value的树。

insert函数首先会生成一个节点实例,节点值为value,然后将这个节点插入到树中。插入成功返回0,插入失败返回非0,当树中包含有相同的节点,即已经存在值为value的节点,插入失败。

erase函数删除树中一个值为value的节点,删除成功返回0,删除失败返回非0,当树中不包含值为value的节点,删除失败。

find函数会在树中查找值为value的节点,返回该节点的指针。如果查找成功返回该节点的指针,查找失败返回NULL,如果树中不包含值为value的节点,查找失败。

size函数返回当前树中节点的数量。

私有成员变量treeSize保存当前树中节点的个数,指针root保存跟节点的地址。

接下来实现构造函数,构造函数中只需要生成一个根节点的实例,然后将根节点的值设为value,同时treeSize值为1,表示当前树中有一个节点。代码如下:

searchTree::searchTree(const int &value)
{
	root = new treeNode();
	root->value = value;
	treeSize = 1;
}

接下来实现size函数,因为size函数最简单,直接返回当前树中节点的个数。代码如下:

int searchTree::size()
{
	return treeSize;
}

接下来实现find函数,find函数会在树中查找值为value的节点,返回这个节点的指针。实现的思路就是从根节点开始比较根节点值和value的大小关系,如果根节点的值大于value,就沿左子节点下降,否则就沿右子节点下降。遵循这种下降规则,会出现两种可能,第一种就是找到节点值和value相等的节点,返回节点指针。第二种就是下降到一个空节点,也就是NULL,此时返回NULL,代表查找失败。代码如下:

treeNode* searchTree::find(const int &value)
{
	treeNode *curr;
	curr = root;					//从根节点开始从上到下查找
	while (curr!=NULL)				//终止条件之一就是下降到空节点
	{
		if ((curr->value) > value)	//值小于当前节点的值,沿左子节点下降
		{
			curr = curr->left;
		}
		else if ((curr->value)<value)	//值大于当前节点的值,沿右子节点下降
		{
			curr = curr->right;
		}
		else						//值等于当前节点的值,查找结束,返回
		{
			return curr;
		}
	}
	return NULL;					//已经下降到空节点还是没找到该节点,代表该节点不在树中,查找失败,返回NULL
}

接下来实现insert函数,insert函数会在当前的树中插入一个节点值为value的节点。函数的实现思想和find函数是类似的,也是从根节点开始比较根节点的值和value的大小关系,如果根节点值大于value,就沿左子节点下降,否则沿右子节点下降。这样下降最终会出现两种可能,第一种就是找到一个节点值和value大小相等,此时代表树中已经有值为value的节点,返回1,代表插入失败。第二种就是下降到一个空节点,也就是NULL,此时找到了正确的插入位置,新生成一个节点,赋值为value,插入到该位置。需要注意的问题就是在下降的过程中需要记录当前节点的父节点,这样当最终插入成功时,新生成的节点才知道自己的父节点是谁。还要记录最后一次下降是沿左子节点还是右子节点下降,这样最终插入的节点就知道自己是其父节点的左子节点还是右子节点。代码如下:

int searchTree::insert(const int &value)
{
	int direct = -1;				//从上到下下降时,需要记录上一次下降时沿左子节点还是沿右子节点下降
	treeNode *curr,*par;			//当前节点以及当前节点的父节点
	curr = root;					//从根节点开始从上到下查找
	/*从上到下查找插入位置的过程需要保存当前节点的父节点,虽然每一个节点保存了自己的父节点,
	但是插入的位置是一个空节点,也就是NULL,空节点是没有自己的父节点*/
	par = root;
	while (curr!=NULL)				//终止条件之一就是查找到一个空节点作为插入位置
	{
		par = curr;					//保存父节点
		if ((curr->value) > value)	//如果值小于当前节点的值,沿左子节点下降
		{
			curr = curr->left;
			direct = 0;
		}
		else if ((curr->value)<value)//如果值大于当前节点的值,沿右子节点下降
		{
			curr = curr->right;
			direct = 1;
		}
		else
		{
			return 1;				//如果值等于当前节点的值,插入失败,代表树中已有该节点,返回1
		}
	}
	curr = new treeNode();			//动态生成一个节点,赋值为value
	curr->value = value;
	if (direct!=-1)					//如果值为-1,代表根节点为NULL,出现错误
	{
		if (direct == 0)			//表示做后一次下降是沿左子节点下降,那么新插入的节点就是父节点的左子节点
		{
			par->left = curr;
		}
		else						//表示最后一次下降是沿右子节点下降,那么新插入的节点就是父节点的右子节点
		{
			par->right = curr;
		}
		curr->parent = par;			//更新插入节点的父节点
		treeSize++;					//节点数量加一
		return 0;
	}
	return 1;						//根节点为NULL,出现错误,返回1
}

接下来实现erase函数,这个函数放在最后一个就是因为这是最难的一个函数。我们的第一反应是删除不是很简单吗,找到这个节点,释放掉这段内存不就好了吗。如果只是单纯删除一个节点,很简单。难的地方在于删除了这个节点之后,还要保持二叉树的结构特性。也就是说删除节点之后,剩下的结点还要组成一棵完整的树,其中还有一层内在的含义就是,树中的每个节点还要满足左子节点值小于父节点,右子节点值大于父节点这样的关系。

我们假设有下面这样一棵树:

假设我们删除8这个节点,如果按照上面的思想,只是单纯的释放掉8节点的内存,那么根节点12就没有了左子节点,左子节点指向了一段未分配的内存,这样在访问的时候就会出错。如果不能沿左子节点下降,那么剩下的6、11、10这三个节点都不能被访问。产生的还有一个问题就是节点6、11没有了自己的父节点,那么在遍历树的时候,是不能沿父节点返回的。显然这种删除的方法是不科学的。可是如果删除10节点,如果还按原先的思路,释放10节点的内存,将11节点左子节点指针指向NULL,好像又是合理的。

节点8和节点10的区别就在于8的左右子节点均不为空,而10节点的左右子节点均为空。这样我们就按左右子节点是否为空的原则,将节点分为四种情况:

1、左右子节点均为空。

2、左子节点为空,右子节点不为空。

3、右子节点为空,左子节点不为空。

4、左右子节点均不为空。

针对上述四种情况,我们要找出不同的删除规则,目的都是一样的。那就是找出一个替代节点,替代当前被删除的节点,这个替代节点的作用就是替代掉被删除的节点,依然能保证剩余的节点依然是一棵完整的二叉树。

第一种情况下,替代节点就是NULL。

第二种情况下,上图中13节点满足这种情况,此时找到的替代节点就是它的右子节点,16节点。

第三种情况下,上图中的11节点满足这种情况,此时找到的替代节点就是它的左子节点,10节点。

第四种情况下,上图中的8节点满足这种情况,此时找替代节点的方法就是先下降到该节点的右子节点,也就是11节点。,如果该节点没有左子节点,那么该节点就是替代节点,也就是图中的11节点。如果有左子节点,就再沿该节点的左子节点一直下降到末端。此时找到的替代节点就是图中的10节点。

替代节点并不是唯一的,只要找到的替代节点能保证剩余的节点依然是一棵完整的二叉树即可。寻找的过程中,要保持一个固定的规则不变,而且要尽量简单、快速。上述寻找的过程中我们的规则就是先将节点分类,再针对四种不同的情况,设置四种子规则。代码如下:

int searchTree::erase(const int &value)
{
	treeNode *curr, *par, *replace, *replacePar; //分别为当前节点,当前节点的父节点,替代节点,替代节点的父节点
	int direct = -1;
	curr = root;								//从根节点开始查找待删除的点,查找过程和find函数一致
	par = root;
	while (curr!=NULL)
	{
		if ((curr->value) > value)
		{
			par = curr;
			curr = curr->left;
			direct = 0;
		}
		else if ((curr->value)<value)
		{
			par = curr;
			curr = curr->right;
			direct = 1;
		}
		else									//找到了待删除的点
		{
			if (curr->left==NULL&curr->right==NULL)		//对应于情况1,左右子节点均为NULL,也就是该节点为叶节点
			{
				if (direct == 0)				//根据最后一次下降方向,将父节点的左子节点或右子节点置为NULL
				{
					par->left = NULL;
				}
				else if (direct==1)
				{
					par->right = NULL;
				}
				delete curr;					//释放内存,节点数量减一
				treeSize--;
				return 0;
			}
			else if (curr->left==NULL&curr->right!=NULL)	//对应于情况2,左子节点为空,右子节点不为空
			{
				replace = curr->right;			//替代节点就是当前节点的右子节点
				/*根据最后一次下降方向,将父节点的左子节点或者右子节点置为替代节点,替代节点父节点为当前节点的父节点*/
				if (direct == 0)
				{
					par->left = replace;
					replace->parent = par;
				}
				else if (direct==1)
				{
					par->right = replace;
					replace->parent = par;
				}
				else			//direct=-1表示删除的根节点,将根节点设置为替代节点,替代节点的父节点置为NULL
				{
					root = replace;
					replace->parent = NULL;
				}
				delete curr;				//释放当前节点的内存,节点数量减一
				treeSize--;
				return 0;
			}
			else if (curr->left != NULL&curr->right == NULL)	//对应于情况3,和情况2类似
			{
				replace = curr->left;
				if (direct == 0)
				{
					par->left = replace;
					replace->parent = par;
				}
				else if (direct == 1)
				{
					par->right = replace;
					replace->parent = par;
				}
				else
				{
					root = replace;
					replace->parent = NULL;
				}
				delete curr;
				treeSize--;
				return 0;
			}
			else								//左右节点均不为空
			{
				replace = curr->right;			//先将替代节点暂定为当前节点右子节点
				if (replace->left==NULL)		//如果右子节点的左子节点为NULL,那么右子节点就是替代节点
				{
					replace->left = curr->left;		//更新替代节点的左子节点为当前节点的左子节点
					if (curr->left!=NULL)
					{
						/*如果当前节点的左子节点存在,更新左子节点的父节点。此时如果左子节点为NULL,NULL是没有父节点的*/
						curr->left->parent = replace;
					}
				}
				else							//右子节点左子节点不为NULL,此时就要沿着左子节点一直下降到末端
				{
					replacePar = replace;		//下降的过程保存替代节点的父节点
					while (replace->left != NULL)	//一直下降到末端
					{
						replacePar = replace;
						replace = replace->left;
					}
					/*下降到末端时,替代节点会出现两种情况。第一种是替代节点的右子节点不为NULL,
					那么就需要将其右子节点作为替代节点父节点的左子节点,也就是将替代节点的右子树,
					重新链接在树上*/
					replacePar->left = replace->right;	//无论哪种情况,都将替代节点的右子节点作为替代节点父节点的左子节点
					if (replace->right!=NULL)			//如果右子节点不为NULL,要更新右子节点的父节点
					{
						replace->right->parent = replacePar;
					}
					replace->left = curr->left;			//更新替代节点的左子节点指针
					curr->left->parent = replace;
					replace->right = curr->right;		//更新替代节点的右子节点指针
					curr->right->parent = replace;
				}
				if (direct == 0)						//根据最后一次下降方向,再更新父节点的指针
				{
					par->left = replace;
					replace->parent = par;
				}
				else if (direct == 1)
				{
					par->right = replace;
					replace->parent = par;
				}
				else
				{
					root = replace;
					replace->parent = NULL;
				}
				delete curr;							//释放掉当前节点的内存,树中节点数量减一
				treeSize--;
				return 0;
			}
		}
	}
	return 1;
}

删除一个节点难就难在最后一种情况,根据下图说明一下如何删除一个左右子节点均不为空的情况

现在假设删除节点36和节点11。

删除节点35:首先找到节点35的右子节点,节点64,发现64的左子节点为NULL,那么64就是替代节点。此时删除掉35节点需要更新有关35节点的三个关系指针,就是和左子节点的关系指针,和右子节点的关系指针,和父节点的关系指针。首先将节点64的左子节点指针设置为35的左子节点,也就是31节点,将31节点的父节点指针设置为节点64。左子节点关系指针更新完毕。同理再更新父节点的关系指针。因为替代节点就是删除节点的右子节点,所以右子节点的关系指针不需要更新。

删除节点11,首先将替代节点暂定为删除节点的右子节点,发现右子节点的左子节点不为NULL,沿着左子节点一直下降到末端,也就是节点12,为替代节点。此时要同时更新左子节点,右子节点,父节点的关系指针(简单的理解就是将节点12填充到原来11的位置,更新节点12与周边的关系)。更新完以后,发现从节点15以下的全部节点脱离了原来的树,所以还需要更新节点15与节点19的关系。

这等于说删除节点11是按两个步骤进行的,第一步找到替代节点,并从树中删除,因为此时替代节点是沿左子节点一直下降到末端,那么其左子节点必为NULL,那么删除替代节点的操作等于情况1或者情况2。第二步再将替代节点填充到删除节点的位置,更新与周边的关系指针,释放删除节点的内存。

到此,我们为了测试方便,还需要拿到一棵树的根节点,如果按照中序遍历输出树中的全部节点,就会按照从小到大排序的顺序输出。拿到这个根节点有两种方法,第一种就是将类中的root节点设置为public,或者我们还需要在类中再添加一个方法rootNode,返回当前的根节点。考虑到封装,后者还是比较好。考虑到安全性,应该将返回的指针设置为常量,这里为了方便。

treeNode* searchTree::rootNode()
{
	return root;
}

为了测试,我们还需要一个按中序将树中节点输出的函数,为了代码简洁,采用递归的方式。代码如下:

void output(treeNode *node)
{
	if (node!=NULL)
	{
		output(node->left);
		cout << node->value << "-";
		output(node->right);
	}
}

到此为止,准备工作全部结束,写一个简单的测试函数,测试一下我们自己实现的二叉树。代码如下:

int main()
{
	treeNode *root,*curr;
	searchTree tree(100);
	for (size_t i = 0; i < 20; i++)
	{
		tree.insert(rand() % 200);
	}
	cout << tree.size() << endl;
	curr = tree.find(41);
	if (curr!=NULL)
	{
		cout << "find the node" << endl;
	}
	else
	{
		cout << "the node is not in the tree" << endl;
	}
	tree.erase(41);
	root = tree.rootNode();
	output(root);
}

首先声明一个树的实例,根节点为100,再随机生成20个0-199的数插入到树中。然后查找值为41的节点,最后将这个节点删除。最后打印树中全部节点,如果节点是按从小到大顺序打印,就代表这是一棵完整的二叉树。输出结果如下:

刚开始size为19的原因是随机可能生成重复点,这样插入就会失败。我们看到节点以及从小到大顺序打印。最后再上传一张在vs环境下,树的内存分布图:

我们可以清晰地看到根节点为100,左右子节点分别为64和134。节点64的左右子节点分别为27和67,也可以清晰地看到它们在内存中的分布情况。

时间: 2024-10-21 04:55:48

二叉树学习笔记-实现的相关文章

二叉树学习笔记。

1. 层序构建和先序遍历: 1 public class Tree { 2 public Tree left; 3 public Tree right; 4 public int val; 5 6 public Tree() { 7 8 } 9 10 public Tree(String n) { 11 this.val = Integer.parseInt(n); 12 } 13 14 public Tree createTreeByLevel(String[] var) { 15 if (v

二叉树学习笔记之二叉查找树(BSTree)

二叉查找树即搜索二叉树,或者二叉排序树(BSTree),学习回顾一下有关的知识. >>关于二叉查找树 二叉查找树(Binary Search Tree)是指一棵空树或者具有下列性质的二叉树:1. 若任意节点的左子树不空,则左子树上所有节点的值均小于它的根节点的值:2. 若任意节点的右子树不空,则右子树上所有节点的值均大于它的根节点的值:3. 任意节点的左.右子树也分别为二叉查找树.4. 没有键值相等的节点,这个特征很重要,可以帮助理解二叉排序树的很多操作.二叉查找树具有很高的灵活性,对其优化可

二叉树学习笔记1

数组.向量.链表都是一种顺序容器,它们提供了按位置访问数据的手段.而很多情况下,我们需要按数据的值来访问元素,而不是它们的位置来访问元素.比如有这样一个数组int num[3]={1,2,3},我们可以非常快速的访问数组中下标为2的数据,也就是说我们知道这个数据的位置,就可以快速访问.有时候我们是不知道元素的位置,但是却知道它的值是多少.假设我们有一个变量,存放在num这个数组中,我们知道它的值为2,却不知道它下标是多少,也就是说不知道它的位置.这个时候再去数组中访问这个元素就比较费劲,就得遍历

二叉树学习笔记之树的旋转

树旋转(Tree rotation)是二叉树中的一种子树调整操作,每一次旋转并不影响对该二叉树进行中序遍历的结果.树旋转通常应用于需要调整树的局部平衡性的场合. >>左旋和右旋 树的旋转有两种基本的操作,即左旋(逆时针方向旋转)和右旋(顺时针方向旋转). 树旋转包括两个不同的方式,分别是左旋转(以P为转轴)和右旋转(以Q为转轴).两种旋转呈镜像,而且互为逆操作. 下图示意了两种树旋转过程中, 子树的初态和终态 +---+ +---+ | Q | | P | +---+ +---+ / \ ri

java学习笔记13--比较器(Comparable、Comparator)

java学习笔记13--比较器(Comparable.Comparator) 分类: JAVA 2013-05-20 23:20 3296人阅读 评论(0) 收藏 举报 Comparable接口的作用 之前Arrays类中存在sort()方法,此方法可以直接对对象数组进行排序. Comparable接口 可以直接使用java.util.Arrays类进行数组的排序操作,但对象所在的类必须实现Comparable接口,用于指定排序接口. Comparable接口的定义如下: public  int

集合类学习笔记

一.概念 集合是存储对象的一种方式.集合中都是存放着地址,方便引用.JDK 1.2版本的时候就有了 二.集合和数组的区别 集合是可变长度,数组是固定长度. 数组可以存储基本数据类型,集合只能存储对象,集合可以存储不同类型的对象. Collection 1.List:元素是有序的,可以重复,有索引 2.Set:元素是无序的,不可以重复,使用hash值排列 三.CURD boolean add(E e); boolean addAll(Collection<? extends E> c); voi

[学习笔记]数据结构与算法

1.排序简单排序:?冒泡排序:将n个数从上往下排列,从第0个数开始依次对前n个.前n-1个.前n-2个数进行比较,保持小数在前大数在后,不符合就交换.在这个过程中,最后一个数始终是最大数.?选择排序:对所有n个.后n-1个.后n-2个依次比较,用一个变量存最小数,一趟比较完成之后,将最小数与所比较数据的第一个数进行交换.在这个过程中,第一个数始终是最小数.?插入排序:从第1个数开始向前扫描比较,小则插入.对于未排序数据,在已排序序列中向前扫描,并找到相应的位置插入.在这个过程中,整个序列局部有序

面向对象先导学习笔记

面向对象先导学习笔记 经过了Python.C语言和数据结构的学习,感觉对于Java这门新学习的语言上手速度明显快了许多.在学习Java的过程中,个人觉得Java的语法规则在很大程度上与C类似,但是在设计程序和具体实现的过程中,更偏向于Python的思路,尤其是对于方法的调用和自带数据结构的使用方面. 数据类型的Java实现 Java自带了大量的数据类型,在完成作业的过程中,个人尝试通过手写二叉树完成问题,但是与Java自带的数据结构相比,无论是在稳定性和运行速度方面都有所欠缺.但是通过自己的摸索

数据结构与算法基础学习笔记

*********************************************            ---算法与数据机结构--- 数据结构:由于计算机技术的发展,需要处理的对象不再是纯粹的数值,还有像字符,表,图像等具有一定结构的数据,需要用好的算法来处理这些数据. 我们把现实中大量而又复杂的问题以特定的数据类型的特定的存储结构保存到主存储器中,以及在此基础上为实现某个功能而执行的相应操作(查找排序),这个相应的操作也叫算法. 数据结构 = 个体 +个体的关系算法 =对存储数据的操