数据结构——二叉树

目录

引言

二叉树

1.二叉树的定义

2.特殊的二叉树

(1) 满二叉树

(2)完全二叉树

3.二叉树的性质

4.二叉树的存储方法

(1)顺序存储

(2)链式存储

5.二叉树的构建与销毁

(1)构建二叉树

(2)销毁二叉树

6.二叉树的遍历

(1)前序遍历

(2)中序遍历

(3)后序遍历

(4)层序遍历

7.二叉树的扩展功能

(1) 求节点个数

(2)求叶子节点个数

(3)求第k层节点个数

(4)查找值为x的节点

(5)求二叉树高度

(6)判断是否为完全二叉树

结束语


引言

在我的博客 数据结构——树的三种表示方法 中,我们学习了一些有关树的知识,今天我们来学习一种特殊的树形结构——二叉树

求点赞收藏评论关注!!!十分感谢!!!

二叉树

1.二叉树的定义

二叉树是由节点组成的树形结构,其中每个节点最多有两个子节点,分别称为“左子节点”和“右子节点”。在二叉树中,根节点没有父节点,而每个非根节点都有一个父节点。叶子节点是没有子节点的节点。

1.二叉树不存在度大于2的结点。

2.二叉树的子树有左右之分,次序不能颠倒,因此二叉树是有序树。

如下图所示:

2.特殊的二叉树

(1) 满二叉树

一棵高度为h,且含有2^h-1个结点的二叉树称为满二叉树。即树中的每层都含有最多的结点

满二叉树有如下几个特点:

叶子只能出现在最下一层。

非叶子结点的度一定是2。

在同样深度的二叉树中,满二叉树的结点个数最多,叶子数最多。

如下图所示:

(2)完全二叉树

完全二叉树是由满二叉树而引出来的

若设二叉树的深度为h,除第 h 层外,其它各层 (1~h-1) 的结点数都达到最大个数,第 h 层所有的结点都连续集中在最左边,这就是完全二叉树。

满二叉树是一种特殊的完全二叉树。

完全二叉树有如下特点:

叶子结点只能出现在最下两层。

最下层的叶子一定集中在左部连续位置。

倒数第二层,若有叶子结点,一定都在右部连续位置。

如果结点度为1,则该结点只有左孩子,即不存在只有右子树的情况。

同样结点数的二叉树,完全二叉树的深度最小。

如下图所示:

3.二叉树的性质

1. 若规定根结点的层数为1,则一棵非空二叉树的第 i 层上最多有 2^i -1 个结点。

2.若规定根节点的层数为1,则深度为 h 的二叉树的最大结点数:

N=1+2+4+…+2^i -1=2^h -1

3.对任何一棵二叉树, 如果度为 0 其叶结点个数为 N0 , 度为 2 的分支结点个数为 N2 , 则有 N0=N2+1。

4.若规定根结点的层数为1,具有n个结点的满二叉树的深度,h= log2(n+1) (ps: log2(n+1)  是log以2 为底,n+1为对数)。

5.对于具有n个结点的完全二叉树,如果按照从上至下从左至右的数组顺序对所有结点从0开始编号,则对 于序号为i的结点有:

1). 若i>0,i位置结点的双亲序号:(i-1)/2;i=0,i为根结点编号,无双亲结点。

2). 若2i+1,左孩子序号:2i+1,2i+1>=n否则无左孩子。

3). 若2i+2,右孩子序号:2i+2,2i+2>=n否则无右孩子。

第三点证明过程如下:

1.假设节点总数为 N ,如果度为0其叶结点个数为 N0 , 度为1的分支结点个数为 N1 ,度为2的分支结点个数为 N2 。

2.结点的总数N可以表示为:N=N0​+N1​+N2 ​。

3.在二叉树中,每个度为1的结点有1个分支,每个度为2的结点有2个分支。因此,二叉树的分支总数B可以表示为:B=N1​+2N2​ 。

4.在二叉树中,除了根结点外,每个结点都有一个分支进入。因此,分支的总数也可以表示为结点总数减去1,即:B=N−1 。

5.公式代入我们可以得到:N1​+2N2​=N0​+N1​+N2​−1 。

6.整理式子可以得出:N0​=N2​+1 。

4.二叉树的存储方法

(1)顺序存储

顺序结构存储就是使用数组来存储,一般使用数组只适合表示完全二叉树,因为不是完全二叉树会有空 间的浪费。而现实中使用中只有堆才会使用数组来存储,堆我们会在后面学习到。

二叉树顺序存储在物理上是一个数组,在逻辑上是一颗二叉树。

(2)链式存储

大多数情况下我们都采用链式存储,此处我们采用链式存储进行数据存储。

typedef struct BinaryTreeNode
{
	DataType data;
	struct BinaryTreeNode* left;
	struct BinaryTreeNode* right;
}BTNode;

5.二叉树的构建与销毁

(1)构建二叉树
// 构建二叉树
BTNode* BinaryTreeCreate(DataType* a, int n, int* pi)
{
	// 如果当前元素是'#',表示该位置是空的,递增索引并返回NULL
	if (a[*pi] == '#')
	{
		(*pi)++;
		return NULL;
	}
	BTNode* root = (BTNode*)malloc(sizeof(BTNode));
	if (root == NULL)
	{
		perror("malloc fail:");
		return NULL;
	}
	root->data = a[(*pi)++];
	root->left = BinaryTreeCreate(a, n, pi);
	root->right = BinaryTreeCreate(a, n, pi);
	return root;
}

这段代码的目的是根据一个给定的数组 a(其中包含二叉树节点的值,其中 '#' 字符表示空节点)和一个索引指针 pi(指向当前正在处理的数组元素的位置)来递归地构建一棵二叉树。

递归思路:

1.检查当前索引 *pi 所指向的数组元素是否为 '#'。如果是,说明该位置应该是一个空节点,函数递增索引 pi 并返回 NULL,表示当前子树的根节点为空。

2.如果当前位置不是空节点,代码将尝试为新的树节点分配内存。

3.在成功分配内存后,代码将当前索引 *pi 所指向的数组元素的值赋给新节点的 data 字段,并递增索引 pi 以准备处理下一个元素。

4.代码递归地调用 BinaryTreeCreate 函数来构建当前节点的左子树和右子树。

(2)销毁二叉树
// 二叉树销毁
void BinaryTreeDestory(BTNode** root)
{
	if (*root == NULL)
	{
		return;
	}
	BinaryTreeDestory(&((*root)->left));
	BinaryTreeDestory(&((*root)->right));
	free(*root);
	*root = NULL;
}

6.二叉树的遍历

二叉树的遍历是二叉树的基本操作之一,它指的是按照某种规则访问二叉树中的所有节点,并且每个节点只被访问一次。

二叉树的遍历方式有多种,其中最常见的有四种:前序遍历(Pre-order Traversal)、中序遍历(In-order Traversal)、后序遍历(Post-order Traversal)和层序遍历(Level-order Traversal)。

(1)前序遍历

前序遍历的访问顺序为:根 左 右 

递归思路:

1.访问根节点

2.前序遍历左子树

3.前序遍历右子树

代码如下:

// 二叉树前序遍历 
void BinaryTreePrevOrder(BTNode* root)
{
	if (root == NULL)
	{
		printf("N");
		return;
	}
	printf("%c", root->data);
	BinaryTreePrevOrder(root->left); 
	BinaryTreePrevOrder(root->right);
}
(2)中序遍历

中序遍历的访问顺序:左 根 右

递归思路:

1.中序遍历左子树

2.访问根节点

3.中序遍历右子树

代码如下:

// 二叉树中序遍历
void BinaryTreeInOrder(BTNode* root)
{
	if (root == NULL)
	{
		printf("N");
		return;
	}
	BinaryTreeInOrder(root->left);
	printf("%c", root->data);
	BinaryTreeInOrder(root->right);
}
(3)后序遍历

后序遍历的访问顺序:左 右 根

递归思路:

1.后序遍历左子树

2.后序遍历右子树

3.访问根节点

代码如下:

// 二叉树后序遍历
void BinaryTreePostOrder(BTNode* root)
{
	if (root == NULL)
	{
		printf("N");
		return;
	}
	BinaryTreePostOrder(root->left);
	BinaryTreePostOrder(root->right);
	printf("%c", root->data);
}
(4)层序遍历

层序遍历的访问顺序:逐层访问

  • 从根节点开始,按层次从上到下、从左到右遍历二叉树

层序遍历通常使用队列来实现:

在二叉树的层序遍历中,我们需要按照层次逐层访问节点,并且保证每一层的节点都按照从左到右的顺序被访问。队列是一种先进先出(FIFO, First In First Out)的数据结构,它允许我们在一端添加元素(队尾),在另一端移除元素(队首)。这种特性与层序遍历的需求非常吻合。

代码如下:

// 层序遍历
void BinaryTreeLevelOrder(BTNode* root)
{
	Queue queue;
	QueueInit(&queue);
	// 如果根节点不为空,则将其加入队列
	if (root)
	{
		QueuePush(&queue, root);
	}
	// 当队列不为空时,继续遍历
	while (!QueueEmpty(&queue))
	{
		BTNode* tmp = QueueFront(&queue);
		printf("%c", tmp->data);
		// 将该节点从队列中移除
		QueuePop(&queue);

		// 如果该节点有左子节点,则将左子节点加入队列  
		if (tmp->left)
		{
			QueuePush(&queue, tmp->left);
		}
		// 如果该节点有右子节点,则将右子节点加入队列  
		if (tmp->right)
		{
			QueuePush(&queue, tmp->right);
		}
	}
	printf("\n");
	QueueDestroy(&queue);
}

层序遍历需要使用我们之前写过的队列,详情可以看看这篇文章:数据结构——链式队列和循环队列 

7.二叉树的扩展功能

(1) 求节点个数

在这里我们使用递归调用来实现求节点个数的功能:

递归调用相加对于给定的二叉树节点,

如果节点为空(NULL),则返回0(表示没有节点)。

否则,返回1(表示当前节点本身)加上左子树和右子树的节点数(递归调用)

代码如下:

// 二叉树节点个数
int BinaryTreeSize(BTNode* root)
{
	if (root == NULL)
	{
		return 0;
	}
	return 1 + BinaryTreeSize(root->left) +
		BinaryTreeSize(root->right);
}
(2)求叶子节点个数

求叶子节点个数同样使用递归实现:

1.对于给定的二叉树节点,如果节点为空(NULL),则返回0(表示没有节点)。

2.如果节点左右孩子都为空,则返回1(表示当前节点为叶子节点)。

3.如果上面的两个return都没有执行,说明该节点既不为空也不是叶子节点,则返回左子树+右子树的叶子节点数(递归调用)。

代码如下:

// 二叉树叶子节点个数
int BinaryTreeLeafSize(BTNode* root)
{
	if (root == NULL)
	{
		return 0;
	}
	if (root->left == NULL && root->right == NULL)
	{
		return 1;
	}
	return BinaryTreeLeafSize(root->left) +
		BinaryTreeLeafSize(root->right);
}
(3)求第k层节点个数

同样是使用递归的思路:

假设根结点为第一层,那么对于第一层,要求的是第k层;对于第二层,需要求的是第k-1层……逐层分解,对于第k层,需求的就是第一层。

1.如果 root 为 NULL,表示当前子树为空,无法再往下遍历,因此返回 0。

2.如果 k 等于 1,这意味着我们要找的是根节点所在的层,也就是第一层。由于根节点一定存在于第一层(如果树不为空),因此返回 1。
3.如果上述都没有执行,说明节点既不为空,也不是第k层,此时递归调用函数,返回该节点的左孩子的k-1层的节点数+右孩子的k-1层的节点数。

代码如下:

// 二叉树第k层节点个数
int BinaryTreeLevelKSize(BTNode* root, int k)
{
	if (root == NULL)
	{
		return 0;
	}
	if (k == 1)
	{
		return 1;
	}
	return BinaryTreeLevelKSize(root->left, k - 1)
		+ BinaryTreeLevelKSize(root->right, k - 1);
}
(4)查找值为x的节点

遍历二叉树,寻找值为x的节点,思路如下:

1.检查传入的根节点 root 是否为 NULL。如果是,说明已经遍历到了树的底部或者树本身就是空的,此时肯定找不到值为 x 的节点,因此直接返回 NULL。

2.如果根节点不为空,则检查根节点的数据 root->data 是否等于要查找的值 x。如果相等,说明找到了目标节点,直接返回当前节点的指针 root。

3.调用函数获取左子树返回的值,如果该值不为空,说明获得了值为x的节点的地址,将该值返回给上一层 如果调用左子树未返回值,再调用函数获取右子树返回的值,如果改值不为空,说明获得了值为x的节点的地址,将该值返回给上一层。

4.若都没找到,则返回NULL。

代码如下:

// 二叉树查找值为x的节点
BTNode* BinaryTreeFind(BTNode* root, DataType x)
{
	if (root == NULL)
	{
		return NULL;
	}
	if (root->data == x)
	{
		return root;
	}
	BTNode* n1 = BinaryTreeFind(root->left, x); 
	BTNode* n2 = BinaryTreeFind(root->right, x);
	if (n1)
	{
		return n1;
	}
	if (n2)
	{
		return n2;
	}
	return NULL;
}
(5)求二叉树高度

求二叉树的高度思路如下:

1.检查传入的根节点root是否为NULL。如果是,则意味着当前子树为空,其高度自然为0。因此,函数返回0。

2.递归计算左子树和右子树的高度。注意:求得的左右子树高度要记录下来,否则会出现重复计算的情况。

代码如下:

// 求树的高度
int BinaryHeight(BTNode* root)
{
	if (root == NULL)
		return 0;
	int leftHeight = BinaryHeight(root->left);
	int rightHeight = BinaryHeight(root->right);
	return leftHeight > rightHeight ? 
		leftHeight + 1 : rightHeight + 1;
}
(6)判断是否为完全二叉树

判断该二叉树是否为完全二叉树同样需要用到队列。思路如下:

通过层序遍历(使用队列)来判断一棵二叉树是否是完全二叉树。完全二叉树的定义是:除了最后一层外,每一层都是完全填满的,并且所有节点都尽可能地向左对齐。在层序遍历的过程中,如果遇到空节点,则后续的所有节点都应该是空节点,否则就不是完全二叉树。

代码如下:

// 判断二叉树是否是完全二叉树
bool BinaryTreeComplete(BTNode* root)
{
	Queue queue;
	QueueInit(&queue);
	// 如果根节点存在,则将其加入队列
	if (root)
	{
		QueuePush(&queue, root);
	}
	while (!QueueEmpty(&queue))
	{
		BTNode* tmp = QueueFront(&queue);
		QueuePop(&queue);

		// 遇到空节点,就退出循环 
		if (tmp==NULL)
		{
			break;
		}
		QueuePush(&queue, tmp->left);
		QueuePush(&queue, tmp->right);
	}
	// 检查队列中是否还有非空节点  
	// 如果队列中还有节点,并且这些节点不是空节点,
	// 则不是完全二叉树
	while (!QueueEmpty(&queue))
	{
		BTNode* tmp = QueueFront(&queue);
		QueuePop(&queue);
		if (tmp)
		{
			QueueDestroy(&queue);
			return false;
		}
	}
	QueueDestroy(&queue);
	return true;
}

结束语

磨磨蹭蹭写了很久,终于是把二叉树写完了。

树的介绍在这里:数据结构——树的三种表示方法

求点赞收藏评论关注!!!

感谢各位大佬的支持!!

评论 18
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值