树.(一)

树的定义

树(tree)可以用几种方式定义。定义树的一种自然的方式是递归的方法。一棵树是一些结点的集合。这个集合可以是空集;若不是空集,则树由称作根(root)的结点r以及零个或多个非空的(子)树T,T2,…,T组成,这些子树中每一棵的根都被来自根r的一条有向的边(edge)所连接。每一棵子树的根叫作根r的儿子(child),而r是每一棵子树的根的父亲(parent)。

在这里插入图片描述

从递归定义中可以发现,一棵树是N个结点和N-1条边的集合,其中的一个结点叫作根。存在N-1条边的结论是由下面的事实得出的:每条边都将某个结点连接到它的父亲,而除去根结点外每一个结点都有一个父亲。
在这里插入图片描述
结点A是根。结点F有一个父亲A并且有儿子K、L和M。每一个结点可以有任意多个儿子,也可能没有儿子。没有儿子的结点称为叶(leaf)结点;图中的叶结点(树叶)是B、C、H、1、P、Q、K、L、M和N。具有相同父亲的结点为兄弟结点;因此,K、L和M都是兄弟。用类似的方法可以定义祖父和孙子关系。

对任意结点n,n的深度为从根到n,的唯一路径的长。因此,根的深度为0。n的高是从n,到一片树叶的最长路径的长。因此所有的树叶的高都是0。一棵树的高等于它的根的高。对于图中的树,E的深度为1而高为2;F的深度为1而高也是1;该树的高为3。一个树的深度等于它的最深的树叶的深度;该深度总是等于这棵树的高。

树的实现

实现树的一种方法是在每一个结点除数据外还要有一些链,来指向该结点的每一个儿子。然而,由于每个结点的儿子数可能变化很大并且事先不知道,因此在数据结构中建立到各儿子结点的直接链接是不可行的,因为这样会产生太多浪费的空间。实际上解法很简单:将每个结点的所有儿子都放在树结点的链表中。以下就是非常典型的声明。

struct TreeNode
{
	ElemType data;
	TreeNode* childlist;//儿子
	//TreeNode* brotherlist;//兄弟
}

在这里插入图片描述
图中向下的箭头是指向child的链。
从左到右的箭头是指向brother的链。因为空链太多,所以没有把它们全画出。

树的应用及遍历

树有很多应用。流行的用法之一是用于包括UNIX和DOS在内的许多常用操作系统中的目录结构。下图是UNIX文件系统中的一个典型的目录。
在这里插入图片描述
这个目录的根是/usr(名字后面的星号指出/usr本身就是一个目录)。/usr有三个儿子: mark、alex和bill,它们自己也都是目录。因此,husr包含三个目录并且没有常规的文件。文件名/usr/mark/book/chl.r先后三次通过最左边的儿子结点而得到。第‘个“I”后的每个“I”都表示一条边;结果为一全路径名(pathname)。这个分级文件系统非常流行,因为它使得用户能够逻辑地组织数据。不仅如此,在不同目录下的两个文件还可以享有相同的名字,因为它们必然有从根开始的不同的路径从而具有不同的路径名。在UNIX文件系统中的目录就是含有它的所有儿子的一个文件,因此,这些目录几乎是完全按照上述的类型声明构造的’。事实上,按照UNIX的某些版本,如果将打印文件的标准命令应用到目录上,那么目录中的文件名能够在输出中看到。

//伪码
Print(Node,depth)//depth用来控制缩进
{
	printf(name);
	Print.all(node->child,depth);
}

一般有前中后三种遍历方式。
在前序遍历中,对结点的处理工作是在它的诸儿子结点被处理之前进行的。
在后序遍历中,在一个结点的工作是在它的诸儿子结点被计算后进行的。
中序遍历一般知道是遍历二叉树树的,先处理左儿子,在处理自己,再去处理右儿子。

二叉树

二叉树(Binary Tree)是另一种树型结构,它的特点是每个结点至多只有两棵子树(即二叉树中不存在度大于2的结点),并且,二叉树的子树有左右之分,其次序不能任意颠倒。
在这里插入图片描述
二叉树的一个性质是平均二义树的深度要比结点个数N小得多,这个性质有时很重要。分析表明,这个平均深度为O( √N ),而对于特殊类型的二叉树,即二叉查找树(binary search tree),其深度的平均值是O(logN)。遗憾的是,正如下图中的例子所示,这个深度也可以大到N-1。

在这里插入图片描述
因为一个二叉树结点最多有两个儿子,所以可以直接链接到它们。树结点的声明在结构上类似于双向链表的声明。在声明中,一个结点就是由element((元素)的信息加上两个到其他结点的引用( left和right)组成的结构。

typedef struct BinaryTreeNode
{
	ElemType data;
	BinaryTreeNode* left;
	BinaryTreeNode* right;
}BT;

简单实现一下,二叉树的插入,删除,查找。

二叉树的插入

BT* Insert_BT(BT* root, ElemType e)//给root里面插个e,因为返回的是BT*,所以也可以兼顾到初始化的作用
{
	if (root == NULL)//如果是空树,就把e当作唯一节点
	{
		BT* tmp = (BT*)malloc(sizeof(BT));
		tmp->data = e;
		tmp->left = NULL;
		tmp->right = NULL;
		return tmp;
	}
	int k = rand() % 2;//随机往左/右子树里插
	if (k == 0) 
	{
		root->left = Insert_BT(root->left, e);//因为root->left可能为空,所以得用它接收一下
	}
	else
	{
		root->right = Insert_BT(root->right, e);
	}
	return root;
}

二叉树的查找

因为插入时没有规律,随意查找只能遍历

BT* Search_BT(BT* root, ElemType e)//返回一个指向该节点的指针
{
	if (root == NULL)//空树/空节点没必要找
	{
		return NULL;
	}
	else if(root->data == e)//当前节点是,就返回
	{
		return root;
	}
	else
	{
		BT* tmp = Search_BT(root->left, e);//先去左树找
		if (tmp == NULL)//没找到只能去右树找
		{
			return Search_BT(root->right, e);//不管找没找到只能返回该答案
		}
		else
		{
			return tmp;//在左树找到了就返回该答案
		}
	}
}

二叉树节点的删除

先说一下思路:
1.先找到该节点,没找到就不用删除。
2.在找该节点的父亲节点,根节点也可以删除,但根节点没有父亲,(单独的节点,也没有父亲),所以在找父亲节点的时候一定得保障该节点是这颗树上的一个结点。
3.如果删除的不是根节点

  • 1.如果只是叶子节点,则直接删了,在修改一下其父节点的指向它的指针为空
  • 2.如果它只有一个孩子,则修改其父节点的指向它的指针指向它唯一的孩子节点,在删除
  • 3.如果它有连个孩子,则在以它为根节点的子树中找到一个叶子节点,来顶替它的位置(类似与堆的删除)

最后返回root就完成了。

4.如果删除的是根节点
思路类似,不过返回值有不同
1.NULL
2.唯一的孩子节点
3.找出来用来顶替的叶子节点

为此我们需要三个函数
1.返回值为BT*或BT的一个查找函数
2.返回值为BT*或BT的一个查找树中节点父节点的函数(使用线索二叉树就不用了)
3.返回值为BT*或BT的一个找叶子节点的函数
接下来就一步步实现了

查找函数

这个直接使用二叉树的查找函数就好了。

找父节点的函数

因为空节点没法找父亲,只能返回NULL,而根节点也没有父亲,也会返会NULL。因此在使用是一定得保证Node是root这个树上的。如果找到好说,返回个正确值,但要返回个NULL,可就无法区分了(也可以在调用前判断要删除的是不是父亲节点,那么在使用时,返回NULL,就只剩一种情况了。)

BT* GETFATHER(BT* root, BT* Node)
{
	if (Node == NULL)//要找的是个空节点没法找
	{
		printf("NULL POINTER HAVE NOT FATHER \n");
		return NULL;
	}
	BT* tmp = NULL;
	if (root == NULL) return NULL;//空节点没有孩子
	if (root->left == Node) return root;
	if (root->right == Node) return root;//判断当前节点是不是Node父亲
	tmp = GETFATHER(root->left, Node);//不是则先去左树上找
	if (tmp == NULL)//没找到再去右树上找	
	{
		tmp = GETFATHER(root->right, Node);
	}
	return tmp;
}

找叶子节点的函数

一颗不空的树肯定有叶子。
找叶子这个函数其实有两个任务,1.找到一个叶子节点,2.修改其父的孩子指针。
所以再用是得确保需要顶底的节点是再用。
因为得修改其父节点的指针嘛,我们能得动过其父节点在去判断其孩子几点是不是叶子节点,这样就可以一步到位。

当让也可以先找到,在使用找父亲节点的函数,再去修改。

BT* GETaLEEF(BT* root)
{
	if (root == NULL) return NULL;
	if (root->left != NULL && root->left->left == NULL && root->left->right == NULL)
	{
		BT* tp = root->left;
		root->left = NULL;
		return tp;
	}
	if (root->right != NULL && root->right ->left == NULL && root->right->right == NULL)
	{
		BT* tp = root->right;
		root->right = NULL;
		return tp;
	}
	int k = rand() % 2;//没有的话,随机去左/右子树上找
	if (k == 1)
	{
		return GETaLEEF(root->left);
	}
	else
	{
		return GETaLEEF(root->right);
	}
}

删除函数的实现

OK,三个函数有了,就可以实现我们的删除了,malloc的空间要free掉

BT* Remove_BT(BT* root, ElemType e)
{
	BT* tmp = Search_BT(root, e);
	if (tmp == NULL)//没找到
	{
		return NULL;
	}
	BT* fat = GETFATHER(root, tmp);//先获取一下父节点
	if(fat == NULL)//要删的是根节点
	{
		if (tmp->left == NULL && tmp->right == NULL)//情况1
		{
			free(tmp);
			return NULL;
		}
		else if (tmp->left == NULL || tmp->right == NULL)//情况2
		{
			BT* tp = tmp->left == NULL ? tmp->left : tmp->right;
			free(tmp);
			return tp;
		}
		else//情况3
		{
			BT* tp = GETaLEEF(tmp);
			tp->left = tmp->left;
			tp->right = tmp->right;
			free(tmp);
			return tp;
		}
	}
	
	if (tmp->left == NULL && tmp->right == NULL)//情况1
	{
		if (fat->left == tmp)
		{
			free(fat->left);
			fat->left = NULL;
		}
		else
		{
			free(fat->right);
			fat->right = NULL;
		}
	}
	else if (tmp->left == NULL || tmp->right == NULL)//情况2
	{
		BT* tp = tmp->left ==NULL ? tmp->left : tmp->right;
		if (fat->left == tmp)
		{
			free(fat->left);
			fat->left = tp;
		}
		else
		{
			free(fat->right);
			fat->right = tp;
		}
	}
	else//情况3
	{
		BT* tp = GETaLEEF(tmp);
		tp->left = tmp->left;
		tp->right = tmp->right;
		if (fat->left == tmp)
		{
			free(fat->left);
			fat->left = tp;
		}
		else
		{
			free(fat->right);
			fat->right = tp;
		}
	}
	return root;
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值