【数据结构】二叉数

1.树概念及结构

1.1树的概念

树是一种非线性的数据结构。

  • 有一个特殊的结点,称为根结点根节点没有前驱结点
  • 除根节点外,其余结点被分成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)棵互不相交的树的集合称为森林;

上面相关概念中常见一点的是2,4,5,8,9,11小点。

1.3树的表示

树结构表示较麻烦,既要保存值域,也要保存结点和结点之间的关系,我们这里就简单的了解其中最常用的孩子兄弟表示法

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

让此节点里的孩子指针指向它的第一个孩子节点,此节点的兄弟指针指向它的下一个兄弟,这样循环就可以将所有节点连接起来。如下:

在这里插入图片描述

2.二叉树概念及结构

2.1概念

一棵二叉树是结点的一个有限集合,该集合:

  1. 或者为空
  2. 由一个根节点加上两棵别称为左子树和右子树的二叉树组成

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

由上图可得:

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

注意:对于任意的二叉树都是由以下几种情况复合而成的:

在这里插入图片描述

2.2特殊的二叉树:

  1. 满二叉树:一个二叉树,如果每一个层的结点数都达到最大值,则这个二叉树就是满二叉树。也就是
    说,如果一个二叉树的层数为K,且结点总数是2^k-1,则它就是满二叉树。
  2. 完全二叉树前h-1层是满的,最后一层不一定满,但从左到右一定连续(即最后一层的节点是依次从左往右铺的)。
    在这里插入图片描述
    注意:满二叉树是一种特殊的完全二叉树。
  • 对于满二叉树:设节点总数为N,高度为h,有2^h-1=N —> h=log(N+1)。
  • 对于完全二叉树:设节点总数为N,高度为h,因为完全二叉树节点总数范围为[2^(h-1), 2^h-1], 2^(h-1)=N —> h=logN+1, 2^h-1=N —> h=log(N+1)。

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=log(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否则无右孩子

注意:上面性质5在堆中很重要,如图先编号:
在这里插入图片描述
编好号后就可以用性质5根据当前节点的下标计算出它的父节点和左右孩子节点的下标,在数组中找到下标就可以直接访问了。

2.4二叉树的存储结构

  1. 顺序存储
    顺序结构存储就是使用数组来存储,一般使用数组只适合表示完全二叉树,因为不是完全二叉树会有空
    间的浪费。而现实中使用中只有堆才会使用数组来存储。二叉树顺序存储在物理上是一个数组,在逻辑上是一颗二叉树。
    在这里插入图片描述

  2. 链式存储

二叉树的链式存储结构是指,用链表来表示一棵二叉树,即用链来指示元素的逻辑关系。 通常的方法是链表中每个结点由三个域组成,数据域和左右指针域,左右指针分别用来给出该结点左孩子和右孩子所在的链结点的存储地址 。链式结构又分为二叉链和三叉链,当前一般都是二叉链,后面高阶数据结构如红黑树等会用到三叉链。

在这里插入图片描述

在这里插入图片描述

3.二叉树的顺序结构及实现

现实中我们通常把堆(一种二叉树)使用顺序结构的数组来存储。堆在物理上是一个数组,逻辑上是二叉树

3.1堆的概念及结构

  • 小堆:任意父亲节点的值小于等于孩子节点
  • 大堆:任意父亲节点的值大于等于孩子节点

堆只有大堆和小堆。
在这里插入图片描述

堆的性质

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

3.2堆的实现

3.2.1堆的插入

如下图在下面堆中插入10,利用向上调整算法调整(从堆底向上调整),直到满足堆为止:
在这里插入图片描述
代码实现:

//堆的初始化
void HeapInit(HP* php)
{
	assert(php);

	php->a = NULL;
	php->size = 0;
	php->capacity = 0;
}

//交换两个数的函数
void Swap(HPDataType* p1, HPDataType* p2)
{
	HPDataType tmp = *p1;
	*p1 = *p2;
	*p2 = tmp;
}

//向上调整算法
void AdjustUp(HPDataType* a, int child)
{
	int parent = (child - 1) / 2;
	while(child>0)
	{
		if (a[parent] > a[child])
		{
			Swap(&a[parent], &a[child]);
			child = parent;
			parent = (parent - 1) / 2;
		}
		else
		{
			break;
		}
	}
}

//插入数据
void HeapPush(HP* php, HPDataType x)
{
	assert(php);

	//检查容量是否充足,不够则扩容
	if (php->size == php->capacity)
	{
		int newcapacity = php->capacity == 0 ? 4 : php->capacity * 2;
		HPDataType* tmp = (HPDataType*)realloc(php->a,newcapacity * sizeof(int));
		if (tmp == NULL)
		{
			perror("HeapPush()::malloc()");
			return;
		}

		php->a = tmp;
		php->capacity = newcapacity;
	}

	php->a[php->size] = x;
	php->size++;

	//要让新插入进来的数据满足堆的结构,用向上调整算法
	AdjustUp(php->a, php->size - 1);
}

注意:上面都是按小堆实现的,若要实现大堆改几个符号即可

因为向上调整算法最多调整高度次,所以时间复杂度为:O(logN)

3.2.2堆的删除

  • 堆的删除要用到向下调整算法,下面介绍一下:

现在我们给出一个数组,逻辑上看做一颗完全二叉树。我们通过从根节点开始的向下调整算法可以把它调整
成一个小堆。向下调整算法有一个前提:左右子树必须是一个堆,才能调整。

int array[] = {27,15,19,18,28,34,65,49,25,37};

在这里插入图片描述

  • 删除堆是删除堆顶的数据,将堆顶的数据根最后一个数据一换,然后删除数组最后一个数据,再进行向下调
    整算法(从堆顶向下调整)。
    在这里插入图片描述

向下调整算法最多也调整高度次,所以时间复杂度为:O(logN)

3.2.3堆的代码实现

#include<stdio.h>
#include<stdlib.h>
#include<assert.h>
#include<stdbool.h>

typedef int HPDataType;

typedef struct Heap
{
	HPDataType* a;
	int size;
	int capacity;
}HP;

//堆的初始化
void HeapInit(HP* php);

//插入数据
void HeapPush(HP* php, HPDataType x);

//删除数据(从堆顶删)
void HeapPop(HP* php);

//获取堆顶元素
HPDataType HeapTop(HP* php);

//堆的元素个数
int HeapSize(HP* php);

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

//堆的销毁
void HeapDestroy(HP* php);

注意以下实现的是小堆,若要实现大堆改几个符号即可
//堆的初始化
void HeapInit(HP* php)
{
	assert(php);

	php->a = NULL;
	php->size = 0;
	php->capacity = 0;
}

//交换两个数的函数
void Swap(HPDataType* p1, HPDataType* p2)
{
	HPDataType tmp = *p1;
	*p1 = *p2;
	*p2 = tmp;
}

//向上调整算法
void AdjustUp(HPDataType* a, int child)
{
	int parent = (child - 1) / 2;
	while(child>0)
	{
		if (a[parent] > a[child])
		{
			Swap(&a[parent], &a[child]);
			child = parent;
			parent = (parent - 1) / 2;
		}
		else
		{
			break;
		}
	}
}

//插入数据
void HeapPush(HP* php, HPDataType x)
{
	assert(php);

	//检查容量是否充足,不够则扩容
	if (php->size == php->capacity)
	{
		int newcapacity = php->capacity == 0 ? 4 : php->capacity * 2;
		HPDataType* tmp = (HPDataType*)realloc(php->a,newcapacity * sizeof(int));
		if (tmp == NULL)
		{
			perror("HeapPush()::malloc()");
			return;
		}

		php->a = tmp;
		php->capacity = newcapacity;
	}

	php->a[php->size] = x;
	php->size++;

	//要让新插入进来的数据满足堆的结构,用向上调整算法
	AdjustUp(php->a, php->size - 1);
}

//向下调整算法
void AdjustDown(HPDataType* a, int size,int parent)
{
	int child = parent * 2 + 1;//左孩子
	while (child<size)
	{
		//判断左孩子和右孩子那个小,选小的那个(右孩子还要存在)
		if (child + 1 < size && a[child + 1] < a[child])
		{
			child++;
		}

		if (a[child] < a[parent])
		{
			Swap(&a[child], &a[parent]);
			parent = child;
			child = parent * 2 + 1;
		}
		else
		{
			break;
		}
	}
}

//删除数据(从堆顶删)
void HeapPop(HP* 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(HP* php)
{
	assert(php);
	assert(php->size > 0);

	return php->a[0];
}

//堆的元素个数
int HeapSize(HP* php)
{
	assert(php);

	return php->size;
}

//判断堆是否为空
bool HeapEmpty(HP* php)
{
	assert(php);

	return php->size == 0;
}

//堆的销毁
void HeapDestroy(HP* php)
{
	assert(php);

	free(php->a);
	php->a = NULL;
	php->size = php->capacity = 0;
}

3.3堆的应用

3.3.1堆排序

堆排序即利用堆的思想来进行排序,总共分为两个步骤:

1.建堆

  • 升序:建大堆
  • 降序:建小堆

这里有两种建堆方式,第一种是直接遍历数组每个元素,先把第一个元素看成堆,再在这个堆后面依次插入剩余元素,直接调用向上调整算法例如:

int main()
{
	//排序思路一
	int a[10] = { 1,3,2,4,5,6,8,9,7,9};
	for (int i = 0; i < sizeof(a)/sizeof(int); i++)
	{
		AdjustUp(a, i);//这个函数在堆实现里面
	}
	for (int i = 0; i < 10; i++)
	{
		printf("%d ", a[i]);
	}
	printf("\n");
}

第一种建堆方式的时间复杂度为:O(NlogN)

下面介绍第二种建堆方式,直接调用向下调整算法,们从倒数的第一个非叶子节点的子树开始调整,一直调整到根节点的树,就可以调整成堆。(注意前面的向下调整是从根节点开始的,这里是从倒数的第一个非叶子节点的子树开始的)
在这里插入图片描述
下面算一下这种向下调整算法的时间复杂度:
在这里插入图片描述
所以推导出时间复杂度为:O(N)
所以这种建堆方式的时间复杂度更低,用它来建堆。

代码实现:

int main()
{
    int n = sizeof(a) / sizeof(int);
	for (int i = (n - 1 - 1) / 2; i >= 0; i--)
	{
		AdjustDown(a, n, i);
	}
}

拓展:
在这里插入图片描述

2.利用堆删除思想来进行排序
假设排升序已经建好大堆后,将堆顶最大的元素和堆底元素交换,所以,最大的元素就到堆底最后的位置处了,再让除堆底元素外的其余元素建堆,再让堆顶最大的元素和堆底元素交换···,例如:
在这里插入图片描述
所以堆排序的代码实现为:

int main()
{
	排序思路一
	//int a[10] = { 1,3,2,4,5,6,8,9,7,9};
	//for (int i = 0; i < sizeof(a)/sizeof(int); i++)
	//{
	//	AdjustUp(a, i);
	//}
	//for (int i = 0; i < 10; i++)
	//{
	//	printf("%d ", a[i]);
	//}//输出发现在上面数组中直接用向上调整去排序行不通,只能在堆顶选出最小或最大的那个数(小堆或大堆),如果想继续排序就将剩余的元素再建堆,再选出次小或次大的那个元素,这样重复···但这样太麻烦了,时间复杂度也上来了
	//printf("\n");

	//
	思路二---堆排序
	int a[10] = { 1,3,2,4,5,6,8,9,7,9 };

	第一种向上(向下)调整算法建堆
	O(N*logN)
	//for (int i = 0; i < sizeof(a)/sizeof(int); i++)
	//{//排升序,建大堆
	//	AdjustUp(a, i);
	//}

	//int end = sizeof(a) / sizeof(int)-1;
	//while (end>0)
	//{
	//	Swap(&a[0], &a[end]);
	//	AdjustDown(a, end - 1, 0);//交换头尾两元素,在堆底选出最大(最小)元素,再将剩余元素建堆,再选次大(小)元素,循环重复执行···
	//	end--;
	//}
	//for (int i = 0; i < sizeof(a) / sizeof(int); i++)
	//{
	//	printf("%d ", a[i]);
	//}


	//第二种向上(向下)调整算法建堆
	//O(N)
	int n = sizeof(a) / sizeof(int);
	for (int i = (n - 1 - 1) / 2; i >= 0; i--)
	{
		AdjustDown(a, n, i);
	}

	int end = sizeof(a) / sizeof(int) - 1;
	while (end > 0)
	{
		Swap(&a[0], &a[end]);
		AdjustDown(a, end - 1, 0);//交换头尾两元素,在堆底选出最大(最小)元素,再将剩余元素建堆,再选次大(小)元素,循环重复执行···
		end--;
	}
	for (int i = 0; i < sizeof(a) / sizeof(int); i++)
	{
		printf("%d ", a[i]);
	}

	return 0;
}

3.3.2TOP-K问题

TOP-K问题:即求数据结合中前K个最大的元素或者最小的元素,一般情况下数据量都比较大。

1.用数据集合中前K个元素来建堆

  • 前k个最大的元素,则建小堆(遍历后面元素,大的就进堆,替换堆顶最小值,遍历完后堆中就是前k个最大的元素)
  • 前k个最小的元素,则建大堆(遍历后面元素,小的就进堆,替换堆顶最大值,遍历完后堆中就是前k个最小的元素)

2.用剩余的N-K个元素依次与堆顶元素来比较,不满足则替换堆顶元素

代码实现:

//TOP-K问题
//在一千万个数中选出最大的10个数

typedef int HPDataType;

//交换两个数的函数
void Swap(HPDataType* p1, HPDataType* p2)
{
	HPDataType tmp = *p1;
	*p1 = *p2;
	*p2 = tmp;
}


//向上调整算法
void AdjustUp(HPDataType* a, int child)
{
	int parent = (child - 1) / 2;
	while (child > 0)
	{
		if (a[parent] > a[child])
		{
			Swap(&a[parent], &a[child]);
			child = parent;
			parent = (parent - 1) / 2;
		}
		else
		{
			break;
		}
	}
}

//向下调整算法
void AdjustDown(HPDataType* a, int size, int parent)
{
	int child = parent * 2 + 1;//左孩子
	while (child < size)
	{
		//判断左孩子和右孩子那个小,选小的那个(右孩子还要存在)
		if (child + 1 < size && a[child + 1] < a[child])
		{
			child++;
		}

		if (a[child] < a[parent])
		{
			Swap(&a[child], &a[parent]);
			parent = child;
			child = parent * 2 + 1;
		}
		else
		{
			break;
		}
	}
}

//造数据
void CreateNData()
{
	int n = 10000000;
	srand(time(0));//重置种子
	const char* file = "data.txt";
	FILE* fin = fopen(file, "w");
	if (fin == NULL)
	{
		perror("fopen error:");
		return;
	}

	//写数据进文件
	for (int i = 0; i < n; i++)
	{
		int x = (rand() + i) % 10000000;//因为在重置种子后rand()最多只能生成三万多个不重复的数据,所以加i后可以大大减少重复率
		fprintf(fin, "%d\n", x);
	}

	fclose(fin);
}

//选取较大的前K个
void PrintTopK(const char* file, int k)
{
	FILE* fout = fopen(file, "r");
	if (fout == NULL)
	{
		perror("fopen error:");
		return;
	}

	int* minheap = (int*)malloc(k * sizeof(int));
	if (minheap == NULL)
	{
		perror("malloc error:");
		return;
	}

	//建一个含k个元素的小堆
	for (int i = 0; i < k; i++)
	{
		fscanf(fout, "%d", &minheap[i]);
		AdjustUp(minheap, i);
	}

	int x;
	while ((fscanf(fout, "%d", &x) != EOF))
	{
		if (minheap[0] < x)
		{//堆中最小的元素在堆顶,大的元素下沉到了堆的下层,读到大的元素就替换堆顶最小的元素,再建堆,让堆中小的元素再浮上来,再替换···,就可以找到最大的前k个
			/*if (x > 10000000)
			{
				int d=1;
			}*/ //可以自己在这儿写一个条件,打一个断点,看一下读取正常与否

			minheap[0] = x;
			AdjustDown(minheap, k, 0);
		}
	}

	for (int i = 0; i < k; i++)
	{
		printf("%d ", minheap[i]);
	}
	printf("\n");

	free(minheap);
	fclose(fout);
}

int main()
{
	CreateNData();
	PrintTopK("data.txt", 10);
	return 0;
}

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

4.1二叉树的遍历

4.2.1 前序、中序以及后序遍历

二叉树遍历(Traversal)是按照某种特定的规则,依次对二叉树中的节点进行相应的操作,并且每个节点只操作一次。

二叉树的遍历:前序/中序/后序的递归结构遍历:

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

简记:

  • 前序:根 左子树 右子树
  • 中序:左子树 根 右子树
  • 后序:左子树 右子树 根

具体实现:

先手动创建一个二叉树:

#include<stdio.h>
#include<stdlib.h>

typedef int BTDataType;

typedef struct BinaryTreeNode
{
	BTDataType data;
	struct BinaryTreeNode* left;
	struct BinaryTreeNode* right;
}TreeNode;

//创建节点函数
TreeNode* BuyTreeNode(BTDataType x)
{
	TreeNode* node = (TreeNode*)malloc(sizeof(TreeNode));
	if (node == NULL)
	{
		perror("BuyTreeNode()::malloc():");
		return;
	}

	node->data = x;
	node->left = node->right = NULL;
	return node;
}

//手动创建一个二叉树
TreeNode* CreateTree()
{
	TreeNode* node1 = BuyTreeNode(1);
	TreeNode* node2 = BuyTreeNode(2);
	TreeNode* node3 = BuyTreeNode(3);
	TreeNode* node4 = BuyTreeNode(4);
	TreeNode* node5 = BuyTreeNode(5);
	TreeNode* node6 = BuyTreeNode(6);
	TreeNode* node7 = BuyTreeNode(7);

	node1->left = node2;
	node1->right = node4;
	node2->left = node3;
	node4->left = node5;
	node4->right= node6;
	node5->left = node7;

	return node1;
}

二叉树创建成功后,用递归进行前序遍历,前序遍历可被拆解为:先访问根,再访问左子树,再访问右子树,左子树又可以分为根和左子树和右子树,再来访问根,再访问左子树,再访问右子树······当遇到根节点为空时结束。

//前序遍历
void PreOrder(TreeNode* root)
{
	//根为空
	if (root == NULL)
	{
		printf("N ");//打印空
		return;
	}

	printf("%d ", root->data);//打印根
	PreOrder(root->left);//遍历左子树
	PreOrder(root->right);//遍历右子树
}

前序遍历示意图:
在这里插入图片描述
在这里插入图片描述

完整代码:

#include<stdio.h>
#include<stdlib.h>

typedef int BTDataType;

typedef struct BinaryTreeNode
{
	BTDataType data;
	struct BinaryTreeNode* left;
	struct BinaryTreeNode* right;
}TreeNode;

//创建节点函数
TreeNode* BuyTreeNode(BTDataType x)
{
	TreeNode* node = (TreeNode*)malloc(sizeof(TreeNode));
	if (node == NULL)
	{
		perror("BuyTreeNode()::malloc():");
		return;
	}

	node->data = x;
	node->left = node->right = NULL;
	return node;
}

//手动创建一个二叉树
TreeNode* CreateTree()
{
	TreeNode* node1 = BuyTreeNode(1);
	TreeNode* node2 = BuyTreeNode(2);
	TreeNode* node3 = BuyTreeNode(3);
	TreeNode* node4 = BuyTreeNode(4);
	TreeNode* node5 = BuyTreeNode(5);
	TreeNode* node6 = BuyTreeNode(6);
	TreeNode* node7 = BuyTreeNode(7);

	node1->left = node2;
	node1->right = node4;
	node2->left = node3;
	node4->left = node5;
	node4->right= node6;
	node5->left = node7;

	return node1;
}
//前序遍历
void PreOrder(TreeNode* root)
{
	if (root == NULL)
	{
		printf("N ");
		return;
	}

	printf("%d ", root->data);//根
	PreOrder(root->left);//左子树
	PreOrder(root->right);//右子树
}
int main()
{
	TreeNode* root = CreateTree();
	PreOrder(root);
	return 0;
}

在这里插入图片描述
中序和后序同理,只是根左右子树访问顺序不同。

//中序遍历
void InOrder(TreeNode* root)
{
	if (root == NULL)
	{
		printf("N ");
		return;
	}

	InOrder(root->left);//左子树
	printf("%d ", root->data);//根
	InOrder(root->right);//右子树
}

//后序遍历
void PostOrder(TreeNode* root)
{
	if (root == NULL)
	{
		printf("N ");
		return;
	}

	PostOrder(root->left);//左子树
	PostOrder(root->right);//右子树
	printf("%d ", root->data);//根
}

4.2.2层序遍历

层序遍历:除了前序遍历、中序遍历、后序遍历外,还可以对二叉树进行层序遍历。设二叉树的根节点所在层数为1,层序遍历就是从所在二叉树的根节点出发,首先访问第一层的树根节点,然后从左到右访问第2层上的节点,接着是第三层的节点,以此类推,自上而下,自左至右逐层访问树的结点的过程就是层序遍历。
在这里插入图片描述
这里我们用队列实现层序遍历。

示意图:
在这里插入图片描述

示例代码:

#include<stdio.h>
#include<stdlib.h>
#include<assert.h>
#include<stdbool.h>

typedef int BTDataType;

typedef struct BinaryTreeNode
{
	BTDataType data;
	struct BinaryTreeNode* left;
	struct BinaryTreeNode* right;
}TreeNode;

typedef TreeNode* QDataType;
typedef struct QueueNode
{
	QDataType val;
	struct QueueNode* next;
}QNode;

typedef struct Queue
{
	QNode* phead;
	QNode* ptail;
	int size;
}Queue;

//队列的初始化
void QueueInit(Queue* pq);

//队尾入队列
void QueuePush(Queue* pq, QDataType x);

//队头出队列
void QueuePop(Queue* pq);

//获取队列尾部元素
QDataType QueueBack(Queue* pq);

//获取队列头部元素
QDataType QueueFront(Queue* pq);

//检测队列是否为空(为空返回非0,不为空返回0)
bool QueueEmpty(Queue* pq);

//销毁队列
void QueueDestory(Queue* pq);

//获取对列中有效元素个数
int QueueSize(Queue* pq);
#include"Queue.h"

//队列的初始化
void QueueInit(Queue* pq)
{
	assert(pq);

	pq->phead = pq->ptail = NULL;
	pq->size = 0;
}

//插入数据
void QueuePush(Queue* pq, QDataType x)
{
	assert(pq);

	QNode* newnode = (QNode*)malloc(sizeof(QNode));
	if (newnode == NULL)
	{
		perror("QueuePush()::malloc()");
		return;
	}
	newnode->val = x;
	newnode->next = NULL;

	if (pq->ptail == NULL)
	{
		pq->phead = pq->ptail = newnode;
	}
	else
	{
		pq->ptail->next = newnode;
		pq->ptail = newnode;
	}

	pq->size++;
}

//队头出队列
void QueuePop(Queue* pq)
{
	assert(pq);
	assert(pq->phead);

	QNode* del = pq->phead;
	pq->phead = pq->phead->next;
	free(del);
	del = NULL;

	if (pq->phead == NULL)
		pq->ptail = NULL;

	pq->size--;
}

//获取队列尾部元素
QDataType QueueBack(Queue* pq)
{
	assert(pq);
	assert(pq->phead);

	return pq->ptail->val;
}

//获取队列头部元素
QDataType QueueFront(Queue* pq)
{
	assert(pq);
	assert(pq->phead);

	return pq->phead->val;
}

//检测队列是否为空(为空返回非0,不为空返回0)
bool QueueEmpty(Queue* pq)
{
	assert(pq);

	return pq->phead == NULL;
}

//获取对列中有效元素个数
int QueueSize(Queue* pq)
{
	assert(pq);

	return pq->size;
}

//销毁队列
void QueueDestory(Queue* pq)
{
	assert(pq);

	QNode* cur = pq->phead;
	while (cur)
	{
		QNode* del = cur;
		cur = cur->next;
		free(del);
	}

	pq->phead = pq->ptail = NULL;
	pq->size = 0;
}
//层序遍历
//版本一打印成一行
//void LevelOrder(TreeNode* root)
//{
//	//创建队列,初始化
//	Queue q;
//	QueueInit(&q);
//
//	//插入根节点
//	if (root)
//	{
//		QueuePush(&q, root);
//	}
//
//	while (!QueueEmpty(&q))
//	{
//		TreeNode* front = QueueFront(&q);
//		QueuePop(&q);
//
//		printf("%d ", front->data);
//
//		if (front->left)
//		{
//			QueuePush(&q, front->left);
//		}
//
//		if (front->right)
//		{
//			QueuePush(&q, front->right);
//		}
//	}
//
//	QueueDestory(&q);
//}

//版本二一层一层打印
void LevelOrder(TreeNode* root)
{
	//创建队列,初始化
	Queue q;
	QueueInit(&q);

	//插入根节点
	if (root)
	{
		QueuePush(&q, root);
	}

	//记录每层节点个数
	int LevelSize = 1;

	while (!QueueEmpty(&q))
	{
		//一层一层访问
		while (LevelSize--)
		{
			TreeNode* front = QueueFront(&q);
			QueuePop(&q);

			printf("%d ", front->data);

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

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

		LevelSize = QueueSize(&q);
	}

	QueueDestory(&q);
}

4.2节点个数以及高度等

  • 二叉树的节点个数

找二叉树的节点个数右可以将此问题拆解为:

  1. 节点个数=左子树节点个数+右子树节点个数+1,
  2. 当根节点为空时返回0.

在这里插入图片描述
代码示例:

//二叉树节点个数
int BinaryTreeSize(TreeNode* root)
{
	return root == NULL ? 0 : BinaryTreeSize(root->left) + BinaryTreeSize(root->right)+1;
}
  • 二叉树叶子节点个数

这个与上面那个求节点个数思路类似,问题拆解为:

  1. 叶子节点个数=左子树叶子节点个数+右子树叶子节点个数,
  2. 根节点为空时返回0,根节点为叶子节点时返回1.

代码示例:

//二叉树叶子节点个数
int BinaryTreeLeafSize(TreeNode* root)
{
	//根节点为空
	if (root == NULL)
	{
		return 0;
	}

	//根节点为叶子节点
	if (root->left==NULL && root->right == NULL)
	{
		return 1;
	}

	return BinaryTreeLeafSize(root->left) + BinaryTreeLeafSize(root->right);
}
  • 二叉树的高度

问题可分解为:

  1. 求出二叉树左子树和右子树高度,比较二者,较大的那个高度+1就是二叉树高度,
  2. 当根节点为空时返回0.

代码示例:

//二叉树高度
int TreeHeight(TreeNode* root)
{
	写法一
	//if (root == NULL)
	//{
	//	return 0;
	//}

	左子树高度
	//int leftheight = TreeHeight(root->left);
	右子树高度
	//int rightheight = TreeHeight(root->right);

	//return leftheight > rightheight ? leftheight + 1 : rightheight + 1;

	//写法二
	if (root == NULL)
	{
		return 0;
	}

	return fmax(TreeHeight(root->left), TreeHeight(root->right)) + 1;
}
  • 二叉树第k层的节点个数

问题分解为:

  1. 第k层节点个数=左子树的第k-1层节点个数+右子树的第k-1层节点个数;
  2. 根节点为空时返回0;
  3. k==1时返回1。

示意图:
在这里插入图片描述

示例代码:

//二叉树第k层节点个数
int TreeLevelk(TreeNode* root, int k)
{
	if (root == NULL)
	{
		return 0;
	}

	if (k == 1)
	{
		return 1;
	}

	return TreeLevelk(root->left, k - 1) + TreeLevelk(root->right, k - 1);
}
  • 二叉树查找值为x的节点

问题分解:

  1. 根节点数值域值==x时返回根节点;
  2. 二叉树查找值为x的节点==查找其左子树值为x的节点+查找其左子树值为x的节点;
  3. 根节点为空时,返回NULL。

示例代码:

//二叉树查找值为x的节点
TreeNode* TreeFind(TreeNode* root, BTDataType x)
{
	if (root == NULL)
	{
		return NULL;
	}

	if (root->data == x)
	{
		return root;
	}

	TreeNode* ret1= TreeFind(root->left, x);
	if (ret1->data == x)
	{
		return ret1;
	}

	TreeNode* ret2 = TreeFind(root->right, x);
	if (ret2->data == x)
	{
		return ret2;
	}

	//扩展写法1
	/*TreeNode* ret1 = TreeFind(root->left, x);
	if (ret1->data == x)
	{
		return ret1;
	}

	TreeFind(root->right, x);*/

	//扩展写法2
	//return TreeFind(root->left, x) || TreeFind(root->right, x);

	return NULL;
}

4.3二叉树的创建和销毁

  • 通过前序遍历的数组"ABD##E#H##CF##G##"构建二叉树

问题分解:

  1. 构建根;
  2. 构建左子树;
  3. 构建右子树;
  4. 遇到‘#’时返回NULL。

示意图:

在这里插入图片描述

示例代码:

//通过前序遍历的数组"ABD##E#H##CF##G##"构建二叉树
TreeNode* TreeCreate(char* a, int* pi)
{
	if (a[*(pi)] == '#')
	{
		(*pi)++;
		return NULL;
	}

	TreeNode* root = (TreeNode*)malloc(sizeof(TreeNode));
	if (root == NULL)
	{
		perror("TreeCreate()::malloc():");
		exit(-1);
	}
	//构建根
	root->data = a[(*pi)++];

	//构建左子树
	root->left = TreeCreate(a, pi);
	//构建右子树
	root->right = TreeCreate(a, pi);

	return root;
}
  • 二叉树销毁

问题分解:

  1. 销毁二叉树=销毁左子树+销毁右子树+销毁根;
  2. 当根节点为空时返回;

示例代码:

//二叉树销毁
void DestroyTree(TreeNode* root)
{
	if (root == NULL)
	{
		return;
	}

	DestroyTree(root->left);
	DestroyTree(root->right);
	free(root);
}
  • 判断二叉树是否是完全二叉树

这个问题用层序遍历很好解决。只需要判断在第一次访问到空后队列中是否还存在有效元素即可。
在这里插入图片描述
示例代码:

// 判断二叉树是否是完全二叉树
bool TreeComplete(TreeNode* root)
{
	//创建队列,初始化
	Queue q;
	QueueInit(&q);

	//插入根节点
	if (root)
	{
		QueuePush(&q, root);
	}

	//层序遍历
	//找到第一次为空的位置处
	while (!QueueEmpty(&q))
	{
		TreeNode* front = QueueFront(&q);
		QueuePop(&q);

		if (front == NULL)
		{
			break;
		}

		QueuePush(&q, front->left);
		QueuePush(&q, front->right);

	}

	//判断队列第一次出现空的后面还有没有元素
	while (!QueueEmpty(&q))
	{
		TreeNode* front = QueueFront(&q);
		QueuePop(&q);

		if (front)
		{
			QueueDestory(&q);
			return false;
		}
	}

	QueueDestory(&q);
	return true;
}
  • 26
    点赞
  • 24
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值