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

【数据结构】——二叉树

文章目录

  • 【数据结构】——二叉树
    • 前言
    • 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题

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

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

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

探索全球实时云渲染与XR技术的多平台兼容性:谁是行业引领者?

在扩展现实&#xff08;XR&#xff09;技术与实时云渲染技术的飞速发展中&#xff0c;多平台兼容性已经成为行业技术竞争的关键要素。能够在不同的平台和设备上高效运行的解决方案&#xff0c;不仅关系到开发效率和场景多样性&#xff0c;还直接影响到用户体验和市场占有率。Pa…

在桌面商业分析应用程序中启用高级 Web UI

挑战 Mercur Business Control 应用程序在企业界&#xff0c;尤其是金融领域&#xff0c;拥有悠久的应用历史。它帮助企业处理、可视化和分析海量数据&#xff0c;从而做出明智的商业决策。 随着产品的不断演进和现代化&#xff0c;Mercur Solutions AB 为该应用创建了 Web 客…

基于SpringBoot的教师人事档案管理系统

你好呀&#xff0c;我是计算机学姐码农小野&#xff01;如果有相关需求&#xff0c;可以私信联系我。 开发语言&#xff1a;Java 数据库&#xff1a;MySQL 技术&#xff1a;SpringBootMyBatis 工具&#xff1a;IDEA/Eclipse、Navicat、Maven 系统展示 首页 教师管理 奖惩…

音频-语言大模型原理

重磅推荐专栏: 《大模型AIGC》 《课程大纲》 《知识星球》 本专栏致力于探索和讨论当今最前沿的技术趋势和应用领域,包括但不限于ChatGPT和Stable Diffusion等。我们将深入研究大型模型的开发和应用,以及与之相关的人工智能生成内容(AIGC)技术。通过深入的技术解析和实践经…

[数据集][目标检测]汽油检泄漏检测数据集VOC+YOLO格式237张2类别

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

开放式耳机具备什么特点?2024排行前十的四款百元蓝牙耳机推荐

开放式耳机具有以下特点&#xff1a; 佩戴舒适&#xff1a; 开放式耳机通常不需要插入耳道&#xff0c;能减少对耳道的压迫和摩擦&#xff0c;长时间佩戴也不易产生闷热、疼痛或瘙痒等不适&#xff0c;对于耳道敏感或不喜欢入耳式耳机压迫感的人来说是很好的选择。 这类耳机…

​​MEPA(Maximum Efficiency Per Ampere)控制

一.控制目的 与MTPA控制相比&#xff0c;没有忽略电机的铁耗&#xff0c;以电能损耗最小为目的优化电流。 分析思路与MTPA控制类似&#xff0c;在此省略。 二. 推导过程

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…

哈希表 和 算法

1.哈希表的作用&#xff1a;将我们要存储的数据&#xff0c;通过关键字与位置的关系函数&#xff0c;来确定具体的位置。 2.写哈希表时常出现的问题&#xff1a;哈希冲突/矛盾&#xff1a;当多个数据满足哈希函数的映射时出现 解决的方法为&#xff1a; 1&#xff09;开放地址…

VScode:前端开发中的常用快捷键和技巧

1.菜单栏 2.内容相关&#xff1a; 格式化文档 搜索文件名 代码双开对比 右上角选择拆分

vulhub GhostScript 沙箱绕过(CVE-2018-16509)

1.执行以下命令启动靶场环境并在浏览器访问 cd vulhub/ghostscript/CVE-2018-16509 #进入漏洞环境所在目录 docker-compose up -d #启动靶场 docker ps #查看容器信息 2.下载包含payload的png文件 vulhub/ghostscript/CVE-2018-16509/poc.png at master vulhub/vulhub Gi…

AIPaperGPT写论文靠谱吗?

AIPaperGPT&#xff0c;论文写作神器~ https://www.aipapergpt.com/ 在信息爆炸的今天&#xff0c;学术写作的挑战日益增加&#xff0c;而AIPaperGPT作为一款旨在提升写作效率的工具&#xff0c;其可靠性自然成为了用户关注的焦点。本文将从多个维度对AIPaperGPT进行全面评估&…

浙大数据结构:03-树2 List Leaves

这道题我借用了一点上一题的代码思路&#xff0c;这题考察的主要是层序遍历&#xff0c;即用队列来实现&#xff0c;当然此处我依然采用数组模拟队列来实现。 机翻 1、条件准备 map的键存下标&#xff0c;后面值分别存左右子树的下标&#xff0c;没有子树就存-1. head数组只…

Golang | Leetcode Golang题解之第385题迷你语法分析器

题目&#xff1a; 题解&#xff1a; func deserialize(s string) *NestedInteger {if s[0] ! [ {num, _ : strconv.Atoi(s)ni : &NestedInteger{}ni.SetInteger(num)return ni}stack, num, negative : []*NestedInteger{}, 0, falsefor i, ch : range s {if ch - {negati…

java后端保存的本地图片通过ip+端口直接访问

直接上代码吧 package com.ydx.emms.datapro.controller;import org.springframework.context.annotation.Configuration; import org.springframework.web.servlet.config.annotation.ResourceHandlerRegistry; import org.springframework.web.servlet.config.annotation.…

【C语言】指针深入讲解(下)

目录 前言回调函数回调函数的概念回调函数的使用 qsort函数的使用和模拟实现qsort函数的介绍qsort函数的使用qsort函数模拟实现 前言 今天我们来学习指针最后一个知识点回调函数&#xff0c;这个知识点也很重要&#xff0c;希望大家能坚持学习下去。 没学习之前指针知识内容的…

光伏并网发电系统中电能质量监测与优化技术探讨

0引言 随着清洁能源技术的持续进步与广泛应用&#xff0c;光伏并网发电系统亦逐步崭露头角。作为一种关键的电力供应方式&#xff0c;其受到了广泛的关注。然而&#xff0c;由于天气等外部条件的影响&#xff0c;光伏发电系统面临若干挑战。电能质量问题&#xff0c;诸如电压波…

并网光伏发电系统对电网电能质量的影响

0引言 随着清洁能源技术的持续进步&#xff0c;光伏发电作为一种关键的可再生能源&#xff0c;正逐步成为电力系统不可或缺的组成部分。然而&#xff0c;将并网光伏发电系统与传统电网相连接&#xff0c;可能会对电网的电能质量造成一定影响&#xff0c;包括但不限于电压波动、…

ROS2 Nav2 - 模型预测路径积分控制器(MPPI)

系列文章目录 前言 这是一个预测控制器&#xff08;局部轨迹规划器&#xff09;&#xff0c;用于实现模型预测路径积分&#xff08;MPPI&#xff09;算法&#xff0c;以跟踪具有自适应避障功能的路径。它包含基于插件的 critic 函数&#xff0c;可影响算法的行为。它由 Aleksei…

大模型RAG实战|构建知识库:文档和网页的加载、转换、索引与存储

我们要开发一个生产级的系统&#xff0c;还需要对LlamaIndex的各个组件和技术进行深度的理解、运用和调优。本系列将会聚焦在如何让系统实用上&#xff0c;包括&#xff1a;知识库的管理&#xff0c;检索和查询效果的提升&#xff0c;使用本地化部署的模型等主题。我将会讲解相…