C语言二叉树详解

一. 树概念及结构

1.1 树的概念

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

1.2 树的相关概念

在这里插入图片描述

  1. 节点的度:一个节点含有的子树的个数称为该节点的度; 如上图:A的为6
  2. 叶节点或终端节点:度为0的节点称为叶节点; 如上图:B、C、H、I…等节点为叶节点
  3. 非终端节点或分支节点:度不为0的节点; 如上图:D、E、F、G…等节点为分支节点
  4. 双亲节点或父节点:若一个节点含有子节点,则这个节点称为其子节点的父节点; 如上图:A是B的父节点
  5. 孩子节点或子节点:一个节点含有的子树的根节点称为该节点的子节点; 如上图:B是A的孩子节点
  6. 兄弟节点:具有相同父节点的节点互称为兄弟节点; 如上图:B、C是兄弟节点
  7. 树的度:一棵树中,最大的节点的度称为树的度; 如上图:树的度为6
  8. 节点的层次:从根开始定义起,根为第1层,根的子节点为第2层,以此类推;
  9. 树的高度或深度:树中节点的最大层次; 如上图:树的高度为4
  10. 堂兄弟节点:双亲在同一层的节点互为堂兄弟;如上图:H、I互为兄弟节点
  11. 节点的祖先:从根到该节点所经分支上的所有节点;如上图:A是所有节点的祖先
  12. 子孙:以某节点为根的子树中任一节点都称为该节点的子孙。如上图:所有节点都是A的子孙
  13. 森林:由m(m>0)棵互不相交的树的集合称为森林;

1.3 树的表示

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

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

图解:
在这里插入图片描述

1.4 树的应用

树比较常见的应用就是用目录的结构,比如Linux的树状目录结构;
在这里插入图片描述

二. 二叉树概念及结构

2.1 二叉树的概念

一棵二叉树是结点的一个有限集合,该集合为空或者由一个根节点加上两棵别称为左子树和右子树的二叉树组成
在这里插入图片描述
通过概念和图解我们可以发现:

  1. 二叉树不存在度大于2的结点
  2. 二叉树的子树有左右之分,次序不能颠倒,因此二叉树是有序树

注意:对于任意的二叉树都是由以下几种情况复合而成的
在这里插入图片描述
现实生活中的二叉树图片:
在这里插入图片描述

2.2 特殊的二叉树

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

在这里插入图片描述
在这里插入图片描述

2.3 二叉树的性质

  1. 若规定根节点的层数为1,则一棵非空二叉树的第i层上最多有2^(i-1)个结点.
  2. 若规定根节点的层数为1,则深度为h的二叉树的最大结点数是2^h - 1 .
  3. 对任何一棵二叉树, 如果度为0其叶结点个数为N0, 度为2的分支结点个数为N2 ,则有N0=N2+1
  4. 若规定根节点的层数为1,具有n个结点的满二叉树的深度,h= log2(n+1)
  5. 对于具有n个结点的完全二叉树,如果按照从上至下从左至右的数组顺序对所有节点从0开始编号,则对于序号为i的结点有:

若i>0,i位置节点的双亲序号:(i-1)/2;i=0,i为根节点编号,无双亲节点
若2i+1<n,左孩子序号:2i+1,2i+1>=n否则无左孩子
若2i+2<n,右孩子序号:2i+2,2i+2>=n否则无右孩子

2.4 二叉树的存储结构

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

  1. 顺序存储
    顺序结构存储就是使用数组来存储,一般使用数组只适合表示完全二叉树,因为不是完全二叉树会有空间的浪费。而现实中使用中只有堆才会使用数组来存储,关于堆我们后面会专门讲解。二叉树顺序存储在物理上是一个数组,在逻辑上是一颗二叉树。
    在这里插入图片描述
  2. 链式存储
    二叉树的链式存储结构是指,用链表来表示一棵二叉树,即用链来指示元素的逻辑关系。 通常的方法是链表中每个结点由三个域组成,数据域和左右指针域,左右指针分别用来给出该结点左孩子和右孩子所在的链结点的存储地址 。
typedef int BTDataType;
// 二叉链
struct BinaryTreeNode
{
	struct BinTreeNode* pLeft; // 指向当前节点左孩子
	struct BinTreeNode* pRight; // 指向当前节点右孩子
 	BTDataType data; // 当前节点值域
}

三. 二叉树的顺序结构及其实现(堆的实现)

3.1 二叉树的顺序结构

普通的二叉树是不适合用数组来存储的,因为可能会存在大量的空间浪费。而完全二叉树更适合使用顺序结构存储。现实中我们通常把堆(一种二叉树)使用顺序结构的数组来存储,需要注意的是这里的堆和操作系统虚拟进程地址空间中的堆是两回事,一个是数据结构,一个是操作系统中管理内存的一块区域分段。

在这里插入图片描述
堆的性质:

  1. 堆中某个节点的值总是不大于或不小于其父节点的值;
  2. 堆总是一棵完全二叉树。

在这里插入图片描述
在这里插入图片描述

3.2 堆的实现(以大堆为例)

声明一下,堆的实现我们不会先讲解如何建堆,而是先讲解调整算法,因为建堆时会用到调整算法。
还有两条性质:

  1. 已知父节点下标parent,要找找到其左孩子child:child=parent*2+1;
  2. 已知孩子结点,找其父亲结点:parent=(child-1)/2;

3.2.1 堆的结构声明

//堆的结构声明
typedef int HPDataType;
typedef struct Heap
{
	HPDataType* a;
	size_t size;//已经存储的数据个数
	size_t capacity;//容量
}Heap;

3.2.2 堆的向下调整算法(建堆会用到)

首先再次声明:堆的实现中,会多次交换两个数据,这里我们将其封装成一个函数:

//交换两个数据
void swap(HPDataType* e1, HPDataType* e2)
{
	HPDataType tmp = *e1;
	*e1 = *e2;
	*e2 = tmp;
}

向下调整的思路:

  1. 首先找到父节点parent的孩子结点child
  2. 找到左右孩子中值较大的那一个与parent比较,如果值大于parent则交换。
  3. 重复上述过程。

图解:
在这里插入图片描述
代码实现:

//从上往下调整
void AdjustDown(HPDataType* a, size_t size, size_t parent)
{
	size_t child = (parent * 2) + 1;//找到左孩子
	while (child < size)
	{
		if (child + 1 < size && a[child] < a[child + 1])//找两个孩子中大的那一个
		{
			child = child + 1;
		}
		if (a[parent] < a[child])//满足条件则交换
		{
			swap(&a[child], &a[parent]);
			parent = child;
			child = (parent * 2) + 1;
		}
		else
		{
			break;
		}
	}
}

3.2.3 堆的向上调整算法

思路:

  1. 先找到child的父亲节点
  2. 比较两者值的大小,如果满足条件则交换
  3. 重复该过程

图解:
在这里插入图片描述
代码实现:

//向上调整
void AdjustUp(HPDataType* a, size_t child)
{
	size_t parent = (child - 1) / 2;//找父亲结点
	while (child > 0)
	{

		if (a[parent] < a[child])//满足条件则交换
		{
			swap(&a[child], &a[parent]);
			child = parent;
			parent = (child - 1) / 2;
		}
		else
		{
			break;
		}
	}
}

3.2.4 建堆算法

思路:

  1. 先将数组元素全部导入至堆中
  2. 从最后一个结点的父亲结点开始,依次向上调整,其中调整的算法是向下调整算法

现在假设有数组arr={2,7,26,25,19,17,1,90,3,36};
我们将其构建成大堆,请看图解
图解:
在这里插入图片描述
动图来自算法可视化网站VisuAlgo:https://visualgo.net/zh
注:本篇博客的可视化算法都是来自该网站。

代码实现:

void HeapCreate(Heap* php, HPDataType* a, int n)
{
	assert(php);
	php->a = (HPDataType*)malloc(sizeof(HPDataType) * n);
	if (php->a == NULL)
	{
		perror("malloc fail");
		exit(-1);
	}
	memcpy(php->a, a, sizeof(HPDataType) * n);//拷贝数组
	php->size = php->capacity = n;

	// 从下往上建堆
	for (int i = (n - 1 - 1) / 2; i >= 0; i--)
	{
		AdjustDown(php->a, n, i);//才用向下调整算法
	}
}

3.2.5 堆的完整代码

Heap.h

//头文件包含
#include <stdio.h>
#include <stdlib.h>
#include <assert.h>
#include <string.h>
#include <stdbool.h>

//结构声明
typedef int HPDataType;
typedef struct Heap
{
	HPDataType* a;
	size_t size;
	size_t capacity;
}Heap;

//函数声明
// 堆的构建
void HeapCreate(Heap* hp, HPDataType* a, int n);
void HeapInit(Heap* php);
//堆的初始化
void HeapInit(Heap* php);

// 堆的销毁
void HeapDestory(Heap* php);

//堆的插入
void HeapPush(Heap* php, HPDataType n);

//堆的删除(删头)
void HeapPop(Heap* php);

//返回堆顶元素
HPDataType HeapTop(Heap* php);

//返回堆的数据个数
size_t HeapSize(Heap* php);

//判断堆是否为空
bool HeapEmpty(Heap* php);

Heap.c

#include "Heap.h"

// 堆的构建
void AdjustDown(HPDataType* php, size_t size, size_t parent);
void HeapCreate(Heap* php, HPDataType* a, int n)
{
	assert(php);
	php->a = (HPDataType*)malloc(sizeof(HPDataType) * n);
	if (php->a == NULL)
	{
		perror("malloc fail");
		exit(-1);
	}
	memcpy(php->a, a, sizeof(HPDataType) * n);
	php->size = php->capacity = n;

	// 从下往上建堆
	for (int i = (n - 1 - 1) / 2; i >= 0; i--)
	{
		AdjustDown(php->a, n, i);
	}
}
//堆的初始化
void HeapInit(Heap* php)
{
	assert(php);
	php->a = NULL;
	php->capacity = php->size = 0;
}

// 堆的销毁
void HeapDestory(Heap* php)
{
	assert(php);
	free(php->a);
	php->capacity = php->size = 0;
}

//交换两个数据
void swap(HPDataType* e1, HPDataType* e2)
{
	HPDataType tmp = *e1;
	*e1 = *e2;
	*e2 = tmp;
}

//向上调整,保证堆的成立
void AdjustUp(HPDataType* a, size_t child)
{
	size_t parent = (child - 1) / 2;
	while (child > 0)
	{

		if (a[parent] < a[child])
		{
			swap(&a[child], &a[parent]);
			child = parent;
			parent = (child - 1) / 2;
		}
		else
		{
			break;
		}
	}
}
//堆的插入
void HeapPush(Heap* php, HPDataType n)
{
	assert(php);
	//如果堆为空或者满了就扩容
	if (php->capacity == php->size)
	{
		size_t newCapacity = php->capacity == 0 ? 4 : 2 * php->capacity;
		HPDataType* tmp = (HPDataType*)realloc(php->a, sizeof(HPDataType) * newCapacity);
		if (tmp == NULL)
		{
			perror("realloc failed");
			exit(-1);
		}
		php->a = tmp;
		php->capacity = newCapacity;
	}
	php->a[php->size] = n;
	php->size++;
	AdjustUp(php->a, php->size - 1);
}

//从上往下调整
void AdjustDown(HPDataType* a, size_t size, size_t parent)
{
	size_t child = (parent * 2) + 1;
	while (child < size)
	{
		if (child + 1 < size && a[child] < a[child + 1])
		{
			child = child + 1;
		}
		if (a[parent] < a[child])
		{
			swap(&a[child], &a[parent]);
			parent = child;
			child = (parent * 2) + 1;
		}
		else
		{
			break;
		}
	}
}
//堆的删除(删头)
void HeapPop(Heap* php)
{
	assert(php);
	assert(php->size > 0);
	swap(&php->a[0], &php->a[php->size - 1]);
	php->size--;
	AdjustDown(php->a, php->size, 0);
}

//返回堆顶元素
HPDataType HeapTop(Heap* php)
{
	assert(php);
	assert(php->size > 0);
	return php->a[0];
}


//返回堆的数据个数
size_t HeapSize(Heap* php)
{
	assert(php);
	return php->size;
}


//判断堆是否为空
bool HeapEmpty(Heap* php)
{
	assert(php);
	return HeapSize(php) == 0;
}

3.3 堆的应用

  1. 堆排序(之后我们讲排序算法时一起讲)
  2. TOP-K问题:求数据结合中前K个最大的元素或者最小的元素,一般情况下数据量都比较大。
    比如:专业前10名、世界500强、富豪榜、游戏中前100的活跃玩家等。
    对于Top-K问题,能想到的最简单直接的方式就是排序,但是:如果数据量非常大,排序就不太可取了(可能
    数据都不能一下子全部加载到内存中)。最佳的方式就是用堆来解决,基本思路如下:1. 用数据集合中前K个元素来建堆,求前k个最大的元素,则建小堆;若求前k个最小的元素,则建大堆。2. 用剩余的N-K个元素依次与堆顶元素来比较,不满足则替换堆顶元素;将剩余N-K个元素依次与堆顶元素比完之后,堆中剩余的K个元素就是所求的前K个最小或者最大的元素。

四. 二叉树链式结构的实现

4.1 前置说明

在学习二叉树的基本操作前,需先要创建一棵二叉树,然后才能学习其相关的基本操作。由于我们现在对二
叉树结构掌握还不够深入,为了降低大家学习成本,此处手动快速创建一棵简单的二叉树,快速进入二叉树
操作学习,等二叉树结构了解的差不多时,我们反过头再来研究二叉树真正的创建方式。
具体操作的代码如下:

#include <stdio.h>
#include <stdlib.h>
#include <assert.h>
typedef int BTDataType;

typedef struct BinaryTreeNode
{
	BTDataType data;
	struct BinaryTreeNode* left;
	struct BinaryTreeNode* right;
}BTNode;
//申请一个新结点
BTNode* BuyNode(BTDataType x)
{
	BTNode* newNode = (BTNode*)malloc(sizeof(BTNode));
	if (newNode == NULL)
	{
		perror("malloc failed");
		exit(-1);
	}
	newNode->data = x;
	newNode->left = newNode->right = NULL;
	return newNode;
}
void test1()
{
	BTNode* root = BuyNode('A');
	BTNode* n2 = BuyNode('B');
	BTNode* n3 = BuyNode('C');
	BTNode* n4 = BuyNode('D');
	BTNode* n5 = BuyNode('E');
	BTNode* n6 = BuyNode('F');
	root->left = n2;
	root->right = n3;
	n2->left = n4;
	n2->right = n5;
	n3->right = n6;
}
int main()
{
	test1();
	return 0;
}

有了以上代码,我们就能构建出如下的一颗二叉树:
![在这里插入图片描述](https://img-blog.csdnimg.cn/6648f943dc294abfba48c15e0ce7ec10.png

接下来我们就将通过在这棵树来学习二叉树的遍历

4.2 二叉树的遍历

学习二叉树结构,最简单的方式就是遍历。所谓二叉树遍历(Traversal)是按照某种特定的规则,依次对二叉树中的节点进行相应的操作,并且每个节点只操作一次。访问结点所做的操作依赖于具体的应用问题。 遍历是二叉树上最重要的运算之一,也是二叉树上进行其它运算的基础。
按照规则,二叉树的遍历有:前序/中序/后序的递归结构遍历:

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

4.2.1 前序遍历

动画演示:
在这里插入图片描述
递归图
在这里插入图片描述

代码实现:

// 二叉树前序遍历 
void BinaryTreePrevOrder(BTNode* root)
{
	if (root == NULL)
	{
		printf("NULL ");//不想打印NULL的话可以去掉
		return;
	}
	printf("%c ", root->data);
	BinaryTreePrevOrder(root->left);
	BinaryTreePrevOrder(root->right);
}

运行效果:
在这里插入图片描述

4.2.2 中序遍历和后序遍历

中序遍历
动画演示:
在这里插入图片描述
代码实现:

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

运行效果:
在这里插入图片描述

后序遍历代码:

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

运行效果:
在这里插入图片描述

4.2.3 层序遍历

顾名思义,层序遍历就是一层一层的遍历。层序遍历需要用到队列这个数据结构,该结构在我之前的博客有讲到
思路如下:

  1. 如果根节点不为空则入队列,此时队列要么为空,要么有一个根节点
  2. 在队列不为空的条件下,打印队头数据,然后在将其出队并且将对头数据的左右子树入队
  3. 重复上述操作
    在这里插入图片描述
    代码实现:
// 层序遍历
void BinaryTreeLevelOrder(BTNode* root)
{
	Queue q;
	QueueInit(&q);
	if (root != NULL)
		QueuePush(&q, root);
	while (!QueueEmpty(&q))
	{
		BTNode* front = QueueFront(&q);
		printf("%c ", front->data);
		//出队并且将左右子树入队
		QueuePop(&q);
		if (front->left)
		{
			QueuePush(&q, front->left);
		}

		if (front->right)
		{
			QueuePush(&q, front->right);
		}
	}
	printf("\n");

	QueueDestroy(&q);
}

运行效果:
在这里插入图片描述

4.3 二叉树的构建与销毁

4.3.1 二叉树的构建

通过前序遍历的数组"ABD##E#H##CF##"构建二叉树
这一部分需要注意的就是数组数据的存放顺序需要按照前序遍历的顺序来存放,其中#表示为空
思路和前序遍历差不多,只是把打印换成了创建二叉树而以,直接上代码

//通过前序遍历的数组"ABD##E#H##CF##"构建二叉树
BTNode* BinaryTreeCreate(BTDataType* a, int* pi)
{
	assert(a);
	if (a[*pi] == '#')
	{
		(*pi)++;
		return NULL;
	}
	BTNode* root = BuyNode(a[*pi]);
	(*pi)++;
	root->left = BinaryTreeCreate(a, pi);
	root->right = BinaryTreeCreate(a, pi);
	return root;
}

这样就可以直接构建出如下二叉树:
在这里插入图片描述
至此,我们以后构建二叉树就可以按照这种方法构建了。

4.3.2 二叉树的销毁

二叉树的销毁需要采用后序遍历的顺序进行销毁

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

4.4 二叉树的其他操作

通过以上的学习,我们已经对二叉树的构建,销毁和遍历有了一定掌握了,接下来我们一起进行二叉树的其他操作。

4.4.1 二叉树节点个数和叶子节点个数

两者都需要用到递归的思想。
求节点个数的思路:
从根节点开始,我们要知道该二叉树的节点个数,我们就需要知道其左子树和右子树的节点个数然后加上根节点即可
按照这个逻辑递归。
图解:
在这里插入图片描述
代码实现:

// 二叉树节点个数
int BinaryTreeSize(BTNode* root)
{
	//方法1
	//if (root == NULL)
	//return 0;
	//int ret = 1;
	//ret += BinaryTreeSize(root->left);
	//ret += BinaryTreeSize(root->right);
	//return ret;

	//方法2
	return root == NULL ? 0 :
		BinaryTreeSize(root->left) + BinaryTreeSize(root->right) + 1;
}

这里提供了两种写法,一个思路清晰,一个代码简介。

求叶子节点个数:
思路基本一样,只是不需要额外算上根节点自己了。直接上代码:

// 二叉树叶子节点个数
int BinaryTreeLeafSize(BTNode* root)
{
	if (root == NULL)
		return 0;
	if (root->left == NULL && root->right == NULL)
		return 1;
	//方法1
	//int ret = 0;
	//ret += BinaryTreeLeafSize(root->left);
	//ret += BinaryTreeLeafSize(root->right);
	//return ret;

	//方法2
	return BinaryTreeLeafSize(root->left) + BinaryTreeLeafSize(root->right);
}

同样也是提供了两种方法

4.4.2 二叉树第k层节点个数

同样也是递归思想,还是以我们之前构建的二叉树为例,要求第三层的节点个数,我们从根出发,就是求第三层的节点个数,相对于求根的左子树的第二层节点个数和根的右子树的第二层的节点个数…依次递归,即求k > 1 子树的k-1层结点个数相加。
图解:
在这里插入图片描述
在这里插入图片描述
如果觉得没有讲述清楚,我们换个方式理解:A的第3层,相对于B和C来说是第2层,而B和C的第2层,相对于D、E和F来说是第一层,而第一层的节点个数要么为0,要么为1
代码实现:

// 二叉树第k层节点个数
int BinaryTreeLevelKSize(BTNode* root, int k)
{
	if (root == NULL)
		return 0;
	if (k == 1)
		return 1;
	// k > 1 子树的k-1层结点个数相加
	return BinaryTreeLevelKSize(root->left, k - 1) + BinaryTreeLevelKSize(root->right, k - 1);
}

4.4.3 二叉树查找值为x的节点

思路很简单,遍历一遍树来查找就行了。这里我们以前序遍历为例,上代码:

// 二叉树查找值为x的节点
BTNode* BinaryTreeFind(BTNode* root, BTDataType x)
{
	if (root == NULL)
		return NULL;
	if (root->data == x)
		return root;
	BTNode* rleft = BinaryTreeFind(root->left, x);
	if (rleft)
		return rleft;
	BTNode* rright = BinaryTreeFind(root->right, x);
	if (rright)
		return rright;
	return NULL;
}

4.5 完整代码

BinaryTree.h

#include <stdio.h>
#include <stdlib.h>
#include <assert.h>
typedef int BTDataType;

typedef struct BinaryTreeNode
{
	BTDataType data;
	struct BinaryTreeNode* left;
	struct BinaryTreeNode* right;
}BTNode;
#include "Queue.h"
//申请一个新结点
BTNode* BuyNode(BTDataType x);
// 通过前序遍历的数组"ABD##E#H##CF##G##"构建二叉树
BTNode* BinaryTreeCreate(BTDataType* a, int* pi);
// 二叉树销毁
void BinaryTreeDestory(BTNode** root);
// 二叉树节点个数
int BinaryTreeSize(BTNode* root);
// 二叉树叶子节点个数
int BinaryTreeLeafSize(BTNode* root);
//二叉树的深度
int TreeHeight(BTNode* root);
// 二叉树第k层节点个数
int BinaryTreeLevelKSize(BTNode* root, int k);
// 二叉树查找值为x的节点
BTNode* BinaryTreeFind(BTNode* root, BTDataType x);
// 二叉树前序遍历 
void BinaryTreePrevOrder(BTNode* root);
// 二叉树中序遍历
void BinaryTreeInOrder(BTNode* root);
// 二叉树后序遍历
void BinaryTreePostOrder(BTNode* root);
#include "Queue.h"

// 层序遍历
void BinaryTreeLevelOrder(BTNode* root);

BinaryTree.c

#include "BinaryTree.h"
//申请一个新结点
BTNode* BuyNode(BTDataType x)
{
	BTNode* newNode = (BTNode*)malloc(sizeof(BTNode));
	if (newNode == NULL)
	{
		perror("malloc failed");
		exit(-1);
	}
	newNode->data = x;
	newNode->left = newNode->right = NULL;
	return newNode;
}
// 通过前序遍历的数组"ABD##E#H##CF##G##"构建二叉树
BTNode* BinaryTreeCreate(BTDataType* a, int* pi)
{
	assert(a);
	if (a[*pi] == '#')
	{
		(*pi)++;
		return NULL;
	}
	BTNode* root = BuyNode(a[*pi]);
	(*pi)++;
	root->left = BinaryTreeCreate(a, pi);
	root->right = BinaryTreeCreate(a, pi);
	return root;
}
// 二叉树销毁
void BinaryTreeDestory(BTNode** root)
{
	if (*root == NULL)
		return;
	BinaryTreeDestory(&(*root)->left);
	(*root)->left = NULL;
	BinaryTreeDestory(&(*root)->right);
	(*root)->right = NULL;
	free(*root);
	*root = NULL;
}
// 二叉树前序遍历 
void BinaryTreePrevOrder(BTNode* root)
{
	if (root == NULL)
	{
		printf("NULL ");
		return;
	}
	printf("%c ", root->data);
	BinaryTreePrevOrder(root->left);
	BinaryTreePrevOrder(root->right);
}
// 二叉树中序遍历
void BinaryTreeInOrder(BTNode* root)
{
	if (root == NULL)
	{
		printf("NULL ");
		return;
	}
	BinaryTreeInOrder(root->left);
	printf("%c ", root->data);
	BinaryTreeInOrder(root->right);
}
// 二叉树后序遍历
void BinaryTreePostOrder(BTNode* root)
{
	if (root == NULL)
	{
		printf("NULL ");
		return;
	}
	BinaryTreePostOrder(root->left);
	BinaryTreePostOrder(root->right);
	printf("%c ", root->data);
}
// 二叉树节点个数
int BinaryTreeSize(BTNode* root)
{
	//方法1
	//if (root == NULL)
	//return 0;
	//int ret = 1;
	//ret += BinaryTreeSize(root->left);
	//ret += BinaryTreeSize(root->right);
	//return ret;

	//杭哥方法
	return root == NULL ? 0 :
		BinaryTreeSize(root->left) + BinaryTreeSize(root->right) + 1;
}
// 二叉树叶子节点个数
int BinaryTreeLeafSize(BTNode* root)
{
	if (root == NULL)
		return 0;
	if (root->left == NULL && root->right == NULL)
		return 1;
	//方法1
	//int ret = 0;
	//ret += BinaryTreeLeafSize(root->left);
	//ret += BinaryTreeLeafSize(root->right);
	//return ret;

	//杭哥方法
	return BinaryTreeLeafSize(root->left) + BinaryTreeLeafSize(root->right);
}
//二叉树的深度
int TreeHeight(BTNode* root)
{
	if (root == NULL)
		return 0;
	int leftHeight = TreeHeight(root->left);
	int rightHeight = TreeHeight(root->right);
	return leftHeight > rightHeight ? leftHeight + 1 : rightHeight + 1;
}
// 二叉树第k层节点个数
int BinaryTreeLevelKSize(BTNode* root, int k)
{
	if (root == NULL)
		return 0;
	if (k == 1)
		return 1;
	// k > 1 子树的k-1层结点个数相加
	return BinaryTreeLevelKSize(root->left, k - 1) + BinaryTreeLevelKSize(root->right, k - 1);
}
// 二叉树查找值为x的节点
BTNode* BinaryTreeFind(BTNode* root, BTDataType x)
{
	if (root == NULL)
		return NULL;
	if (root->data == x)
		return root;
	BTNode* rleft = BinaryTreeFind(root->left, x);
	if (rleft)
		return rleft;
	BTNode* rright = BinaryTreeFind(root->right, x);
	if (rright)
		return rright;
	return NULL;
}
// 层序遍历

void BinaryTreeLevelOrder(BTNode* root)
{
	Queue q;
	QueueInit(&q);
	if (root != NULL)
		QueuePush(&q, root);
	while (!QueueEmpty(&q))
	{
		BTNode* front = QueueFront(&q);
		printf("%c ", front->data);
		QueuePop(&q);

		if (front->left)
		{
			QueuePush(&q, front->left);
		}

		if (front->right)
		{
			QueuePush(&q, front->right);
		}
	}
	printf("\n");

	QueueDestroy(&q);
}

五. 总结

二叉树的内容是比较多的,而且很多算法的思路都需要用到递归的思想,需要大家多花时间思考和画图分析。希望本篇博客对大家的数据结构有帮助。后面两期的博客我应该会讲一下几个比较常见的排序算法和Linux的入门。
还有一点注意的是:本篇博客用到的可视化视频是在VisuAlgo网站截取的,如果大家有需要也可以去该网站学习。
那我们下一期博客再见吧!

在这里插入图片描述

  • 7
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 3
    评论
二叉树的前序遍历是先访问根结点,然后访问左子树,最后访问右子树;中序遍历是先访问左子树,然后访问根结点,最后访问右子树。因此,如果已知二叉树的前序遍历和中序遍历,就可以构造出整个二叉树。 对于给定的前序遍历序列和中序遍历序列,我们可以通过递归的方式来构造出二叉树。具体地,我们可以按照以下步骤进行: 1. 前序遍历序列的第一个元素即为根结点,找到该结点在中序遍历序列中的位置,该位置左边的序列即为左子树的中序遍历序列,右边的序列即为右子树的中序遍历序列。 2. 根据左子树中序遍历序列的长度,可以得到前序遍历序列中左子树的前序遍历序列,右子树的前序遍历序列即为剩余部分。 3. 递归地对左子树和右子树分别进行上述步骤,得到左子树和右子树的结构。 4. 最后将根结点和左右子树连接起来,得到完整的二叉树。 根据上述思路,我们可以编写如下的递归代码来构造二叉树: ```c // 前序遍历序列 preorder,中序遍历序列 inorder,序列长度为 len struct TreeNode* buildTree(int* preorder, int preorderSize, int* inorder, int inorderSize){ if (preorderSize == 0) { // 前序遍历序列为空,返回空指针 return NULL; } struct TreeNode* root = (struct TreeNode*)malloc(sizeof(struct TreeNode)); // 创建根结点 root->val = preorder[0]; int i; for (i = 0; i < inorderSize; i++) { // 在中序遍历序列中找到根结点的位置 i if (inorder[i] == root->val) { break; } } int leftLen = i; // 左子树的中序遍历序列长度 int rightLen = inorderSize - i - 1; // 右子树的中序遍历序列长度 root->left = buildTree(preorder+1, leftLen, inorder, leftLen); // 递归构造左子树 root->right = buildTree(preorder+1+leftLen, rightLen, inorder+leftLen+1, rightLen); // 递归构造右子树 return root; } ``` 该代码的时间复杂度为 $O(n^2)$,其中 n 表示二叉树的结点数。因为在每一次递归中,都需要在中序遍历序列中查找根结点的位置,时间复杂度为 $O(n)$,因此总时间复杂度为 $O(n^2)$。 对于大规模的数据,我们可以使用哈希表来优化查找根结点位置的过程,将时间复杂度降为 $O(n)$。
评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值