【数据结构(初阶)】——二叉树

news2024/12/22 16:06:32

【数据结构】——二叉树

文章目录

  • 【数据结构】——二叉树
    • 前言
    • 1. 树的概念及结构
      • 1.1 树的概念
      • 1.2 树的结构
    • 2. 二叉树的概念及结构
      • 2.1 二叉树的概念
      • 2.2 二叉树的结构
      • 2.3 二叉树的性质
    • 3. 二叉树顺序结构及概念
      • 3.1 二叉树的顺序结构
      • 3.2 堆的概念及结构
      • 3.3 堆的实现
        • 3.3.1 堆的基本操作
        • 3.3.2 堆的基本实现
        • 3.3.4 文件中查找TopK问题
    • 4. 二叉树链式结构及概念
      • 4.1 二叉树链式结构的遍历
      • 4.2 二叉树的基本操作
      • 4.3 二叉树的基本实现
    • 结语

前言

小伙伴们,大家好呀,今天我们学习的是数据结构中的 二叉树

之前我们写过二叉树的OJ题,但是有很多小伙伴不知道 二叉树 讲的是什么,咱们今天就好好详细地讲讲

1. 树的概念及结构

1.1 树的概念

在数据结构中,有一种被称为树的结构。和链式结构相似,需要用一个节点中的指针去查找下一块位置。但与链表不同的是,指向其他节点的指针会大于一,其结构如图所示

在这里插入图片描述

根据上图我们能够很清楚的了解树的概念:每个节点都会存储数据和指针,树有一个特点,就是虽然指针很多,但是能找到一个固定节点的指针只有一个

为了方便我们更加清楚地描述树,接下来将会讲解相关概念

  • 节点的度:一个节点含有的子树个数(例如图中的节点A的度为2,节点D的度为1)
  • 叶子节点或终端节点:度为0的节点被称为叶节点(例如图中节点E、I、J、K)
  • 非叶子节点或非终端节点:度不为0的节点(例如图中节点A、B、C、D、F、G、H)
  • 双亲节点或父节点:有子节点的节点,非叶子节点都是某个节点的父节点(例如图中节点A、B等)
  • 孩子节点或子节点:一个节点有父节点则为该父节点的子节点(例如图中除了A节点之外都能够是子节点)
  • 兄弟节点:具有相同父节点的节点(例如节点D、E、F、G都是兄弟节点)
  • 树的度:整个树中最大的度(指节点A的度)
  • 节点的层次:开始为1层,向下递增(例如图中节点A的层数为1,节点K的层数是5)
  • 树的高度或深度:最大节点的层次,也就是最大层(这里一共有5层,所以树的高度为5)
  • 堂兄弟节点:父节点在同一层的节点,但是父节点不相同(例如图中节点D和G是堂兄弟节点)
  • 祖先节点:特定节点向上的所有节点都是祖先节点(例如图中节点I的祖先节点有A、C、F)
  • 子孙节点:以某结点为根的子树中任一结点都称为该结点的子孙(例如图中的节点A,剩下的节点都是子孙节点)

1.2 树的结构

由上图可知,树的子节点都是不固定的,那么我们没办法直接构建相同结构体来表示数。因此有人想出了另一种方法来接收描述树。用一个子节点来找到他的兄弟节点,如下图所示

在这里插入图片描述

红色部分为实际数据存储方式,蓝色为树原本结构

如此解决了结构体多定义的问题,形成了一种只有子节点指针和右兄弟指针的方式,这种方式称作孩子兄弟表示法

代码实现如下:

struct TreeNode
{
    int val; //存储的数据
    struct TreeNode* leftchild; // 左孩子指针
    struct TreeNode* rightbrother; // 右兄弟指针
};

孩子兄弟表示法示意图

在这里插入图片描述

2. 二叉树的概念及结构

2.1 二叉树的概念

二叉树(binary tree)是指树中节点的度不大于2的有序树,它是一种最简单且最重要的树。二叉树的递归定义为:二叉树是一棵空树,或者是一棵由一个根节点和两棵互不相交的,分别称作根的左子树和右子树组成的非空树;左子树和右子树又同样都是二叉树

如图就是一颗标准的二叉树

在这里插入图片描述

二叉树的特点:

  1. 每个结点最多有两棵子树,即二叉树不存在度大于2的结点
  2. 二叉树的子树有左右之分,其子树的次序不能颠倒

特殊的二叉树:

  1. 满二叉树:一个二叉树,如果每一个层的结点数都达到最大值,则这个二叉树就是满二叉树。也就是说,如果一个二叉树的层数为K,且结点总数是(2^k) -1 ,则它就是满二叉树,也就是每一层都是满的

    在这里插入图片描述

  2. 完全二叉树:对于深度为K的,有n个结点的二叉树,当且仅当其每一个结点都与深度为K的满二叉树中编号从1至n的结点一一对应时称之为完全二叉树。就是除了最后一层之外都是满的,并且最后一层的元素是连续的

    在这里插入图片描述

需要我们注意的是满二叉树是特殊的完全二叉树

2.2 二叉树的结构

有关二叉树的结构,我们可以从物理结构逻辑结构两个角度进行理解

逻辑结构(想象出来的):使用左右指针储存数据

在这里插入图片描述

物理结构(也叫存储结构,内存中存取数据的结构):使用数组存储数据

在这里插入图片描述

二叉树如果按照存储结构可以分为顺序结构链式结构两种主要形式

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

这里插入图片描述

链式结构:用链表来表示一棵二叉树,即用链来指示元素的逻辑关系。 通常的方法是链表中每个结点由三个域组成,数据域和左右指针域,左右指针分别用来给出该结点左孩子和右孩子所在的链结点的存储地址

在这里插入图片描述

2.3 二叉树的性质

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

  • 若规定根节点的层数为1,则深度为h的二叉树的最大结点数是2^h- 1

  • 对任何一棵二叉树, 如果度为0其叶结点个数为 n0, 度为2的分支结点个数为 n2,则有n0=n2+1

  • 若规定根节点的层数为1,具有n个结点的满二叉树的深度,h=Log2(n+1). (ps:Log2(n+1)是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否则无右孩子
    

3. 二叉树顺序结构及概念

3.1 二叉树的顺序结构

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

在这里插入图片描述

3.2 堆的概念及结构

堆在物理(存储)结构上是数组,逻辑结构上就是完全二叉树

堆的性质:

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

然而堆又分为大堆和小堆

大根堆就是整个完全二叉树的 任意一个根节点的值都比左右子树的值大

在这里插入图片描述

小根堆表示整个完全二叉树的 任意一个根节点的值都比左右子树的值小

在这里插入图片描述

我们不难发现堆是父亲和孩子是有关系的,但是兄弟之间是没有大小关系的

3.3 堆的实现

3.3.1 堆的基本操作
// 堆的构建
void HeapCreate(Heap* hp, HPDataType* a, int n);
// 堆的销毁
void HeapDestory(Heap* hp);
// 堆的插入
void HeapPush(Heap* hp, HPDataType x);
// 堆的删除
void HeapPop(Heap* hp);
// 取堆顶的数据
HPDataType HeapTop(Heap* hp);
// 堆的数据个数
int HeapSize(Heap* hp);
// 堆的判空
int HeapEmpty(Heap* hp);
// 对数组进行堆排序
void HeapSort(int* a, int n);
3.3.2 堆的基本实现

堆的定义

在物理结构上堆就是数组,所以这里我们可以先定义一个堆的结构体,里面存放栈数组的指针,有size来记录堆中数据的个数,capacity来记录堆的空间大小

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

堆的初始化

这里我们先不给数组开辟空间,当堆里插入数据时我们再开辟空间

void HeapInit(Heap* hp)
{
	assert(php);
	hp->a = NULL;
	hp->capacity = hp->size = 0;
}

堆的销毁

堆的销毁就是释放掉给堆存放数据的空间,我们先free销毁数组,然后再给数组指针指向空,再将 top 和 capacity 都给0表示栈为空

void HeapDestory(Heap* hp)
{
	assert(hp);
	free(hp->a);
	hp->a = NULL;
	hp->capacity = hp->size = 0;
}

堆的插入

堆的插入我们需要得先开辟一定的空间,和队列一样的,扩容时 realloc 相比与 malloc 会更好,然后再更新a和capacity,赋值x,size++,堆插入的基本思想就是在堆的尾部插入x,然后就可以通过向上调整算法,将x调整到合适的位置,这里我们得好好讲一讲这个向上调整算法

void AdjustUp(HPDataType* a, int child)
{
	int parent = (child - 1) / 2;
	while (child > 0)
	{
		if (a[child] < a[parent])
		{
			Swap(&a[child], &a[parent]);
			child = parent;
			parent = (child - 1) / 2;
		}
		else
		{
			break;
		}
	}
}
void HeapPush(Heap* hp, HPDataType x)
{
	assert(hp);
	if (hp->capacity == hp->size)
	{
		int newcapacity = hp->capacity == 0 ? 4 : hp->capacity * 2;
		Heap* tmp = (Heap*)realloc(hp->a, newcapacity * sizeof(HPDataType));
		if (tmp == NULL)
		{
			perror("realloc fail!");
			return;
		}
		hp->a = hp;
		hp->capacity = newcapacity;
	}
	hp->a[hp->size++] = x;
	AdjustUp(hp->a, hp->size - 1);
}

向上调整算法

我们将要插入的那个元素的位置视为孩子,利用这个位置找到父亲节点

上面写了孩子的下标为i,父亲的节点是(i-1)/2

拿这个图举列子,这个是建立小堆,所以小节点在上面

在这里插入图片描述

按照小堆来调整,所以当发现父亲比孩子大的数据就交换。

循环交替,互换父亲和孩子的位置,直到孩子的数组下标为0时循环就截至

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

堆的删除

堆的删除就是要将堆顶位置的元素删除,先将堆顶元素和堆尾元素交换一下,然后直接size–,将交换后堆尾元素给删除掉,最后通过向下调整算法,将交换后的堆顶元素调整到合适的位置,这里我们再好好讲一讲这个向下调整算法

void AdjustDown(HPDataType* a, int n, int parent)
{
	int child = 2 * parent + 1;
	while (child < n)// child >= n 说明孩子不存在,调整到叶子了
	{
		if (child + 1 < n && a[child] > a[child + 1])
		{
			++child;
		}
		if (a[child] < a[parent])
		{
			Swap(&a[child], &a[parent]);
			parent = child;
			child = 2 * parent + 1;
		}
		else
		{
			break;
		}
	}
}
void HeapPop(Heap* hp)
{
	assert(hp);
	assert(hp->size > 0);
	Swap(&hp->a[0], &hp->a[hp->size - 1]);
	hp->size--;
	AdjustDown(hp->a, hp->size, 0);
}

向下调整算法

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

我们将要调整的那个元素的位置视为父亲,利用这个位置找到孩子节点

我们就将父亲的下标为i, 这时孩子节点有两个怎么办,我咋知道谁更小(大),我们就可以运用假设法的思想,找出较小(大)的节点

找到合适的孩子节点就交换

然后再将孩子节点传给父亲节点,然后继续往下找,直到孩子节点到达了叶子节点时循环就结束

下图是全过程:

在这里插入图片描述

void AdjustDown(HPDataType* a, int n, int parent)
{
	int child = 2 * parent + 1;
	while (child < n)// child >= n 说明孩子不存在,调整到叶子了
	{
		if (child + 1 < n && a[child] > a[child + 1])
		{
			++child;
		}
		if (a[child] < a[parent])
		{
			Swap(&a[child], &a[parent]);
			parent = child;
			child = 2 * parent + 1;
		}
		else
		{
			break;
		}
	}
}

获取堆顶元素

我们直接取数组下标为0位置的元素就行了,因为数组下标为0位置就是堆顶

HPDataType HeapTop(Heap* hp)
{
	assert(hp);
	assert(hp->size > 0);
	return hp->a[0];
}

判空

当数组中没有元素时堆为空,即size == 0

int HeapEmpty(Heap* hp)
{
	assert(hp);
	return hp->size == 0;
}

堆的数据个数

堆的数据个数相当于数组的元素个数,直接取size就行了

int HeapSize(Heap* hp)
{
	assert(hp);
	return hp->size;
}
3.3.4 文件中查找TopK问题

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

最佳的方式就是用堆来解决,基本思路如下:

  1. 用数据集合中前 K 个元素来建堆
  • 前 k 个最大的元素,则建小堆。建小堆,堆顶就是这K个元素的最小值,然后向后遍历其他数,如果其他数大于堆顶的元素,就弹出堆顶元素,插入这个较大的元素,这样遍历完成之后,堆中最小的元素都比剩下的数字大,这样堆中的K个元素就是所有元素前K大的
  • 前 k 个最小的元素,则建大堆。建大堆,堆顶就是这K个元素的最大值,然后向后遍历其他数,如果其他数小于堆顶的元素,就弹出堆顶元素,插入这个较小的元素,这样遍历完成之后,堆中最大的元素都比剩下的数字大,这样堆中的K个元素就是所有元素前K小的
  1. 用剩余的 N-K 个元素依次与堆顶元素来比较,不满足则替换堆顶元素
  • 将剩余 N-K 个元素依次与堆顶元素比完之后,堆中剩余的 K 个元素就是所求的前 K 个最小或者最大的元素。

文件中的TopK多一步是读取文件的一部分数据,因为文件可能很大,没办法全部加载到文件中,就可以循环使用一块缓冲区进行TopK,然后再加载文件中的内容,然后再执行TopK,一直到文件被读取完,这时候堆中的元素就是文件中TopK的元素

接下来就是代码实现:

void CreateNDate()
{
	// 造数据
	int n = 10000;
	srand(time(0));
	const char* file = "data.txt";
	FILE* fin = fopen(file, "w");
	if (fin == NULL)
	{
		perror("fopen error");
		return;
	}

	for (size_t i = 0; i < n; ++i)
	{
		int x = rand() % 1000000;
		fprintf(fin, "%d\n", x);
	}

	fclose(fin);
}

void PrintTopK(int k)
{
	const char* file = "data.txt";
	FILE* fout = fopen(file, "r");
	if (fout == NULL)
	{
		perror("fopen error");
		return;
	}

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

	for (int i = 0; i < k; i++)
	{
		fscanf(fout, "%d", &kminheap[i]);
	}

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

	int val = 0;
	while (!feof(fout))
	{
		fscanf(fout, "%d", &val);
		if (val > kminheap[0])
		{
			kminheap[0] = val;
			AdjustDown(kminheap, k, 0);
		}
	}

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

4. 二叉树链式结构及概念

4.1 二叉树链式结构的遍历

所谓遍历(Traversal)是指沿着某条搜索路线,依次对树中每个结点均做一次且仅做一次访问。访问结点所做的操作依赖于具体的应用问 题。 遍历是二叉树上最重要的运算之一,是二叉树上进行其它运算之基础

前序/中序/后序的递归结构遍历:是根据访问结点操作发生位置命名

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

由于被访问的结点必是某子树的根,所以N(Node)、L(Left subtree)和R(Right subtree)又可解释为根、根的左子树和根的右子

所以前序遍历、中序遍历和后序遍历分别又称为先根遍历、中根遍历和后根遍历

在这里插入图片描述

层序遍历:除了先序遍历、中序遍历、后序遍历外,还可以对二叉树进行层序遍历。设二叉树的根节点所在层数为1,层序遍历就是从所在二叉树的根节点出发,首先访问第一层的树根节点,然后从左到右访问第2层上的节点,接着是第三层的节点,以此类推,自上而下,自左至右逐层访问树的结点的过程就是层序遍历

在这里插入图片描述

4.2 二叉树的基本操作

// 二叉树前序遍历 
void BinaryTreePrevOrder(BTNode* root);
// 二叉树中序遍历
void BinaryTreeInOrder(BTNode* root);
// 二叉树后序遍历
void BinaryTreePostOrder(BTNode* root);
// 通过前序遍历的数组"ABD##E#H##CF##G##"构建二叉树
BTNode* BinaryTreeCreate(BTDataType* a, int n, int* pi);
// 二叉树销毁
void BinaryTreeDestory(BTNode** root);
// 二叉树节点个数
int BinaryTreeSize(BTNode* root);
// 二叉树叶子节点个数
int BinaryTreeLeafSize(BTNode* root);
// 二叉树第k层节点个数
int BinaryTreeLevelKSize(BTNode* root, int k);
// 二叉树查找值为x的节点
BTNode* BinaryTreeFind(BTNode* root, BTDataType x);
// 层序遍历
void BinaryTreeLevelOrder(BTNode* root);
// 判断二叉树是否是完全二叉树
int BinaryTreeComplete(BTNode* root);

4.3 二叉树的基本实现

树的定义

定义一个二叉树的结构体,里面存着节点的数据,还有左子树的结构体和右子树的结构体

typedef char BTDataType;
typedef struct BinaryTreeNode
{
	BTDataType data;
	struct BinaryTreeNode* left;
	struct BinaryTreeNode* right;
}BTNode;

创建树的节点

节点的创建我们需要 malloc一个结构体,再检查节点是否开辟成功,然后将节点数据赋值为X即可,再将左右指针指向空,最后返回开辟好的节点

BTNode* BuyNode(int x)//创建树的节点
{
	BTNode* node = (BTNode*)malloc(sizeof(BTNode));
	if (node == NULL)
	{
		perror("malloc fail");
		exit(-1);
	}
	node->a = x;
	node->left = node->right = NULL;
	return node;
}

手动创建一颗树

我们可以先手动创建一个树试试

在写完创建树的节点的函数之后,我们再手动创建一颗树就变得更简单了

BTNode* CreatTree()//建树
{
	BTNode* node1 = BuyNode(1);
	BTNode* node2 = BuyNode(2);
	BTNode* node3 = BuyNode(3);
	BTNode* node4 = BuyNode(4);
	BTNode* node5 = BuyNode(5);
	BTNode* node6 = BuyNode(6);
	node1->left = node2;
	node1->right = node4;
	node2->left = node3;
	node4->left = node5;
	node4->right = node6;
	return node1;
}

在这里插入图片描述

这样做的话,一颗树二叉树就简单地构建好了

前序遍历

前序遍历一颗树时分为两种情况:

  1. 如果树为空,那就不用访问了直接 return 结束了
  2. 如果树不为空,那就先访问根节点,然后还需要继续左右子树前序遍历,那我们就递归函数解决
void BinaryTreePrevOrder(BTNode* root)
{
	if (root == NULL)
		return;

	printf("%c", root->data);
	BinaryTreePrevOrder(root->left);
	BinaryTreePrevOrder(root->right);
}

中序遍历

和前序遍历思想一样,只不过中序就先递归左子树 再访问根 再递归右子树即可

void BinaryTreeInOrder(BTNode* root)
{
	if (root == NULL)
		return;

	BinaryTreeInOrder(root->left);
	printf("%c", root->data);
	BinaryTreeInOrder(root->right);
}

后序遍历

也是和前序遍历的思想一样,只不过后序就先递归左子树 再递归右子树 再递归根即可

void BinaryTreePostOrder(BTNode* root)
{
	if (root == NULL)
		return;

	BinaryTreePostOrder(root->right);
	BinaryTreePostOrder(root->left);
	printf("%c", root->data);
}

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

用一个字符数组中递归地构建二叉树,数组 a 中的元素按照前序遍历的方式表示二叉树节点,如果数组元素是 #,表示空节点。函数通过递归的方式创建每个节点,直到数组处理完成

BTNode* BinaryTreeCreate(BTDataType* a, int n, int* pi)
{
	if (*pi >= n || a[*pi] == '#')
	{
		(*pi)++;
		return NULL;
	}

	// 创建根节点
	BTNode* root = (BTNode*)malloc(sizeof(BTNode));
	if (root == NULL)
	{
		perror("malloc fail");
		exit(-1);
	}
	root->data = a[*pi];
	*pi++;

	// 创建左子树和右子树
	root->left = BinaryTreeCreate(a, n, pi);
	root->right = BinaryTreeCreate(a, n, pi);

	return root;
}

二叉树销毁

这里需要用到了后序遍历,如果先释放root的话就找不到root的左子树和右子树了,先找到左右子树,再去释放掉根节点。这样二叉树的销毁就完成了

void BinaryTreeDestory(BTNode** root)
{
	if (root == NULL || *root == NULL)
		return;

	// 递归销毁左子树和右子树
	BinaryTreeDestory(&(*root)->left);
	BinaryTreeDestory(&(*root)->right);

	// 释放当前节点
	free(*root);
	*root == NULL;
}

二叉树节点个数

如果二叉树为空就返回0,然后再去递归左右子树, +1 是根节点

int BinaryTreeSize(BTNode* root)
{
	if (root == NULL)
		return 0;

	return BinaryTreeSize(root->left)
		+ BinaryTreeSize(root->right) + 1;
}

二叉树叶子节点个数

如果二叉树为空就返回0,这里就需要我们理解什么是叶子节点了,我们说叶子节点是没有左右子树的节点,那就说明它的左右子树为空,然后再递归左右子树就行了

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);
}

二叉树第K层节点个数

还是如果节点为空就返回0,如果根节点不为空,就继续往下访问,当k–到1时,就不能继续往下访问了,因为求的就是求二叉树第k层结点个数,所以再往下访问就是违背了要求,所以这里直接返回1,也就是到达哪一层的那个节点,然后就是递归左子树和右子树,并且需要传 k - 1 这个变量

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);
}

二叉树查找值为X的节点

需要注意的是这个返回值是一个指针,找到了就返回节点,没找到就去左子树找,左子树找到了就不用再往后执行代码了,如果没有找到就去右子树找,右子树找到了就不用再往后执行代码了,如果都没找到的话就返回空

BTNode* BinaryTreeFind(BTNode* root, BTDataType x)
{
	if (root == NULL)
		return NULL;

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

	BTNode* ret1 = BinaryTreeFind(root->left, x);
	if (ret1)
		return ret1;

	BTNode* ret2 = BinaryTreeFind(root->left, x);
	if (ret2)
		return ret2;

	return NULL;
}

层序遍历

层序遍历,就是一层一层来遍历

先遍历第一层 1,再遍历第二层 2,4 ,再遍历最后一层 3,5,6。这里你可以发现使用递归无法实现,因为递归,你只能先把左子树或者右子树遍历完才能遍历其他的,但是这里层序遍历是一层一层来的

因为树不是连在一起的,根节点带左子树和右子树,所以这里我们可以用队列,一层带着一层入队列

现在1是根节点那就入队列,入队列的时候就把2,4带着,正好是第一二层打印顺序就搞定了,此时1再出队列,我们在实现队列时写了一个函数是取队头数据,所以直接取即可,这时1取走了,这时2就是队头了,取出队头2的数据,然后再将2带的左右子树入队列

在这里插入图片描述

此时2就是队头,将其取出,然后2的左子树和右子树入队列

在这里插入图片描述

接下来步骤一样 取出4,然后 5,6 入队列

在这里插入图片描述

此时,一个个出队列,直到队列为空,循环就结束了

在这里插入图片描述

void BinaryTreeLevelOrder(BTNode* root)
{
	if (root == NULL)
		return;

	Queue q;
	QueueInit(&q);
	if (root)
		QueuePush(&q, root);

	while (!QueueEmpty(&q))
	{
		BTNode* front = QueueFront(&q);
		QueuePop(&q);

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

		if (root->left)
			QueuePush(&q,root->left);

		if (root->right)
			QueuePush(&q,root->right);
	}

	BinaryTreeDestory(&q);
}

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

我们在这里再次回忆一下什么是完全二叉树,完全二叉树就是假设有k层, k - 1 层都是满节点,而第k层的节点存在必须是连续的,中间不能有空节点,如果中间有空节点,然后又有节点的话这种就不是完全二叉树

根据这个介绍,再根据上面队列的思想,入一个带左子树与右子树,如果我们遇到第一个空就开始判断如果接下来全是空即可说明是完全二叉树,如果空后又有节点就说明不是完全二叉树

如果还有数据没有入队,我们就可以不用管它,这时因为在空空后已经有数据出现了,所以不用入数据了,已经不是完全二叉树了

int BinaryTreeComplete(BTNode* root)
{
	if (root == NULL)
		return;

	Queue q;
	QueueInit(&q);
	if (root)
		QueuePush(&q, root);

	while (!QueueEmpty(&q))
	{
		BTNode* front = QueueFront(&q);
		QueuePop(&q);

		// 遇到第一个空,就可以开始判断,如果队列中还有非空,就不是完全二叉树
		if (front == NULL)
		{
			break;
		}

		QueuePush(&q, root->left);
		QueuePush(&q, root->right);
	}

	while (!QueueEmpty(&q))
	{
		BTNode* front = QueueFront(&q);
		QueuePop(&q);

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

	BinaryTreeDestory(&q);
	return true;
}

结语

这些就是 数据结构(初阶)——二叉树 的全部内容了,要是想做一点题的可以看看这篇哦【数据结构】——二叉树OJ题

感谢你能看到这里,希望这篇文章对你有用,溜了溜了,我们下篇再见吧

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

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

相关文章

【C++ Qt day9】

2、将day1做的登录界面升级优化【资源文件的添加】 3、 使用手动连接&#xff0c;将登录框中的取消按钮使用第2种方式的连接到自定义的槽函数中&#xff0c;在自定义的槽函数中调用关闭函数 将登录按钮使用qt4版本的连接到自定义的槽函数中&#xff0c;在槽函数中判断ui界面上…

黑马点评16——多级缓存-JVM进程缓存

文章目录 什么是多级缓存导入商品案例初识Caffeine实现进程缓存 什么是多级缓存 但是现在的nginx的压力太大了&#xff0c;所以nginx也要部署成集群 当然我们的redis、tomcat都可以部署成集群 导入商品案例 我们在docker中开启了一个mysql的数据库&#xff0c;里面配置了一个…

C和指针:高级指针话题

进一步探讨指向指针的指针 int i; int *pi; int **ppi; 这些声明在内存中创建了下列变量。如果它们是自动变量&#xff0c;无法猜测它们的初始值。 二级指针指向一级指针 ppi&pi; *ppi&i; ia; *pia; **ppia; 为什么要使用指针&#xff1f; 因为函数传参使用值传递不会…

[C#学习笔记]接口的特性与用法

视频地址&#xff1a;一期视频看透C#接口的全部特性及用法_哔哩哔哩_bilibili 强烈推荐学习C#和WPF的朋友关注此UP&#xff0c;知识点巨多&#xff0c;讲解透彻&#xff01; 一、总览 public interface IOverall {/// <summary>/// 最普通的方法/// </summary>v…

[数据集][目标检测]打电话检测数据集VOC+YOLO格式8985张1类别

数据集格式&#xff1a;Pascal VOC格式YOLO格式(不包含分割路径的txt文件&#xff0c;仅仅包含jpg图片以及对应的VOC格式xml文件和yolo格式txt文件) 图片数量(jpg文件个数)&#xff1a;8985 标注数量(xml文件个数)&#xff1a;8985 标注数量(txt文件个数)&#xff1a;8985 标注…

【C++】C++入门基础,详细介绍命名空间,缺省参数,函数重载,引用,内联函数等

目录 1. 命名空间 1.1 使用命名空间的目的 1.2 命名空间定义 1.3 命名空间使用 2. 缺省参数 2.1 缺省参数概念 2.2 缺省参数分类 2.3 实际案例 2.4 注意事项 3. 函数重载 3.1 函数重载概念 3.2 函数重载原理 4. 引用 4.1 引用的概念 4.2 引用的特性 4.3 使用…

JavaScript案例---求质数

n等于19&#xff0c;是质数 <!DOCTYPE html> <html lang"en"> <head><meta charset"UTF-8"><meta http-equiv"X-UA-Compatible" content"IEedge"><meta name"viewport" content"wid…

OpenAI Gymnasium, are there any libraries with algorithms supporting it?

题意&#xff1a;对于OpenAI Gym&#xff0c;是否有支持它的算法库&#xff1f; 问题背景&#xff1a; OpenAI has released a new library called Gymnasium which is supposed to replace the Gym library. There are many libraries with implamentations of RL algorithms…

机械学习—零基础学习日志(Python做数据分析02)

现在开始使用Python尝试做数据分析。具体参考的网址链接放在了文章末尾。 引言 我通过学习《利用Python进行数据分析》这本书来尝试使用Python做数据分析。书里让下载&#xff0c;anaconda&#xff0c;使用Jupyter来写代码&#xff0c;只是下载一个anaconda的确有点费时间&am…

RabbitMQ 04 集群,用于提高系统性能

01.背景 02.单个节点的MQ会持久化的记录什么数据 03.集群情况下的MQ会持久化的记录什么数据 04.集群中的队列 单个节点的队列&#xff1a; 集群的队列&#xff1a; 05. 两个原因&#xff1a; 这样做带来的好处&#xff1a; 05.集群的交换机 交换机的本质 交换机在集…

Unity TextMeshPro 设置竖排

默认竖排是这样的 但是我们要的竖排效果并不是这样我们要是竖排连续的根据文本限制来进行换行 第一步我们先设置文本的旋转Z轴为90如下图 然后我们给文本加一个Tag <rotate270> 如下图 但是这个效果还是不是我们想要的效果我们可以使用TexeMeshPro提供的一个选项EnableR…

97.游戏的启动与多开-共享内存多开检测

免责声明&#xff1a;内容仅供学习参考&#xff0c;请合法利用知识&#xff0c;禁止进行违法犯罪活动&#xff01; 内容参考于&#xff1a;易道云信息技术研究院 上一个内容&#xff1a;96.游戏的启动与多开-窗口多开检测与破解 以 96.游戏的启动与多开-窗口多开检测与破解 …

Word文档的读取(1)

读取一个班的答题卡 解决方法&#xff1a; 导入os模块后&#xff0c;将乔老师的文件夹路径 /Users/qiao/answerKey 赋值给变量allKeyPath。使用os.listdir()函数获取该路径下所有的答题卡名称列表&#xff0c;并赋值给变量allItems。最后使用for循环遍历所有答题卡&#xff0c…

【知识小妙招来喽!】文件防泄密措施有哪些?这6个有效方法防止企业员工泄密!

在信息高度发达的时代&#xff0c;企业数据的安全性和保密性成为了企业运营中不可忽视的重要环节。 一旦敏感文件被泄露&#xff0c;不仅可能导致商业机密被窃取&#xff0c;还可能给企业带来重大的经济损失和声誉损害。 因此&#xff0c;采取一系列有效的文件防泄密措施&…

spring security 中的异常

一、简介 Spring Security 中异常主要分为两大类: 1、AuthenticationException: 认证异常 2、AccessDeniedException: 授权异常 AuthenticationEntryPoint 该类用来统一处理 AuthenticationException 异常 AccessDeniedHandler 该类用来统一处理 AccessDeniedException 异常…

HarmonyOS笔记

HarmonyOS简介 HarmonyOS是新一代的智能终端操作系统&#xff0c;为不同设备的智能化、互联与协同提供了统一的语言&#xff0c;为用户带来简捷&#xff0c;流畅&#xff0c;连续&#xff0c;安全可靠的全场景交互体验。 HarmonyOS结合移动生态发展的趋势&#xff0c;提出了三大…

keepalive和nginx高可用集群

keepalived 和 nginx 高可用集群搭建 主备模式 zyj86主机和zyj87主机安装nginx和keepalived yum install nginx keepalived -y systemctl enable --now nginx.service keepalived.service主调度器配置 编辑zyj86主机&#xff08;主&#xff09;配置文件 vi /etc/keepalived…

Java类和对象(2)(重点*)

封装&#xff1a; 面向对象程序三大特性&#xff1a;封装、继承、多态。而类和对象阶段&#xff0c;主要研究的就是封装特性。何为封装呢&#xff1f;简单来说就是套壳屏蔽细节 。 封装&#xff1a;将数据和操作数据的方法进行有机结合&#xff0c;隐藏对象的属性和实现细节&a…

为什么用Redis?说说Redis的线程模型

使用Redis存储相比直接使用Java内存的Map有以下几个优势&#xff1a; 持久化&#xff1a; Redis支持数据持久化&#xff0c;即使发生服务器重启或意外宕机&#xff0c;数据仍然可以被恢复。而使用Java内存的Map&#xff0c;当应用程序关闭或重启时&#xff0c;数据将会丢失。…

一个工程要兼容mysql8和mysql5

将mysql8原本jar包的jdbc文件夹删除&#xff0c;然后将mysql5 jar包的jdbc文件夹和fabric文件夹拉到mysql8的jar包下&#xff0c;记得别把jar包解压再压缩&#xff0c;以避免不必要的错误&#xff0c;直接用7-zip打开压缩包&#xff0c;然后拖拽操作&#xff0c;然后完美解决&a…