C语言数据结构(超详细讲解)| 二叉树的实现

news2024/11/25 4:47:28

二叉树

引言

在计算机科学中,数据结构是算法设计的基石,而二叉树(Binary Tree)作为一种基础且广泛应用的数据结构,具有重要的地位。无论是在数据库索引、内存管理,还是在编译器实现中,二叉树都扮演着关键角色。本文将带你深入了解二叉树的基本概念、性质,以及如何在C语言中实现一个完整的二叉树。

首先,我们将探讨二叉树的定义和基本特性,包括其节点、子节点、以及树的高度和深度等基本概念。接着,我们会逐步介绍如何使用C语言构建二叉树,涉及节点的定义、树的初始化、节点的插入与删除等基本操作。最后,我们还将讨论一些常见的二叉树遍历算法,如前序遍历、中序遍历和后序遍历,并提供相应的代码示例。

通过这篇文章,你不仅能掌握二叉树的基本理论,还能学会如何在C语言中灵活运用这一数据结构,为解决实际编程问题提供有力的工具和方法。让我们一起开启这段探索二叉树世界的旅程吧!

本篇文章重点在4. 二叉树的链式结构及实现,有需要的小伙伴可以直接点击目录进行跳转

目录

  • 二叉树
    • 引言
    • 1. 树的概念及表示方法
      • 1.1 树的概念
      • 1.2 树的表示
    • 2. 二叉树的概念及性质
      • 2.1 二叉树的概念
      • 2.2 特殊的二叉树
      • 2.3 二叉树的性质
    • 3. 二叉树的顺序结构及实现
      • 3.1 堆的概念及结构
      • 3.2 堆的实现
        • 3.2.1 定义堆的结构体
        • 3.2.2 堆的初始化
        • 3.2.3 堆的销毁
        • 3.2.4 数据交换函数
        • 3.2.5 向上调整函数
        • 3.2.6 堆的插入
        • 3.2.7 向下调整算法
        • 3.2.8 堆的删除
        • 3.2.9 取堆顶的数据
        • 3.2.10 堆的判空
        • 3.2.11 堆的数据个数
      • 3.3 建堆的时间复杂度
      • 3.4 堆的应用
        • 3.4.1 堆排序
        • 3.4.2 Top-k 问题
    • 4. 二叉树的链式结构及实现
      • 4.1 二叉树的实现
        • 4.1.1 二叉树的前序遍历
        • 4.1.2 二叉树的中序遍历
        • 4.1.3 二叉树的后序遍历
        • 4.1.4 根据数组创建二叉树
        • 4.1.5 二叉树的销毁
        • 4.1.6 二叉树的节点个数
        • 4.1.7 二叉树叶子节点个数
        • 4.1.8 二叉树第k层节点个数
        • 4.1.9 二叉树查找值为x的节点
        • 4.1.10 层序遍历
        • 4.1.11 判断二叉树是否是完全二叉树

1. 树的概念及表示方法

在这一部分我会简单的介绍一下树的相关概念,不会太详细,因为这篇文章着重介绍的是二叉树,所以默认读者是对树这种结构有一定了解了,如果给您带来不便,还请见谅。

1.1 树的概念

树是一种非线性的数据结构,它是由n(n>=0)个有限结点组成一个具有层次关系的集合。把它叫做树是因为它看起来像一棵倒挂的树,也就是说它是根朝上,而叶朝下的。
在这里插入图片描述
注意:树型结构中,子树之间不能有交集,否则就不是树形结构
在这里插入图片描述
重点
在这里插入图片描述


1.2 树的表示

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

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

在这里插入图片描述


2. 二叉树的概念及性质

2.1 二叉树的概念

一棵二叉树是结点的一个有限集合,该集合或者为空,或者由一个根结点加上两棵别称为左子树和右子树的二叉树组成。在这里插入图片描述

二叉树的特点:

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

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

2.2 特殊的二叉树

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

对于完全二叉树的概念介绍大家可能还不能深刻理解什么是完全二叉树,我来给大家总结一下,我们可以对着下面的图片进行理解。
在这里插入图片描述
假设树的高度为h,前h-1层都是满的,最后一层不满,但是最后一层一定是从左往右连续分布的,不能有空节点。这就是完全二叉树。


2.3 二叉树的性质

  1. 若规定根结点的层数为1,则一棵非空二叉树的i层上最多有2^(i-1)个结点.
  2. 若规定根结点的层数为1,则深度为h的二叉树的最大结点数是(2^h)-1 .
  3. 对任何一棵二叉树, 如果度为0其叶结点个数为n0 , 度为2的分支结点个数为n2 ,则有n0 = n2 +1的关系.

下边附上性质3的证明

/*
* 假设二叉树有N个结点
* 从总结点数角度考虑:N = n0 + n1 + n2 ①
* 
* 从边的角度考虑,N个结点的任意二叉树,总共有N-1条边
* 因为二叉树中每个结点都有双亲,根结点没有双亲,每个节点向上与其双亲之间存在一条边
* 因此N个结点的二叉树总共有N-1条边
* 
* 因为度为0的结点没有孩子,故度为0的结点不产生边; 度为1的结点只有一个孩子,故每个度为1的结
点* * 产生一条边; 度为2的结点有2个孩子,故每个度为2的结点产生两条边,所以总边数为:
n1+2*n2 
* 故从边的角度考虑:N-1 = n1 + 2*n2 ②
* 结合① 和 ②得:n0 + n1 + n2 = n1 + 2*n2 - 1
* 即:n0 = n2 + 1
*/
  1. 若规定根结点的层数为1,具有n个结点的满二叉树的深度为h,节点数与深度的关系为:h=log2(n+1) . (ps:关系式是log以2为底,n+1为对数)
  2. 对于具有n个结点的完全二叉树,如果按照从上至下从左至右的数组顺序对所有结点从0开始编号,则对于序号为i的结点有:
    1.i>0i位置结点的双亲序号:(i-1)/2i=0i为根结点编号,无双亲结点
    2.2i+1<n,左孩子序号:2i+12i+1>=n否则无左孩子
    3.2i+2<n,右孩子序号:2i+22i+2>=n否则无右孩

在讲二叉树的存储结构之前,我先为大家简单的说明下这两种存储结构(顺序结构、链式结构)。

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

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

现实中我们通常把堆(一种二叉树)使用顺序结构的数组来存储,需要注意的是这里的堆和操作系统虚拟进程地址空间中的堆是两回事,一个是数据结构,一个是操作系统中管理内存的一块区域分段。

用数组来存储就避不开一个问题,当你拥有父节点的下标时,你如何快速的找到左右子节点的下标?
在这里插入图片描述
这张图总结的父子节点下标关系,是大家必须要掌握的,因为后边在顺序存储结构实现阶段会频繁用到

接下来我们将通过堆来继续学习顺序存储结构


3.1 堆的概念及结构

堆的概念实在是有点晦涩难懂,在这里我斗胆带大家通过堆的性质和图片实例简单的了解一下什么是堆。
堆主要有两种类型:大根堆和小根堆。

堆的性质:

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

在这里插入图片描述
根据上面的信息我们可以看出,最大堆的根节点是所有节点的中最大的,小根堆同理。每个节点都比自己的左右子节点要大或者小。希望大家现在可以对“堆”清晰的认识了。


3.2 堆的实现

3.2.1 定义堆的结构体
typedef int HPDataType;

typedef struct Heap
{
	HPDataType* a;	//堆是基于数组结构来实现的
	int size;		//数组下标
	int capacity;	//数组容量
}HP;
3.2.2 堆的初始化
//堆的初始化
void HPInit(HP* php)	//传入数组结构的指针
{
	assert(php);
	php->a = NULL;	//置空
	php->size = php->capacity = 0;	//归零
}
3.2.3 堆的销毁
//堆的销毁
void HPDestroy(HP* php)
{
	assert(php);
	free(php->a);
	php->a = NULL;
	php->size = php->capacity = 0;
}

3.2.4 数据交换函数
//数据交换
void Swap(HPDataType* p1, HPDataType* p2)
{
	HPDataType tmp = *p1;
	*p1 = *p2;
	*p2 = tmp;
}

3.2.5 向上调整函数
//向上调整——可以将数组恢复成堆的顺序——小根堆
void AdjustUp(HPDataType* a, int child)	//传入数组的地址和新插入的叶节点的下标
{
	int parent = (child - 1) / 2;	//无论是奇偶子节点,均可以通过数组的下标“-1再/2”来获得父节点的下标
	
	//while (parent >= 0) 为了防止数组的越界访问,我们不采取这个循环条件
	while (child > 0)
	{
		if (a[child] < a[parent])		//如果子节点小于父节点
		{
			Swap(&a[child], &a[parent]);	//使用交换函数,交换父子的数据

			child = parent;				//子节点位置换到父节点上
			parent = (child - 1) / 2;	//然后父节点通过转换方法重新指向变换后的子节点的父节点		
		}
		else
		{
			break;						//如果子节点大于父节点就不需要调整了
		}
	}
}
3.2.6 堆的插入

示例:先插入一个10到数组的尾上,在进行向上调整算法,直到满足堆。
在这里插入图片描述

//堆的插入
void HPPush(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(HPDataType));
		if (tmp == NULL)
		{
			perror("realloc fail");
			return;
		}

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

	php->a[php->size] = x;	//将数据x插入,数组下标为size的空间中
	php->size++;

	AdjustUp(php->a, php->size - 1);	//利用向上调整将插入新数据之后的数组恢复为堆
}


3.2.7 向下调整算法

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

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

在这里插入图片描述

//向下调整——可以将数组恢复成堆的顺序——小根堆
void AdjustDown(HPDataType* a, int n, int parent)	//传入数组的地址和数组的总大小还有根的下标
{	//传入根位置的下标是因为需要调整的元素的位置就是根位置

	// 先假设左孩子小——利用假设法
	int child = parent * 2 + 1;	//第一次先找到根节点下面的子节点

	while (child < n)  // child >= n说明孩子不存在,调整到叶子了
	{
		// 找出小的那个孩子
		if (child + 1 < n && a[child + 1] < a[child])
		{
			++child;
		}

		if (a[child] < a[parent])	
		{
			Swap(&a[child], &a[parent]);	//将小的子节点和父节点交换数据

			parent = child;				//父节点位置换到子节点上
			child = parent * 2 + 1;		//然后子节点通过转换方法重新指向变换后的父节点的子节点	
		}
		else
		{
			break;						//如果最小的节点都大于父节点了,就说明不需要调了
		}
	}
}

堆的创建
下面我们给出一个数组,这个数组逻辑上可以看做一颗完全二叉树,但是还不是一个堆,现在我们通过向下调整算法,把它构建成一个堆。从倒数的第一个非叶子结点的子树开始调整,一直调整到根结点的树,就可以调整成堆。

int a[] = {1,5,3,8,7,6};

图片中的是调整为大根堆,思路是一样的,代码只有些许不同
在这里插入图片描述

3.2.8 堆的删除

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

//堆顶的删除
void HPPop(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);	//利用向下调整将删除数据之后的数组恢复为堆
}

3.2.9 取堆顶的数据
//取堆顶的数据
HPDataType HPTop(HP* php)
{
	assert(php);
	assert(php->size > 0);

	return php->a[0];
}
3.2.10 堆的判空
//堆的判空
bool HPEmpty(HP* php)
{
	assert(php);

	return php->size == 0;
}
3.2.11 堆的数据个数
//堆的数据个数
int HPSize(HP* php)
{
	assert(php);
	return php->size;
}

3.3 建堆的时间复杂度

因为堆是完全二叉树,而满二叉树也是完全二叉树,此处为了简化,使用满二叉树来证明(时间复杂度本来看的就是近似值,多几个结点不影响最终结果):
在这里插入图片描述
在这里插入图片描述


3.4 堆的应用

3.4.1 堆排序

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

  1. 建堆
    升序:建大堆
    降序:建小堆
  2. 利用堆删除思想来进行排序
    建堆和堆删除中都用到了向下调整,因此掌握了向下调整,就可以完成堆排序。
    在这里插入图片描述
//堆排序 0(N*logN)
void HeapSort(int* a, int n)
{
	for (int i = (n - 1 - 1) / 2; i >= 0; i--)
	{
		AdjustDown(a, n, i);
	}

	int end = n - 1;
	while (end > 0)
	{
		Swap(&a[0], &a[end]);
		AdjustDown(a, end, 0);
		--end;
	}
}

3.4.2 Top-k 问题

TOP-K问题:即求数据结合中前K个最大的元素或者最小的元素,一般情况下数据量都比较大。
比如:专业前10名、世界500强、富豪榜、游戏中前100的活跃玩家等。
对于Top-K问题,能想到的最简单直接的方式就是排序,但是:如果数据量非常大,排序就不太可取了(可能数据都不能一下子全部加载到内存中)。最佳的方式就是用堆来解决,基本思路如下:

  1. 用数据集合中前K个元素来建堆
    前k个最大的元素,则建小堆
    前k个最小的元素,则建大堆
  2. 用剩余的N-K个元素依次与堆顶元素来比较,不满足则替换堆顶元素
    将剩余N-K个元素依次与堆顶元素比完之后,堆中剩余的K个元素就是所求的前K个最小或者最大的元素。

粗略讲解图
在这里插入图片描述
为了方便测试Top-k方法,我们写一个造数据的函数

//造数据
void CreateNDate()
{
	// 造数据
	int n = 100000;
	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) ;
		fprintf(fin, "%d\n", x);
	}

	fclose(fin);
}

Top-k方法实现

//Top-k方法
void Topk()
{
	int k;
	printf("请输入k>:");
	scanf("%d", &k);
	int* kminheap = (int*)malloc(sizeof(int) * k);
	if (kminheap == NULL)
	{
		perror("malloc fail");
		return;
	}
	const char* file = "data.txt";
	FILE* fout = fopen(file, "r");
	if (fout == NULL)
	{
		perror("fopen error");
		return;
	}

	// 读取文件中前k个数
	for (int i = 0; i < k; i++)
	{
		fscanf(fout, "%d", &kminheap[i]);
	}

	
	// 建K个数的小堆
	for (int i = (k - 1 - 1) / 2; i >= 0; i--)
	{
		AdjustDown(kminheap, k, i);
	}

	// 读取剩下的N-K个数
	int x = 0;
	while (fscanf(fout, "%d", &x) > 0)
	{
		if (x > kminheap[0])
		{
			kminheap[0] = x;
			AdjustDown(kminheap, k, 0);
		}
	}

	printf("最大前%d个数:", k);
	for (int i = 0; i < k; i++)
	{
		printf("%d ", kminheap[i]);
	}
	printf("\n");
}

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

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

按照规则,二叉树的遍历有:前序/中序/后序的递归结构遍历:

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

这3种遍历方式均是深度优先遍历

由于被访问的结点必是某子树的根,所以N(Node)、L(Left subtree)和R(Right subtree)又可解释为根、根的左子树和根的右子树。NLR、LNR和LRN分别又称为先根遍历、中根遍历和后根遍历。


下面主要分析前序递归遍历,中序与后序都是同理,大家可以自行分析。

前序遍历递归图解
在这里插入图片描述
在这里插入图片描述


4.1 二叉树的实现

因为二叉树的递归结构,可能会有些绕,我也尽量能配图的我就配图帮助大家理解,在二叉树中,大家自己的反复画图,和逻辑推理也是不能少的,只有足够的耐心才能掌握二叉树。

4.1.1 二叉树的前序遍历
// 二叉树前序遍历 
void BinaryTreePrevOrder(BTNode* root)
{
   //先判空
    if (root == NULL)
    {
        return;
    }
    //打印根节点
    printf("%c",root->data);
    //向下递归此根节点的左子树和右子树
    BinaryTreePrevOrder(root->left);
    BinaryTreePrevOrder(root->right);
}

后面在实现结构的时候我们会经常遇到这段代码

 //先判空
    if (root == NULL)
    {
        return;
    }

它的作用,我大概总结有以下几点,希望大家能够牢记

//开头的判空可以防止空指针的解引用
//递归遍历都需要先判空,因为这是“归”的条件
//当“递”到叶子节点的子节点的时候,一定为空,这时候需要return往回归
4.1.2 二叉树的中序遍历
// 二叉树中序遍历
void BinaryTreeInOrder(BTNode* root) 
{
    if (root == NULL) 
    {
        return;
    }
    BinaryTreeInOrder(root->left);
    printf("%c", root->data);
    BinaryTreeInOrder(root->right);
}

在这里插入图片描述

4.1.3 二叉树的后序遍历
// 二叉树后序遍历
void BinaryTreePostOrder(BTNode* root)
{
    if (root == NULL)
    {
        return;
    }
    BinaryTreePrevOrder(root->left);
    BinaryTreePrevOrder(root->right);
    printf("%c", root->data);
}
4.1.4 根据数组创建二叉树
// 通过前序遍历的数组"ABD##E#H##CF##G##"构建二叉树
//定义一个构造二叉树函数,将前序遍历的字符串用节点全部构建出来
//传入字符串和一个下标变量(此变量需要地址来改变,防止同时传给左右子树相同下标导致数组中数据覆盖)
BTNode* BinaryTreeCreate(char* a, int* pi) 
{
    //首先先判空
    if (a[*pi] == '#') {
        (*pi)++;
        return NULL;
    }
    //前序遍历保存根节点
    BTNode* root = (BTNode*)malloc(sizeof(BTNode));
    root->data = a[(*pi)++];
    //对左右子树进行递归
    root->left = BinaryTreeCreate(a, pi);
    root->right = BinaryTreeCreate(a, pi);
    return root;
}

在这里插入图片描述

4.1.5 二叉树的销毁
// 二叉树销毁   使用后序遍历的思想从后往前销毁每一个节点
void BinaryTreeDestory(BTNode* root)   
{	//要销毁二叉树链表就得把链表的头结点的指针的地址传入
    if (root == NULL)
        return;

    BinaryTreeDestory(root->left);
    BinaryTreeDestory(root->right);
    free(root);
}
4.1.6 二叉树的节点个数
// 二叉树节点个数
int BinaryTreeSize(BTNode* root)
{
    return root == NULL ? 0 : BinaryTreeSize(root->left) + BinaryTreeSize(root->right) + 1;
}   //子树节点个数等于左右节点个数+根节点(1)个数

在这里插入图片描述

4.1.7 二叉树叶子节点个数
//二叉树叶子节点个数
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);
}   //通过+运算和return可以达到统计的功能

在这里插入图片描述

4.1.8 二叉树第k层节点个数
// 二叉树第k层节点个数
int BinaryTreeLevelKSize(BTNode* root, int k)
{   //参数部分的k没有传地址,是因为需要左右子树记录向下递的层数相同。
    if (root == NULL)
    {
        return 0;
    }
    //k--,减到1的时候就是第k层了,返回1
    if (k == 1)
    {
        return 1;
    }
    return BinaryTreeLevelKSize(root->left, k - 1) + BinaryTreeLevelKSize(root->right, k - 1);
}   //通过+运算和return对k层节点数进行统计并返回顶层   //每次递下去,对k-1,可以达到对应层k为1的效果

在这里插入图片描述

4.1.9 二叉树查找值为x的节点
BTNode* BinaryTreeFind(BTNode* root, BTDataType x)
{
    //用前序遍历思想进行比较,找到就返回地址
    if (root == NULL)
    {
        return NULL;
    }
    //进行比较,判断此节点是否为x,不是就向下递归
    if (root->data == x)
    {
        return root;
    }
    //递归函数将归回来的结果传给变量进行判断,这样才能将结果递归回去。
    BTNode* ret1 = BinaryTreeFind(root->left, x);
    if (ret1)   //if的结果只有可能是NULL和值为x的节点指针root
    {
        return ret1;
    }
    BTNode* ret2 = BinaryTreeFind(root->right, x);
    if (ret2)
    {
        return ret2;
    }
    //如果下边都没有那就向上返回NULL,找到了就返回节点地址
    return NULL;
}

在这里插入图片描述


层序遍历

层序遍历:除了先序遍历、中序遍历、后序遍历外,还可以对二叉树进行层序遍历。设二叉树的根结点所在层数为1,层序遍历就是从所在二叉树的根结点出发,首先访问第一层的树根结点,然后从左到右访问第2层上的结点,接着是第三层的结点,以此类推,自上而下,自左至右逐层访问树的结点的过程就是层序遍历。
在这里插入图片描述
在实现层序遍历的时候我们需要用到队列结构,如果有对队列结构还不熟悉的同学,可以先点击链接跳转,学习过后再来看,可能也会有更多的收货。

4.1.10 层序遍历
//层序遍历
//层序遍历不像二叉树一样递归遍历,而是用队列结构通过循环先进先出进行遍历
//运用上一层根节点出带动下一层子节点入
void BinaryTreeLevelOrder(BTNode* root)
{
    Queue Q;
    QueueInit(&Q);          //先初始化队列结构,实现先进先出的功能,进行一个层序遍历
    if (root)       //根不为空的时候,就入对列开始遍历 
    {                       //如果为空,直接跳到函数的最后销毁
        QueuePush(&Q, root);
    }
    while(!QueueEmpty(&Q))      //队列不为空就一直循环
    {
        BTNode* front = QueueFront(&Q);//需要创建一个指针,指向队顶的元素,防止Pop出队顶元素,找不到其子节点
        QueuePop(&Q);       //排出上一层的子节点
        printf("%c", front->data);//并打印

        if (front->left)    //带入下一层的节点(只要不为空)
        {
            QueuePush(&Q, front->left);
        }
        if (front->right)
        {
            QueuePush(&Q, front->right);
        }
    }
    QueueDestroy(&Q);
}
4.1.11 判断二叉树是否是完全二叉树
// 判断二叉树是否是完全二叉树
//当所有非空节点都出队列的时候,判定队列是否为空,即可判断其是否为完全二叉树
bool BinaryTreeComplete(BTNode* root)
{
    Queue Q;
    QueueInit(&Q);          //先初始化队列结构,实现先进先出的功能,进行一个层序遍历
    if (root)       //根不为空的时候,就入对列开始遍历 
    {                       //如果为空,直接跳到函数的最后销毁,并返回false
        QueuePush(&Q, root);
    }
    while (!QueueEmpty(&Q))      //队列不为空就一直循环
    {
        BTNode* front = QueueFront(&Q);//需要创建一个指针,指向队顶的元素,防止Pop出队顶元素,找不到其子节点
        QueuePop(&Q);       //排出上一层的子节点
        
        //空节点也入队列,所以需要if语句来判断何时结束循环
        if (front == NULL)//如果前面的非空节点都Pop掉了,后面就应该全部是空节点了
        {                   
            break;        //所以break跳出循环,进入下一个循环来判断
        }

        //带入下一层的节点
        QueuePush(&Q, front->left);
        QueuePush(&Q, front->right);
    }
    while (!QueueEmpty(&Q))
    {
        BTNode* front = QueueFront(&Q);
        QueuePop(&Q);
        
        //如果进入了if语句,就说明此节点为非空节点,也就证明了此二叉树不是完全二叉树
        if (front)
        {
            QueueDestroy(&Q);
            return false;
        }
    }
    //如果队列剩下的节点均为空节点,就可以销毁队列,并返回true了
    QueueDestroy(&Q);
    return true;
}

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1795957.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

springboot 社区疫苗管理网站系统-计算机毕业设计源码89484

摘 要 随着社会的发展&#xff0c;社会的各行各业都在利用信息化时代的优势。计算机的优势和普及使得各种信息系统的开发成为必需。 社区疫苗管理网站系统&#xff0c;主要的模块包括查看首页、网站管理&#xff08;轮播图、公告信息&#xff09;人员管理&#xff08;管理员、…

Liunx音频

一. echo -e "\a" echo 通过向控制台喇叭设备发送字符来发声&#xff1a; echo -e "\a"&#xff08;这里的 -e 选项允许解释反斜杠转义的字符&#xff0c;而 \a 是一个响铃(bell)字符&#xff09; 二. beep 下载对应的包 yum -y install beep 发声命令 be…

金融创新浪潮下的拆分盘投资探索

随着数字化时代的步伐加速&#xff0c;金融领域正经历着前所未有的变革。在众多金融创新中&#xff0c;拆分盘作为一种新兴的投资模式&#xff0c;以其独特的增长机制&#xff0c;吸引了投资者的广泛关注。本文将对拆分盘的投资逻辑进行深入剖析&#xff0c;并结合具体案例&…

TikTok广告投放攻略——广告类型详解

TikTok广告是品牌或创作者付费向特定目标受众展示的推广内容&#xff08;通常是全屏视频&#xff09;。TikTok 上的广告是一种社交媒体营销形式&#xff0c;通常旨在提高广告商的知名度或销售特定产品或服务。 就 TikTok广告投放而言&#xff0c;其组织层级分为三个层级&#x…

【SpringBoot + Vue 尚庭公寓实战】项目初始化准备(二)

尚庭公寓SpringBoot Vue 项目实战】项目初始化准备&#xff08;二&#xff09; 文章目录 尚庭公寓SpringBoot Vue 项目实战】项目初始化准备&#xff08;二&#xff09;1、导入数据库2、创建工程3、项目初始配置3.1、SpringBoot依赖配置3.2、创建application.yml文件3.3、创建…

RabbitMQ(五)集群配置、Management UI

文章目录 一、安装RabbitMQ1、前置要求2、安装docker版复制第一个节点的.erlang.cookie进入各节点命令行配置集群检查集群状态 3、三台组合集群安装版rabbitmq节点rabbitmq-node2节点rabbitmq-node3节点 二、负载均衡&#xff1a;Management UI1、说明2、安装HAProxy3、修改配置…

代码随想录算法训练营第四十六 | ● 139.单词拆分 ● 关于多重背包,你该了解这些! ● 背包问题总结篇!

139.单词拆分 视频讲解&#xff1a;https://www.bilibili.com/video/BV1pd4y147Rh https://programmercarl.com/0139.%E5%8D%95%E8%AF%8D%E6%8B%86%E5%88%86.html class Solution { public:bool wordBreak(string s, vector<string>& wordDict) {unordered_set<st…

springboot vue 开源 会员收银系统 (6) 收银台的搭建

前言 完整版演示 前面我们对会员系统 分类和商品的开发 完成了收银所需的基础信息 下面我们开始完成收银台的开发 简单画了一个收银的流程图大家参考下 从这张图我们可以分析一下几点 可以选择会员或散客收银选择会员使用相应的会员价结算使用会员卡则在价格基础根据卡折扣…

sql server:数据库处于单用户模式,当前某个用户已与其连接

在 SQL Server 中&#xff0c;数据库可以设置为不同的用户模式&#xff0c;以便根据需要限制对数据库的访问。单用户模式&#xff08;Single-User Mode&#xff09;是其中一种模式&#xff0c;它限制了对数据库的访问&#xff0c;使得一次只能有一个用户连接到数据库。 单用户…

七月份大理站、ACM独立出版、高录用稳检索,2024年云计算与大数据国际学术会议(ICCBD 2024)

【ACM独立出版 | 高录用 | EI核心检索稳定】 2024年云计算与大数据国际学术会议&#xff08;ICCBD 2024) 2024 International Conference on Cloud Computing and Big Data (ICCBD 2024) 一、重要信息 大会官网&#xff1a;www.iccbd.net &#xff08;点击投稿/参会/了解会…

Vue2项目错误提示:Vue: <template v-for> key should be placed on the <template> tag.

1. 场景还原 升级了最新的Webstorm后打开Vue2项目提示以下波浪线错误&#xff1a; Vue: <template v-for> key should be placed on the <template> tag. 该错误不会影响正常运行和构建&#xff0c;但我们看到了会不舒服。 2. 错误原因 Vue2中key不能放在temp…

[数据集][图像分类]城市异常情况路边倒树火灾水灾交通事故分类数据集15223张8类别

数据集类型&#xff1a;图像分类用&#xff0c;不可用于目标检测无标注文件 数据集格式&#xff1a;仅仅包含jpg图片&#xff0c;每个类别文件夹下面存放着对应图片 图片数量(jpg文件个数)&#xff1a;15223 分类类别数&#xff1a;8 类别名称:[“badroad”,“fallentree”,“f…

四、 【源码】数据源的解析、创建和使用

源码地址&#xff1a;https://github.com/mybatis/mybatis-3/ 仓库地址&#xff1a;https://gitcode.net/qq_42665745/mybatis/-/tree/04-datasource-use 数据源的解析、创建和使用 流程&#xff1a; 1.Resources加载MyBatis配置文件生成Reader字符流 2.SqlSessionFactory…

[ZJCTF 2019]NiZhuanSiWei、[HUBUCTF 2022 新生赛]checkin、[SWPUCTF 2021 新生赛]pop

目录 [ZJCTF 2019]NiZhuanSiWei [HUBUCTF 2022 新生赛]checkin 1.PHP 关联数组 PHP 数组 | 菜鸟教程 2.PHP 弱比较绕过 PHP 类型比较 | 菜鸟教程 [SWPUCTF 2021 新生赛]pop [ZJCTF 2019]NiZhuanSiWei BUUCTF [ZJCTF 2019]NiZhuanSiWei特详解&#xff08;php伪…

推荐个 Edge/Chrome/Firefox 都支持的 IP 定位查询扩展

作为一个博客站长&#xff0c;对 IP 地址应该都不陌生&#xff0c;可以说是跟站长的工作是息息相关的&#xff0c;反正明月几乎每天都会面临 IP 查询、定位的需要&#xff0c;今天让明月给找到了一个叫”IP 定位查询“的浏览器扩展&#xff0c;在 Edge 和 Firefox 下体验后感觉…

新奇css模板

引言 (csscoco.com)https://csscoco.com/inspiration/#/./init 可视化集合 (hepengwei.cn)http://hepengwei.cn/#/html/visualDesign 30 秒代码 (30secondsofcode.org)https://www.30secondsofcode.org/ Animate.css |CSS动画的跨浏览器库。https://animate.style/

springboot+vue+mybatis房屋租贷系统+PPT+论文+讲解+售后

本论文系统地描绘了整个网上房屋租赁系统的设计与实现&#xff0c;主要实现的功能有以下几点&#xff1a;管理员&#xff1b;首页、个人中心、房屋类型管理、房屋租赁管理、会员管理、订单信息管理、合同信息管理、退房评价管理、管理员管理&#xff0c;系统管理&#xff0c;前…

基于SSM前后端分离版本的论坛系统-自动化测试

目录 前言 一、测试环境 二、环境部署 三、测试用例 四、执行测试 4.1、公共类设计 创建浏览器驱动对象 测试套件 释放驱动类 4.2、功能测试 注册页面 登录页面 版块 帖子 用户个人中心页 站内信 4.3、界面测试 注册页面 登录页面 版块 帖子 用户个人中心页…

【Python】教你彻底了解 Python中的文件处理

​​​​ 文章目录 一、文件的打开与关闭1. 打开文件2. 关闭文件3. 文件模式 二、文件的读写操作1. 读取文件内容2. 写入文件内容 三、使用上下文管理器四、异常处理五、二进制文件操作1. 读取二进制文件2. 写入二进制文件 六、实际应用示例1. 处理CSV文件2. 处理JSON文件 结论…

如何学习使用淘宝API?淘宝API运营场景

学习使用淘宝API涉及对其功能、分类、调用方法及实际应用的综合理解。下面按部分详细解释如何系统地学习和掌握淘宝API的使用&#xff1a; 淘宝API接口入门 了解淘宝开放平台&#xff1a;淘宝开放平台为开发者提供了一个可以与淘宝数据进行交互的平台&#xff0c;涵盖了丰富的A…