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

【数据结构】——二叉树

前言

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

之前我们写过二叉树的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题

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

  • 22
    点赞
  • 15
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
1. 什么是二叉树二叉树是一种树形结构,其中每个节点最多有两个子节点。一个节点的左子节点比该节点小,右子节点比该节点大。二叉树通常用于搜索和排序。 2. 二叉树的遍历方法有哪些? 二叉树的遍历方法包括前序遍历、中序遍历和后序遍历。前序遍历是从根节点开始遍历,先访问根节点,再访问左子树,最后访问右子树。中序遍历是从根节点开始遍历,先访问左子树,再访问根节点,最后访问右子树。后序遍历是从根节点开始遍历,先访问左子树,再访问右子树,最后访问根节点。 3. 二叉树的查找方法有哪些? 二叉树的查找方法包括递归查找和非递归查找。递归查找是从根节点开始查找,如果当前节点的值等于要查找的值,则返回当前节点。如果要查找的值比当前节点小,则继续在左子树中查找;如果要查找的值比当前节点大,则继续在右子树中查找。非递归查找可以使用栈或队列实现,从根节点开始,每次将当前节点的左右子节点入栈/队列,直到找到要查找的值或者栈/队列为空。 4. 二叉树的插入与删除操作如何实现? 二叉树的插入操作是将要插入的节点与当前节点的值进行比较,如果小于当前节点的值,则继续在左子树中插入;如果大于当前节点的值,则继续在右子树中插入。当找到一个空节点时,就将要插入的节点作为该空节点的子节点。删除操作需要分为三种情况:删除叶子节点、删除只有一个子节点的节点和删除有两个子节点的节点。删除叶子节点很简单,只需要将其父节点的对应子节点置为空即可。删除只有一个子节点的节点,需要将其子节点替换为该节点的位置。删除有两个子节点的节点,则可以找到该节点的后继节点(即右子树中最小的节点),将其替换为该节点,然后删除后继节点。 5. 什么是平衡二叉树? 平衡二叉树是一种特殊的二叉树,它保证左右子树的高度差不超过1。这种平衡可以确保二叉树的查找、插入和删除操作的时间复杂度都是O(logn)。常见的平衡二叉树包括红黑树和AVL树。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值