后台开发学习笔记(五、树、二叉树)

作为电子专业,对树的认识比较少,因为平时基本用不到,最多也就是链表,不过了解了树之后,才发现树的用处比链表的还多,所以这一次有必要好好补充一些树的知识。

5.1 树

5.1.1 树的概念

树(Tree)是n(n>=0)个结点的有限集。n=0时称为空树。在任意一颗非空树中:
(1)有且仅有一个特定的称为根(Root)的结点;
(2)当n>1时,其余结点可分为m(m>0)个互不相交的有限集T1、T2、…Tm,其中每一个集合本身又是一颗树,并且称为根的子树(subTree),如图
在这里插入图片描述
图片来自网络
概念说了那么多,还不如来图的实在。实话说的好有图有真相。

5.1.2 其他重要概念

结点:树的结点包含数据元素和若干个指向其子树的分支。
度(Degree):结点拥有的子树数称为结点的度。最大结点的度称为树的度
叶结点(leaf):度为0的结点称为叶结点或终端结点。
树的深度(Depth):结点的层次从根开始定义,根为第一层,根的孩子为第二层一次类推,树中结点的最大层次称为树的深度或高度。

上面的概念都来自《大话数据结构》,这些概念还是需要了解了解的,后面需要用到的。

5.2 二叉树

5.2.1 二叉树的定义

二叉树(Binary Tree)是(n>=0)个结点的有限集合,该集合或者为空集(称为空二叉树),或者由一个根结点和两颗互不相交的,分别称为根结点的左子树和右子树的二叉树组成。《大话数据结构》

概念这东西,看着就是难受,下面抽取一些特点再简化描述一下:
(1)每个结点最多有两颗子树,所以二叉树中不存在度大于2的结点,注意二叉树是可以没有子树或者有一颗子树的存在。
(2)左子树和右子树是有顺序的,次序不能任意颠倒。
(3)即使树种某结点只有一颗子树,也要区分它是左子树还是右子树。

二叉树图:
在这里插入图片描述
图片来源网络

  1. 斜树
    树的所有结点都只有左子树或者右子树,看着图就斜在一边的。这样就退化到线性结构了。图就不画了,只要是偷懒。

  2. 满二叉树
    在一颗二叉树中,如果所有分支结点都有左子树和右子树,并且所有叶子都在同一层上,这样的二叉树称为满二叉树。
    在这里插入图片描述
    看图,这就是满二叉树,每个结点都有左右子树,所有叶子节点都在同一层。

  3. 完全二叉树
    在这里插入图片描述
    完全二叉树这个就有点难受了,好好的搞这么多概念干啥,按我的话说,完全二叉树就是按0-9这中顺序排的,中间不能有缺失,就可以看着是完全二叉树,如果要看具体的文字描述,可以博客也可以看《大话数据结构》

5.2.2 二叉树的性质

这部分来自《大话数据结构》,个人感觉大话数据结构讲的还不错,这里借鉴借鉴

  1. 在二叉树的第i层上至多有2i-1个结点(i>=1)
    第i层包括第一层,根结点那层,因为每层都是2的分散出去的,就是2的几次方,所以这个公式就是2i-1,可以通过归纳法证明,我已经忘记归纳法了。

  2. 深度为k的二叉树至多有2k-1个结点(k>=1)
    注意这是2k之后再减1,跟上面的不一样,深度就是树的高。

  3. 对任何一颗二叉树T,如果其终端结点树为n0,度为2的结点树为n2,则n0=n2+1.
    这个具体怎么推到我也不是很清楚

4. 具有n个结点的完全二叉树深度为|log2n|+1
这一条比较重要,因为这是算深度的公式,由满二叉树的定义我们知道,深度为k的满二叉树的结点树为2k-1,通过n=2k-1到推出满二叉树的深度为k=log2n。完全二叉树层次序号跟满二叉树是一样的,只是再最后几个位置缺了几个,所以完全二叉树的结点数一点大于2k-1-1个,所以2k-1-1<n≤2k-1,因为n是整数,所以2k-1≤n<2k,两边取对数k-1≤log2n<k,而k作为深度也是整数,所以k-1=log2n,然后k=|log2n|+1.

  1. 如果对一颗有n个结点的完全二叉树(其深度为|log2n+1|)的结点按层序编号,(从第1层到第|log2n|层,每层从左到右),对任一结点i(1≤i≤n)有:
    (1)如果i=1,则结点i是二叉树根,无双亲;如果i>1,则其双亲是结点i/2.
    (2)如果2i>n,则结点i无左孩子(结点i为叶子结点);否则其左孩子是结点2i
    (3)如过2i+1>n,则结点i无右孩子;否则其右孩子是结点2i+1

后面三条加粗的话比较适合用在数组构建的二叉树上,因为用数组查找左右孩子和双亲就是利用下标,但是上面的情况适合根结点在数组下标为1的情况。就是把数组的首元素空出来,如果要用到数组下标为0的话:
(1)子结点为i,则双亲为(i-1)/2
(2)结点为i的,其左孩子为2i+1
(3)结点为i的,其右孩子为2i+2

二叉树的性质就讲到这里,这些理论终于讲完了,下面就可以详细研究二叉树。

5.2.3 二叉树的创建

大话数据结构里面有创建二叉树的例子,不过我就不写那种了, 直接写二叉排序树的创建。这个还比较实用一点。
二叉排序树的定义:二叉排序树,又称二叉查找树。它或者是一颗空树,或者是具有下列性质的二叉树。

  • 若它的左子树不空,则左子树上所有结点的值均小于它的根结点的值;
  • 若它的右子树不空,则右子树上所有结点的值均大于它的根结点的值;
  • 它的左、右子树也分别为二叉排序树。

讲的这么多,简单一点理解就是左子树比根结点小,右子树比根结点大,所以创建的要按照这个方式插入。
原始数据:12, 45, 89, 127, 7, 4, 56, 57, 789, 9

  1. 第一步插入头结点
    在这里插入图片描述
    只要一个头结点的树是不完整的,接下来插入第二个结点
  2. 插入45
    在这里插入图片描述因为45比12大,所以往右子树插入
  3. 接下来的结点89,127都比根结点大,所以都是插入到右子树上
    在这里插入图片描述
  4. 7对根结点12小,所以在左子树上,4又比7小,所以在7为结点的左子树上
    在这里插入图片描述
  5. 56比12大往右边走,也比45大,往右边走,比89小,所以插入89的左子树
    在这里插入图片描述
  6. 剩下的57,789,9 就一起画了,应该也知道二叉树排序树怎么插入了
    在这里插入图片描述
    插入完成之后就是这样的一颗树,明显这个数是不平衡的,右重左轻,如果是这样查找的话,效率也会下降很多,不过怎么说,这颗树是符合二叉排序树的性质的。

下面来看插入的代码:

//先看看树的数据结构
typedef int Elemtype;

#define BITREE_ENTRY(name, type)	\
	struct name						\
	{								\
		struct type *left;			\
		struct type *right;			\
	}

typedef struct BiTree_node
{
	Elemtype data;								//结点数据
	BITREE_ENTRY(, BiTree_node) bst;				//左右孩子的结点
}_BiTree_node;


typedef struct BiTree
{
	struct BiTree_node *root;
}_BiTree;

这次利用结构和数据分离,树的结点单独定义成一个结构体,然后再用一个大的结构体包含树的结点数据,其实链表也是可以这样实现的,只不过当初觉得麻烦,就没这样实现。

插入的操作代码:

/**
		* @brief  创建新结点
		* @param	
		* @retval 
		*/ 
		struct BiTree_node *biTree_creat_node(Elemtype data)
		{	
			struct BiTree_node *node = (struct BiTree_node*)malloc(sizeof(struct BiTree_node));
			assert(node);

			node->data = data;
			node->bst.left = NULL;
			node->bst.right = NULL;

			return node;
		}
		
	/**
		* @brief  插入二叉树对象(包括根结点)
		* @param	
		* @retval 
		*/ 
		int biTree_insert(struct BiTree *T, Elemtype data)
		{	
			assert(T);

			//如果头节点为空,就创建头结点
			if(T->root == NULL){
				T->root = biTree_creat_node(data);
				return 0;
			}

			//1.判断插入点
			struct BiTree_node *node = T->root;
			struct BiTree_node *temp = T->root;
			while(node != NULL){
				//记录上一个结点指针,这个跟单链表的插入有点像
				temp = node;
				//data比根结点小,往左子树走
				if(node->data > data){
					node = node->bst.left;
				}else {  //否则往右子树走
					node = node->bst.right;
				}				
			}

			//判断要插入的是左子树还是右子树
			if(temp->data > data){
				temp->bst.left = biTree_creat_node(data);	
			}else {
				temp->bst.right = biTree_creat_node(data);
			}	

			return 0;
		}

插入的思想,就是循环判断当前要插入的元素是在哪一个位置,是哪个结点的左子树还是右子树,当找到这个结点的时候,就可以适当的插入到对应的位置。

5.2.4 二叉树的遍历

二叉树都创建了,但是不知道创建的对不对是吧,总要遍历出来看看,像链表那样,一遍历就知道插入的对不对了,所以二叉树也是需要遍历的,但是二叉树的遍历跟其他的线性表不同,线性表就是以前说的数组,链表,因为计算机是顺序执行的,所以这个线性表比较好遍历,一个挨着一个遍历就行了,但二叉树因为有两个节点,所以要有策略去选择先遍历那一个子树,然后在遍历另一个子树,如果我们限制了从左到右的习惯方式,那么二叉树遍历只要有4种方式:

  1. 前序遍历
    规则是若二叉树为空,则空操作返回,否则先访问根结点,然后前序遍历左子树,再前序遍历右子树。还是看图好懂。
    在这里插入图片描述
    从图看出先从根结点出发,然后往左子树方向走,7是12的左子树,也是4,9的根结点,所以继续往7的左子树4,然后4没有了子树就,就切回到7的右子树9,然后9也没有子树,至此12的左子树遍历完成,接下来切到右子树45,然后45又根据上面所说的遍历。
    看着这么复杂,一度以为程序会很复杂,其实程序是很简单,利用了递归调用,这个就有点像我们当初写的二叉堆的递归了,先看看代码吧
/**
			* @brief  前序遍历,先遍历根结点,再左子树,然后右子树
			* @param	
			* @retval 
			*/ 
			void bstree_preOrderTraversal(struct BiTree_node* node)
			{	
				if(node == NULL)
					return ;

				printf("%d ", node->data);
				bstree_preOrderTraversal(node->bst.left);
				bstree_preOrderTraversal(node->bst.right);	
			}

函数参数是树的根结点,前序遍历是从根结点出发,所以先打印,然后左子树的优化,所以下次传承是根结点的左子树,函数第二次调用,然后打印这个结点,然后以这个结点又为根结点继续左子树打印,直到遇到了结点为空,开始回退函数,回退的第一个之后,就接着调用右子树的遍历,如果右子树有子树继续遍历,没有就退出,一直退出就可完成遍历。
这样我就不细写了,排序的时候因为不熟递归,所以写了两个详细的,现在熟了很多了,就不细写了。

  1. 中序遍历
    规则是若树为空,则空操作返回,否则从根结点开始(注意并不是先访问根结点),中序遍历根结点的左子树,然后访问根结点,最后中序遍历右子树。看图:
    在这里插入图片描述
    看图就好懂了,先从最左边的左子树开始(4),然后遍历这个左子树的根结点(7),再遍历这个左子树的根结点的右子树(9),然后往上7作为左子树,又开始遍历7的根结点(12),12的右子树(45)又作为根结点,所以要寻找45的左子树,但是我这个45是没有左子树的,所以直接遍历根结点(45),然后45的右子树(89)作为根结点,寻找左子树(56),然后56又作为根结点寻找左子树,这个56也没有左子树,然后遍历56根结点和右子树(57),接下来就遍历根结点89,在遍历89的右子树127、789。
    知道为什么右边遍历这么奇怪么,是因为我用了程序的里面的思路讲的,从程序的思想讲左边其实跟右边是一样的,先看程序
/**
			* @brief  中序遍历,先左子树,再根结点,然后右子树
			* @param	
			* @retval 
			*/ 
			void bstree_iNOrderTraversal(struct BiTree_node* node)
			{	
				if(node == NULL)
					return ;

				bstree_iNOrderTraversal(node->bst.left);
				printf("%d ", node->data);
				bstree_iNOrderTraversal(node->bst.right);	
			}

从函数来看,传入的参数也是树的结点,但是我们要从最左边的开始遍历,所以我们首先得目的的寻找最左边的结点,所以首先就去的就是遍历左子树,一直没有左子树的情况才返回,接着打印,这也是像我们刚刚遍历12的右子树的时候,因为45这个结点没有左子树,所以只能遍历45这个结点,正因为89有左子树,所以从56开始打印(89的左子树),我这里就不多说了,能体会到代码的自然能理解这种遍历方式

  1. 后序遍历
    规则是若树为空,则空操作返回,否则从左到右先叶子后结点的方式遍历访问左右子树,最后根结点。
    有前面两个铺垫,这个后续遍历就不讲这么多了。
    在这里插入图片描述
    刚刚在画的时候,老是画错,这个逻辑根我们平时思考的不太一样,谨记一点就是从叶子结点开始,如果没有左节点,就遍历右节点。
		/**
			* @brief  后序遍历,先左子树,再右子树,然后根结点
			* @param	
			* @retval 
			*/ 
			void bstree_postOrderTraversal(struct BiTree_node* node)
			{	
				if(node == NULL)
					return ;

				bstree_postOrderTraversal(node->bst.left);
				
				bstree_postOrderTraversal(node->bst.right);	
				printf("%d ", node->data);
			}

这三种遍历可以不用递归方式使用,可以用栈来实现,栈的原理根递归的也差不多,有兴趣可以多了解了解。

栈实现前序遍历的基本思想:(入栈打印,有左子树进栈,没有出栈,右子树进栈)
根结点12入栈
栈:12
然后12左子树7入栈
栈:12 7
然后7左子树4入栈
栈:12 7 4
然后4没有左子树,所以4出栈,7也出栈,7的右子树9进栈
栈:12 9
然后9没有子树出栈,12出栈,12的右子树45进栈
栈:45
然后45没有左子树,45出栈,右子树89出栈
栈:89
然后89的左子树56进栈
栈:89 56
然后56没有左子树出栈,56的右子树57进栈
栈:89 57
然后57没有子树 出栈
栈:89
然后89出栈,89右子树127进栈
栈:127
然后127没有左子树出栈,789进栈
栈:789
然后789没有子树,出栈
(现在先这么写,以后有时间完善完善)

  1. 层序遍历
    这个层序遍历我只画图,和说一下方法,具体的就不实现了,c语言没有泛型就是有点难受。
    在这里插入图片描述
    这个层序遍历是一层一层的遍历的,因为用链表实现的二叉树,一层一层遍历有点难受,所以需要借助队列。
    基本思想是:(出队输出打印)
    根结点12入队
    队列:12
    然后12出队,左右子树7,45入队
    对列:7 45
    然后7出队,7的左右子树4,9入队
    对列:45 4 9
    然后45出队,45的左右子树89入队
    对列:4 9 89
    然后4出队,4没有子树,不入队
    队列:9 89
    然后9出队,9没有子树,不入队
    对列:89
    然后89出队,左右子树56,127入队
    对列56 127
    然后65出队,右子树57入队
    对列:127 57
    然后127出队,右子树789入队
    对列:57 789
    然后57 789都出队
5.2.5 二叉排序树

前面的二叉树的创建,就是二叉排序树的插入操作,所以这里插入操作就不说了,看前面就可以了,二叉排序树的目的不是为了排序的,是为了提高查找和插入删除关键字的速度,不管怎么说,在一个有序数据集上查找,速度总是要快于无序的数据集的,而二叉排序树这种非线性的结构,也有利于插入和删除的实现。

  1. 查找操作
    其实查找操作也比较简单,可以用循环判断的方式,也可以用递归,用循环判断的方式可以看创建的时候,就是用了循环判断应该插入那个结点,查找我用递归,递归看着比较简单:
/**
		* @brief  查找二叉树数据
		* @param	
		* @retval 
		*/ 
		int biTree_search(struct BiTree_node* node, Elemtype data)
		{	
			if(node == NULL)						//说明找不到结点
				return -1;

			if(node->data == data)  {				//递归的返回条件
				printf("biTree_search %d\n", data);
				return 0;
			}
			else if(node->data > data){				//往左子树
				biTree_search(node->bst.left, data);
			}
			else{    //往右子树
				biTree_search(node->bst.right, data);
			}

			return 0;
		}
  • 删除操作
    二叉树的删除,有几种情况:

  • 只删除了叶子节点
    如果只删除了叶子节点的话,其他结点是不受影响的,所以直接删除

  • 删除结点只有左子树或者只有右子树的情况下
    结点删除后,将它的左子树或者右子树整个移动到删除结点的位置即可,它的左子树本来就比被删除的结点的父结点小,所以补上也符合二叉排序树的要求。
    在这里插入图片描述
    删除127,它的右子树789就补上它的位置,成为89的右子树
    在这里插入图片描述

  • 删除的结点有左右子树的情况
    这个比较复杂了,我们这里做的是要找到要删除结点的直接前驱(或者直接后驱),把这个直接前驱(或者直接后驱)直接替换要删除的结点。
    在这里插入图片描述
    删除89结点,这个结点刚好左右子树都存在,按照要找的89结点的左子树中的最右边的结点,就是57,然后把57直接替换89结点,如图:
    在这里插入图片描述
    这样看不是很完美,这个就是找到直接的前继,这个直接前继就是比原来的左子树的所有书都打,比右子树的所有都小,所以这样直接替换才很完美,但是还有一种情况,就是89的左子树56没有右子树,这时候89的直接前继就是56,所以这时候也可以直接把65替换到89,但是在程序中就要分开处理了。

代码:

/**
		* @brief  删除二叉树结点
		* @param	
		* @retval 
		*/ 
		static int biTree_deleteNode(struct BiTree_node *node, struct BiTree_node *prev_node)
		{	
			struct BiTree_node *q, *s;
			//只有右子树,需要接左子树
			if(node->bst.left == NULL)  {
				prev_node->bst.right = node->bst.right;
				free(node);
			}else if(node->bst.right == NULL)  {   //只有右子树,只接右子树
				prev_node->bst.left = node->bst.left;
				free(node);
			}else {		
				//左右子树都存在,直接寻找要删除结点的直接前继
				//直接前继就是node结点的左子树的最后边的数据
				s = node->bst.left;
				q = node;
				while(s->bst.right)
				{
					q = s;s = s->bst.right;		//寻找最右边的结点,还要考虑这个结点是否有左子树
				}
				
				//q要保存,这是s的父结点,s的左子树要挂在到q这个结点上
				if(q == node)  //相等的话,就是s=node左子树,已经是直接后继了
				{
					//不改变
				}
				else		//不想等的话,说明node的左子树是有右子树的,q是s的父节点,s是q的右子树,s的左子树要挂在q的右子树上
				{		
					q->bst.right = s->bst.left;
					s->bst.left = node->bst.left;
				}

				//把s挂在prev_node的上,不过需要判断是左子树还是右子树
				if(prev_node->data > s->data)   //左子树
				{
					prev_node->bst.left = s;
				}
				else
				{
					prev_node->bst.right = s;
				}
				s->bst.right = node->bst.right;
				free(node);
			}

			return 0;
		}
	


	/**
		* @brief  查找二叉树数据
		* @param	
		* @retval 
		*/ 
		int biTree_delete(struct BiTree *T, Elemtype data)
		{	
			struct BiTree_node *node = T->root;
			struct BiTree_node *temp = T->root;

			while(node)
			{
				if(node->data == data){
					//要删除的结点
					biTree_deleteNode(node, temp);
				}
				else if(node->data > data) {   //往左子树走
					temp = node;
					node = node->bst.left;
				}
				else  {
					temp = node;
					node = node->bst.right;
				}
			}

			return 0;
		}

删除操作,使用了两个函数来实现,biTree_delete()这个函数是循环查找对应的结点和父节点,这次是利用循环不用递归操作,两个方式都行,biTree_deleteNode()这个函数是删除结点的操作,删除结点中,分别判断了对应的情况,并做对应的处理,特别是有左右子树的情况下,处理比较麻烦,基本思想就是上面说的,仔细推敲就明白了。

二叉排序树总结:
对于二叉排序树的查找,走的就是从根结点到要查找的结点的路径,其比较次数等于给定值的结点在二叉排序树的层数。极端情况,最少为1次,即根结点就是要找的结点,最多也不会超过树的深度。
也就是说,我们希望二叉排序树是比较平衡的,即其深度与完全二叉树相同,均为|log2n+1|,那么查找时间复杂度为O(logn),近似折半查找,我举的例子的树也是不平衡,右重左轻。不平衡的最坏情况就是斜树,查找时间负责度为O(n),这等与顺序查找。

所以我们需要把二叉排序树转化成平衡二叉树。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值