【数据结构初阶之二叉树】:二叉树相关的性质和经典的习题(用C语言实现,附图详解)

在这里插入图片描述

一、树的概念及结构

1.树的概念

在数据结构中什么是树呢?我们有如下定义和性质:
定义: 树是一种非线性的数据结构,它是由n(n>=0)个有限结点组成一个具有层次关系的集合。把它叫做树是因为它看起来像一棵倒挂的树,也就是说它是根朝上,而叶朝下的。
性质:
1、 有一个特殊的结点,称为根结点,根节点没有前驱结点。
2、 除根节点外,其余结点被分成M(M>0)个互不相交的集合T1、T2、……、Tm,其中每一个集合Ti(1<= i<= m)又是一棵结构与树类似的子树。每棵子树的根结点有且只有一个前驱,可以有0个或多个后继结点。
3、 因此,树是递归定义的。
注意: 树形结构中,子树之间不能有交集,否则就不是树形结构
在这里插入图片描述

2.树当中相关的概念

在这里插入图片描述

  • 节点的度: 一个节点含有的子树的个数称为该节点的度; 如上图:B结点的度为3。
  • 叶节点或终端节点: 度为0的节点称为叶节点; 如上图:K、J、F、L、O、P节点为叶节点。
  • 双亲节点或父节点: 若一个节点含有子节点,则这个节点称为其子节点的父节点; 如上图:A是B的父节点。
  • 孩子节点或子节点: 一个节点含有的子树的根节点称为该节点的子节点; 如上图:B是A的孩子节点。
  • 兄弟节点: 具有相同父节点的节点互称为兄弟节点; 如上图:B、C是兄弟节点。
  • 树的度: 一棵树中,最大的节点的度称为树的度; 如上图:树的度为3。
  • 节点的层次: 从根开始定义起,根为第1层,根的子节点为第2层,以此类推。
  • 树的高度或深度: 树中节点的最大层次; 如上图:树的高度为5
  • 堂兄弟节点: 双亲在同一层的节点互为堂兄弟;如上图:M、N互为兄弟节点。
  • 节点的祖先: 从根到该节点所经分支上的所有节点;如上图:A是所有节点的祖先。
  • 子孙: 以某节点为根的子树中任一节点都称为该节点的子孙。如上图:所有节点都是A的子孙。

3. 树的表示

树结构相对线性表就比较复杂了,要存储表示起来就比较麻烦了,既然保存值域,也要保存结点和结点之间的关系,实际中树有很多种表示方式如:双亲表示法,孩子表示法、孩子双亲表示法以及孩子兄弟表示法等。
我们这里就简单的了解其中最常用的孩子兄弟表示法:

typedef int DataType;
struct Node
{
struct Node* leftChild1; // 第一个孩子结点
struct Node* rightBrother; // 指向其下一个兄弟结点
DataType data; // 结点中的数据域
};

在这里插入图片描述

二、二叉树的概念及结构

1.二叉树的概念

二叉树是n个有限元素的集合,该集合或者为空、或者由一个称为根(root)的元素及两个不相交的、被分别称为左子树和右子树的二叉树组成,是有序树。
当集合为空时,称该二叉树为空二叉树。在二叉树中,一个元素也称作一个结点。
在这里插入图片描述
根据上图可以分析出:

  1. 二叉树不存在度大于2的结点
  2. 比特科技2. 二叉树的子树有左右之分,次序不能颠倒,因此二叉树是有序树
  3. 对于任意的二叉树都是由以下几种情况复合而成的:空树、只有根结点、只有左子树、只有右子树、左右子树都存在。

2.特殊的二叉树

  • 满二叉树: 一个二叉树,如果每一个层的结点数都达到最大值,则这个二叉树就是满二叉树。也就是说,如果一个二叉树的层数为K,且结点总数是 ,则它就是满二叉树。
  • 完全二叉树: 完全二叉树是效率很高的数据结构,完全二叉树是由满二叉树而引出来的。对于深度为K的,有n个结点的二叉树,当且仅当其每一个结点都与深度为K的满二叉树中编号从1至n的结点一一对应时称之为完全二叉树。 要注意的是满二叉树是一种特殊的完全二叉树。
    在这里插入图片描述

3.二叉树的性质

  • 若规定根节点的层数为1,则一棵非空二叉树的第 i 层上最多有2(i-1) 个结点.
  • 若规定根节点的层数为1,则深度为h的二叉树的最大结点数是2h - 1
  • 对任何一棵二叉树, 如果度为0其叶结点个数为N0 , 度为2的分支结点个数为N2 ,则有N0 =N2 +1
  • 若规定根节点的层数为1,具有n个结点的满二叉树的深度,h= log2(n+1) (ps: 是log以2为底,n+1为对数)
  • 对于具有n个结点的完全二叉树,如果按照从上至下从左至右的数组顺序对所有节点从0开始编号,则对于序号为i的结点有:
    1、若i>0,i位置节点的双亲序号:(i-1)/2;i=0,i为根节点编号,无双亲节点
    2、若2i+1<n,左孩子序号:2i+1,2i+1>=n否则无左孩子。
    3、若2i+2<n,右孩子序号:2i+2,2i+2>=n否则无右孩子。

4.二叉树的存储结构

二叉树一般可以使用两种结构存储,一种顺序结构,一种链式结构。

  • 顺序存储:
    顺序结构存储就是使用数组来存储,一般使用数组只适合表示完全二叉树,因为不是完全二叉树会有空间的浪费。而现实中使用中只有堆才会使用数组来存储,这样二叉树顺序存储在物理上是一个数组,在逻辑上是一颗二叉树。
    关于堆在这篇博客中详细分析了,点击跳转即可–>【堆的详细分析】

在这里插入图片描述

  • 链式存储
    二叉树的链式存储结构是指,用链表来表示一棵二叉树,即用链来指示元素的逻辑关系。 通常的方法是链表中每个结点由三个域组成,数据域和左右指针域,左右指针分别用来给出该结点左孩子和右孩子所在的链结点的存储地址,下面我们主要是用这种方法来学习二叉树
    在这里插入图片描述

三、二叉树链式结构的实现

1.二叉树的创建

在学习二叉树的基本操作前,需先要创建一棵二叉树,然后才能学习其相关的基本操作;但是用C语言实现是非常麻烦和不容易理解的,所以下面我们都是手动来创建一颗简单树,来学习二叉树的精华。
和其它数据结构一样,创建二叉树得先创建一个二叉树类型的数据,代码如下:

typedef char BTDataType; //对char类型重新起个名字叫BTDataType

//创建一个二叉树类型
typedef struct BinaryTreeNode
{
	BTDataType data;
	struct BinaryTreeNode* left;
	struct BinaryTreeNode* right;
}BTNode;

在这里插入图片描述
创建代码如下:

typedef char BTDataType; //对char类型重新起个名字叫BTDataType

//创建一个二叉树类型
typedef struct BinaryTreeNode
{
	BTDataType data;
	struct BinaryTreeNode* left;
	struct BinaryTreeNode* right;
}BTNode;

//开辟一个结点函数
BTNode* BuyNode(BTDataType x)
{
	BTNode* tmp = (BTNode*)malloc(sizeof(BTNode));
	if (tmp == NULL)
	{
		perror("erron ");
		exit(-1);
	}
	tmp->data = x;
	tmp->left = NULL;
	tmp->right = NULL;
	return tmp;

}

//创建一个树的函数
BTNode* CreatBinaryTree()
{
    //先依次开辟多个结点
	BTNode* a = BuyNode('A');
	BTNode* b = BuyNode('B');
	BTNode* c = BuyNode('C');
	BTNode* d = BuyNode('D');
	BTNode* e = BuyNode('E');
	BTNode* f = BuyNode('F');
	BTNode* g = BuyNode('G');
    
    //然后把结点连接成一棵树
	a->left = b;
	a->right = c;
	b->left = d;
	c->left = e;
	c->right = f;
	
	return a;
}

int main()
{
    //创建一棵树,用变量root来接收树的根
	BTNode* root = CreatBinaryTree();
}

问题来了,我们创建一棵二叉树有什么价值呢?要研究二叉树的什么呢?下面我们慢慢分析关于二叉树的经典问题。

我们先来分析二叉树的前中后遍历,关于前中后遍历的定义如下:

  1. 前序遍历(Preorder Traversal 亦称先序遍历)——访问根结点的操作发生在遍历其左右子树之前。
  2. 中序遍历(Inorder Traversal)——访问根结点的操作发生在遍历其左右子树之中。
  3. 后序遍历(Postorder Traversal)——访问根结点的操作发生在遍历其左右子树之后。

二叉树的前中后遍历的结果如下图分析:
在这里插入图片描述

2.二叉树的前序遍历

我们了解这棵树的前序遍历,那怎么用代码实现出来呢?
代码如下:

//前序遍历
void PrevOrder(BTNode* root)
{
	if (root == NULL){
		printf("NULL ");
		return;
	}
	//先访问根节点
	printf("%c ", root->data);
	//再访问左右子树
	PrevOrder(root->left);
	PrevOrder(root->right);
}

int main()
{
    //手动连接结点创建一棵树
	BTNode* root = CreatBinaryTree();
	//前序遍历
	PrevOrder(root);
	printf("n");
}

可能大家一看,这是个递归呀!它是这么做到前序遍历的呢?怎么看都看不出来呀。其实这和C语言的函数栈帧这块知识点连续起来了,如果还没有了解函数栈帧这块可以先看看我的这两篇博客,里面介绍了递归和函数栈帧,点击即可跳转==> 【递归的快速掌握】 【函数栈帧的创建与销毁】
好了,那既然看得有点迷迷糊糊的,拿我们来画一下递归的展开图,图片如下:

3.二叉树的中序遍历

这个二叉树的中序遍历是先访问左子树,再访问根结点,最后再访问右子树
代码实现如下:

//中序遍历
void InOrder(BTNode* root)
{
	if (root == NULL)
	{
		printf("NULL ");
		return;
	}
	//先访问左子树
	InOrder(root->left);
	//再访问根
	printf("%c ", root->data);
	//再访问右子树
	InOrder(root->right);
}

int main()
{
    //手动连接结点创建一棵树
	BTNode* root = CreatBinaryTree();
	//中序遍历
	InOrder(root);
	printf("n");
}

这个和前序遍历都是用递归实现的,我们脑海里可能构想不出来递归的全部路径,我们还是画出递归展开图来分析,图片如下:
在这里插入图片描述

4.二叉树的后序遍历

二叉树的后序遍历是先访问左子树,再访右子树,最后访问根。
代码实现如下:

//后序遍历
void PostOrder(BTNode* root)
{
	if (root == NULL)
	{
		printf("NULL ");
		return;
	}
	//先访问左子树
	PostOrder(root->left);
	//再访问右子树
	PostOrder(root->right);
	//再访问根
	printf("%c ", root->data);
}

int main()
{
    //手动连接结点创建一棵树
	BTNode* root = CreatBinaryTree();
	//后序遍历
	PostOrder(root);
	printf("n");
}

5.二叉树的销毁

这个二叉树销毁我们要注意不能先销毁根,再销毁左右子树,因为先释放根就找不到左右子树的地址了。所以我们先销毁左右子树最后再销毁根,和后序遍历相似。
代码如下:

//二叉树的销毁
void DestroyTree(BTNode* root)
{
	if (root == NULL)
	{
		return;
	}
	//先释放左右子树再释放根
	DestroyTree(root->left);
	DestroyTree(root->right);
	free(root);
}

int main()
{
    //手动连接结点创建一棵树
	BTNode* root = CreatBinaryTree();
	
    //前序遍历
	PrevOrder(root);
	printf("n");
	//中序遍历
	InOrder(root);
	printf("n");
	//后序遍历
	PostOrder(root);
	printf("n");
	
    //销毁二叉树
    DestroyTree(root);
}

二叉树的销毁和后序遍历是差不多的,都是先处理左右子树再处理根,感觉不够理解的小伙伴可以按我前面前序和中序遍历一样把递归展开图一个一个地画出来,能大大增加我们对递归的理解。

四、二叉树的节点和高度问题

1.求二叉树节点个数

要想求二叉树结点的个数我们还是转化成子问题去解决。求二叉树总的结点个数,那我可以先求左右子树的结点个数再加上1自己就是总的个数了;而左右子树又可以细分左右子树一层层的递归下去再返回来就可以了。
代码如下:

int  BinaryTreeSize(BTNode* root)
{
	if (root == NULL)
	{
		return 0 ;
	}
	//先求左右子树的个树再加上根结点就是总的结点个数
	return BinaryTreeSize(root->left) + BinaryTreeSize(root->right) + 1;
}

int main()
{
    //手动连接节点创建一棵树
	BTNode* root = CreatBinaryTree();
	
    //统计二叉树的结点个数
	int ret=BinaryTreeSize(root);
	printf("二叉树结点的个数为%dn", ret);
}

我们如果看起来感觉还是心里不踏实,总感觉不对劲,我们可以用老方法画递归展开图来分析分析,递归展开图如下:
在这里插入图片描述

2.求二叉树叶子节点个数

这个求的是叶子节点个数,而叶子节点的定义是左右子树都为空才是叶子节点,所以我们还是用递归分而治之的思想,统计左子树的叶子结点个数加上右子树的叶子结点个数即可,这样左右子树分别递归下去,最后返回的就是总的叶子结点个数。
代码如下:

//统计叶子结点的个数
int BinaryTreeLeafSize(BTNode* root)
{
	if (root == NULL)
	{
		return 0;
	}
	//当左右子树都为空才是叶子,就返回1
	if (root->left == NULL && root->right == NULL)
	{
		return 1;
	}
	//分别统计左右子树的叶子结点个数,加起来就是总数
	return BinaryTreeLeafSize(root->left) + BinaryTreeLeafSize(root->right);
}

递归展开图如下:
在这里插入图片描述

3.求二叉树第k层节点个数

这个也是要用递归的分而治之思想,统计第K层结点个数,我们先统计K - 1层的结点个数,一直递归分下去到k等于1的时候,结点如果不为空就是1个结点。
比如求第4层结点的个数,我们可以转化为求第3层左子树结点的个数 + 右子树结点的个数;而第3层又可以转化为第二层左右子树的结点个数,一直递归下去。
代码实现如下:

// 二叉树第k层节点个数
int BinaryTreeLevelKSize(BTNode* root, int k)
{
	if (root == NULL)
	{
		return 0;
	}
	//当结点不为空,且k==1结点个数就为1
	if (k == 1)
	{
		return 1;
	}
    //转化为求第k-1层的左右子树结点个数,一直分治下去,直到遇到NULL或者k==1
	return BinaryTreeLevelKSize(root->left, k - 1) 
		+ BinaryTreeLevelKSize(root->right, k - 1);
}

递归展开图如下:
在这里插入图片描述

4.求二叉树的高度

这个还是采用递归的分而治之思想,我们先求出左子树的高度,再求出右子树的高度,然后二者相比较,大的加1就是二叉树的高度;而左右子树再递归下去求高度,最后就可以得到二叉树的高度。代码实现如下:

//求二叉树的高度
int BinaryTreeDepth(BTNode* root)
{
	if (root == NULL)
	{
		return  0;
	}
	//先求出左右子树的高度
	int leftDepth = BinaryTreeDepth(root->left);
	int rightDepth = BinaryTreeDepth(root->right);

	//比较左右子树的高度,大的加1就是树的高度
	return leftDepth > rightDepth ? leftDepth + 1 : rightDepth + 1;
}

5. 二叉树中查找值为x的节点

题目意思是在二叉树中找和一个X值相等的结点,如果找到了就返回该结点的地址,如果没有找到就返回空指针NULL;
我们还是用分而治之来解决,先在左子树中找,如果找到了就返回该结点地址,如果左子树没有找到那就去右子树里面找,找到了就返回地址。如果左右子树都没有找到就返回空指针NULL。
代码实现如下:

//在二叉树中找值为X的结点
BTNode* BinaryTreeFind(BTNode* root, BTDataType x)
{
	if (root == NULL)
	{
		return NULL;
	}
	//找到了就直接返回该结点地址
	if (root->data == x)
	{
		return root;
	}
	//先在左子树中找
	BTNode* left = BinaryTreeFind(root->left,x);
	if (left != NULL)
	{
		return left;
	}
     //左子树中没有找到就往右子树中找
	BTNode* right = BinaryTreeFind(root->right, x);
	if (right != NULL)
	{
		return right;
	}
	//如果左右子树都没有找到就返回空指针
	return NULL;
}

6.二叉树的层序遍历

我们学了二叉树的前、中、后序遍历,还剩下最后一个就是层序遍历;顾名思义就是要一层层地往下面遍历,我们可以利用前面学过的队列来解决。
第一步,先判断二叉树是否为空,为空就直接返回
第二步,把二叉树的根入队列,然后取队头的元素,再出队列
第三步,判断队头元素的左右子树是否为空,不为空就入队列,为空就不入队列
第四步,判断队列是否为空,不为空就继续取队头的元素,再出队列,循环第三、四步即可,这样就做到了一层层遍历
图片分析如下:
在这里插入图片描述
因为C语言中没有队列,所以我们需要手动实现一个队列,在前面的博客中详细地介绍并实现了队列,点击即可跳转==>【队列的模拟实现】,所以下面代码中直接引用即可,
代码实现如下:

void BinaryTreeOrder(BTNode* root)
{
	if (root == NULL)
	{
		return;
	}
	Queue q;
	QueueInit(&q);
	//把根入队列
	QueuePush(&q, root);

	while (!QueueEmpty(&q))
	{
		//取队头的元素
		BTNode* front = QueueFront(&q);
		printf("%c", front->data);
		QueuePop(&q);  //出队列
        
		if (front->left != NULL)
		{
			QueuePush(&q, front->left);
		}
		if (front->right != NULL)
		{
			QueuePush(&q, front->right);
		}
	}
	//销毁队列
	QueueDestroy(&q);

}

7.判断二叉树是否是完全二叉树

我们知道完全二叉树最后一层的叶子结点一定都是连续的,如果叶子结点没有连续中间有空指针的话就不是完全二叉树了;所以我们利用这个特点和利用队列解决,
首先我们把不为空的根先入队列,然后取队头的元素,接着出队列
然后不管队头的元素左右子树是否为空我们都入队列;
接着就不断取队头的元素,取到空指针就不再入队列,判断队列剩下的元素是否都为空指针,如果都为空指针代表叶子结点都是连续的,是完全二叉树;如果不都为空指针,代表叶子结点是不连续的不是完全二叉树。
在这里插入图片描述
代码实现如下:

//判断二叉树是否为完全二叉树
bool BinaryTreeComplete(BTNode* root)
{
	if (root == NULL)
	{
		return false;
	}
	Queue q;
	QueueInit(&q);
	QueuePush(&q, root);

	while (!QueueEmpty(&q))
	{
		BTNode* front = QueueFront(&q);
		QueuePop(&q);
		//如果队头元素不为空就入队头元素的左右子树
		//如果队头元素为空就退出
		if (front != NULL)
		{
			QueuePush(&q, front->left);
			QueuePush(&q, front->right);
		}
		else
		{
			break;
		}
	}

	//判断队列剩下的元素是否都为空指针
	//都为空指针就是完全二叉树
	while (!QueueEmpty(&q))
	{
		BTNode* front = QueueFront(&q);
		if (front != NULL)
		{
			QueueDestroy(&q);
			return false;
		}
		QueuePop(&q);
	}

	return true;
}

本图文内容来源于网友网络收集整理提供,作为学习参考使用,版权属于原作者。
THE END
分享
二维码
< <上一篇
下一篇>>