DS二叉树的存储

news2024/11/24 7:37:53

前言

我们上一期已经介绍了树相关的基础知识,了解了树相关的概念和结构、二叉树的概念和结构以及性质、也介绍了他的存储方式!本期我们来根据上期介绍的对二叉树的顺序存储和链式存储分别进行实现!

本期内容介绍

二叉树的顺序结构

堆的概念以及结构

堆的实现

堆的应用

二叉树的链式结构

二叉树的遍历

二叉树的基本问题

一、二叉树的顺序结构

1、二叉树的顺序结构

上一期我们介绍了一般的二叉树是不适合用数组来存储的,原因是可能会造成大量的空间浪费!但完全二叉树是适合用数组存储的!现实中的(完全二叉树)就是这么玩的!注意这里的堆是一种数据结构,不是内存中说的那个堆(操作系统的概念)!

2、堆的概念以及结构

如果有一个关键码集合K={k0,k1,k2,...,kn-1}把他们的所有元素按照完全二叉树的顺序存储方式存储在一个一维数组中,并满足:Ki <= K2*i+1 && Ki <= K2*i+2(Ki >= K2*i+1 && Ki >= K2*i+2) i = 0,1,2....则称为小堆(大堆),将根节点最大的堆叫做最大堆或大根堆,根节点最小的堆叫做最小堆或小根堆。

堆的性质:

堆中的某个节点的值总数不大于或不小于父节点的值!

总是一颗完全二叉树!

大堆:父节点的值大于或等于其孩子节点的值

小堆:父节点的值小于或等于其孩子节点的值!

OK,画个图来解释一下:

3、堆的实现

堆结构声明

typedef int HPDataType;
typedef struct Heap 
{
	HPDataType* a;//存储数据的数组
	int size;//有效个数
	int capacity;//数组容量
}HP;

堆的初始化、销毁和打印

这里和顺序表、顺序栈玩的一样就不再解释了,直接上代码!

//初始化
void HPInit(HP* p)
{
	assert(p);
	p->a = NULL;
	p->size = 0;
	p->capacity = 0;
}

//销毁
void HPDestory(HP* p)
{
	assert(p);
	free(p->a);
	p->size = p->capacity = 0;
}

//打印
void HPPrint(HP* p)
{
	assert(p);
	for (int i = 0; i < p->size; i++)
	{
		printf("%d ", p->a[i]);
	}
	printf("\n");
}

堆的插入

由于这里堆是按照完全二叉树的顺序存到数组中的,所以对于数组的插入而言,尾插是效率和操作最好的!也符合这里堆的特点。但插入后要继续保证是堆就要做出一些调整了!这里有两种方式,一个是向上调整、一个是向下调整!我们先来介绍调整算法!

向上调整算法

当在堆中插入一个数据后即在数组尾插了一个元素后,可能改数组已经不是一个堆了,要变成堆需要进行向上调整(待会介绍向下调整)!

向上调整的思路:

当孩子节点的值小于(小堆)或大于(大堆)父亲节点时则需要堆孩子和父亲进行交换!由于具体插入的值会影响多少,具体不知道,所以对要对该节点的每一层的祖先节点逐一层对比,即父节点的值小于或大于交换,否则不交换结束掉!交换后孩子到父亲的位置,父亲到他父亲的位置(-1除2),一直循环判断...。到底交换(调整)到什么时候循环才结束呢?我们一般想的是:父亲小于0时结束,但!父亲不可能小于0,即使父亲是0号位置,(0-1)/2 == 0,C语言是会取整的!所以,用父亲判断不行。那就只能用孩子判断了,当孩子调整到0位置时就不能在调了(再调就越界了)即就结束了!

具体调整过程:

代码实现:

void AdjsutUp(HPDataType* a, int child)
{
	int parent = (child - 1) / 2;//第一次找孩子的父亲
	while (child > 0)//当孩子为0结束
	{
		if (a[child] < a[parent])//孩子比父亲小(小堆)
		{
			Swap(&a[child], &a[parent]);//交换
			child = parent;//孩子到父亲的位置
			parent = (parent - 1) / 2;//父亲到他父亲的位置
		}
		else
		{
			break;//孩子和父亲相等或大于父亲时结束
		}
	}
}

复杂度分析:这里少调整0次,最多调整高度(h = log2(N+1))次,所以时间复杂度是:O(lg(N)),空间复杂度:额外使用临时变量的个数是常数个,所以空间复杂度:O(1)

要实现插入这里还要实现一个向下调整算法!我们来实现一下:

向下调整算法

要进行向下调整的前提是:左右子树必须是堆!!!

向下调整算法的思路:

当该节点的左右子树都是堆(大堆或小堆)且该节点的值小于(大堆)或大于(小堆)左右子树的根节点的值时,要保证还是堆则需要向下调整。此时就得找出该节点左右子树中较小或较大的那个节点,进行与该节点进行交换,但具体交换的层数确定,所以得逐一对下每一层比较!当父亲节点的值和要交换的那个孩子节点交换后,父亲到孩子的位置,孩子在到孩子的孩子的位置,继续判断,当碰到该节点等于或大于的子树的节点时直接结束否则继续循环判断!什么时候循环结束呢?当父亲到最后一层此时孩子的位置再*2+1就超过数组长度结束掉!但这里要注意一点就是,该节点有左孩子不一定有右孩子,所以在找较小或较大的那个孩子时一定要判断他的右孩子是否存在即*2+1 是否 < n

具体调整过程:

这里就是一个他的右孩子不存在的情况(会导致越界):

代码实现:


//向下调整算法
void AdjustDown(HPDataType* a, int n, int parent)
{
	int child = parent * 2 + 1;//假设第一个孩子就是要交换的那个孩子
	while (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 = parent * 2 + 1;//孩子到他孩子的位置
		}
		else
		{
			break;//当父亲和孩子相等或父亲大于孩子时结束
		}
	}
}

向下调整算法也是最多调整高度次,最少调整0次。所以,时间复杂度:O(lg(N)),额外使用临时变量的个数为常数个,所以空间复杂度:O(1)

堆插入数据的代码:

void HPPush(HP* p, HPDataType x)
{
	assert(p);
	//判断扩容
	if (p->capacity == p->size)
	{
		int newcapacity = p->capacity == 0 ? 4 : p->capacity * 2;
		HPDataType* tmp = (HPDataType*)realloc(p->a, sizeof(HPDataType) * newcapacity);
		if (tmp == NULL)
		{
			perror("realloc failed");
			exit(-1);
		}

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

	//插入
	p->a[p->size++] = x;
	//向上调整
	AdjustUp(p->a, p->size - 1);
}

时间复杂度是向上调整的复杂度:O(lg(N)), 额外使用的空间个数是常量个即空间复杂度为O(1),

堆的创建

这里创建堆的思路就是把数组中的元素逐一插入到堆中,每次插入都会向上调整,最后就变成了堆!

void Test()
{
	int a[] = { 65,100,70,32,50,60 };
	HP hp;
	HPInit(&hp);
	for (int i = 0; i < sizeof(a) / sizeof(a[0]); i++)
	{
		HPPush(&hp, a[i]);
	}
	HPPrint(&hp);

	HPDestory(&hp);
}

时间复杂度:n个元素,每插入一次为lgN,所以整体是:O(N*lgN)。

空间复杂度:创建堆要为其底层数组开空间,假设数组长度为N,即 O(N)

注意:这里如果想要大堆的话可以在向上调整算法那里把大于号改小于符号即可实现!

堆的删除

堆的物理存储时数组,所以我们本质操作的还是数组!堆的删除是,删除堆顶的数据。但不能直接删掉堆顶的数据,如果直接干掉堆顶的数据剩下的数据就不一定是堆了!下次插入或删除时就得重新建堆(O(N*(N*lgN)),向上调整建堆为O(N*lgN),这里每删一个建一次堆,有N个元素就是O(N*(N*lgN),整体复杂度会变的很高!所以这种方式是不行的。这里的玩法是,第一个与最后一个先交换,然后删除最后一个(--size)然后再让0号位置的元素进行向下调整!时间复杂度:O(lg(N))

代码实现

//删除
void HPPop(HP* p)
{
	assert(p);
	assert(p->size > 0);//数组为空就不要删了
	//先把堆的第一个数据与最后一个数据交换,然后--size
	Swap(&p->a[0], &p->a[p->size - 1]);
	--p->size;
	//向下调整
	AdjustDown(p->a, p->size, 0);
}

判空和返回堆顶数据

//取堆顶的数据
HPDataType HPTop(HP* p)
{
	assert(p);
	assert(p->size > 0);//没有元素
	return p->a[0];
}

//判断是否为空
bool IsEmpty(HP* p)
{
	assert(p);
	return p->size == 0;
}

整体测试一下:

void Test()
{
	int a[] = { 65,100,70,32,50,60 };
	HP hp;
	HPInit(&hp);
	for (int i = 0; i < sizeof(a) / sizeof(a[0]); i++)
	{
		HPPush(&hp, a[i]);
	}
	HPPrint(&hp);

	while (!IsEmpty(&hp))
	{
		printf("%d ", HPTop(&hp));
		HPPop(&hp);
	}

	HPDestory(&hp);
}

这里取堆里的数据并删除后一趟下来后直接变得有序了!但这并不是堆排序!待会再介绍堆排是解释!

堆的全部代码

#pragma once
#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 HPInit(HP* p);
//销毁
void HPDestory(HP* p);
//打印
void HPPrint(HP* p);
//交换
void Swap(HPDataType* a, HPDataType* b);
//向上调整算法
void AdjustUp(HPDataType* a, int child);
//向下调整算法
void AdjustDown(HPDataType*a, int n, int parent);
//插入
void HPPush(HP* p, HPDataType x);
//删除
void HPPop(HP* p);
//取堆顶的数据
HPDataType HPTop(HP* p);
//判断是否为空
bool IsEmpty(HP* p);
#include "Heap.h"

//初始化
void HPInit(HP* p)
{
	assert(p);
	p->a = NULL;
	p->size = 0;
	p->capacity = 0;
}

//销毁
void HPDestory(HP* p)
{
	assert(p);
	free(p->a);
	p->size = p->capacity = 0;
}

//打印
void HPPrint(HP* p)
{
	assert(p);
	for (int i = 0; i < p->size; i++)
	{
		printf("%d ", p->a[i]);
	}
	printf("\n");
}

//交换
void Swap(HPDataType* a, HPDataType* b)
{
	HPDataType tmp = *a;
	*a = *b;
	*b = tmp;
}

//向上调整算法
void AdjustUp(HPDataType* a, int child)
{
	int parent = (child - 1) / 2;//第一次找孩子的父亲
	while (child > 0)//当孩子为0结束
	{
		if (a[child] < a[parent])//孩子比父亲小(小堆)
		{
			Swap(&a[child], &a[parent]);//交换
			child = parent;//孩子到父亲的位置
			parent = (parent - 1) / 2;//父亲到他父亲的位置
		}
		else
		{
			break;//孩子和父亲相等或大于父亲时结束
		}
	}
}

//向下调整算法
void AdjustDown(HPDataType* a, int n, int parent)
{
	int child = parent * 2 + 1;//假设第一个孩子就是要交换的那个孩子
	while (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 = parent * 2 + 1;//孩子到他孩子的位置
		}
		else
		{
			break;//当父亲和孩子相等或父亲大于孩子时结束
		}
	}
}

//插入
void HPPush(HP* p, HPDataType x)
{
	assert(p);
	//判断扩容
	if (p->capacity == p->size)
	{
		int newcapacity = p->capacity == 0 ? 4 : p->capacity * 2;
		HPDataType* tmp = (HPDataType*)realloc(p->a, sizeof(HPDataType) * newcapacity);
		if (tmp == NULL)
		{
			perror("realloc failed");
			exit(-1);
		}

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

	//插入
	p->a[p->size++] = x;
	//向上调整
	AdjustUp(p->a, p->size - 1);
}

//删除
void HPPop(HP* p)
{
	assert(p);
	assert(p->size > 0);//数组为空就不要删了
	//先把堆的第一个数据与最后一个数据交换,然后--size
	Swap(&p->a[0], &p->a[p->size - 1]);
	--p->size;
	//向下调整
	AdjustDown(p->a, p->size, 0);
}

//取堆顶的数据
HPDataType HPTop(HP* p)
{
	assert(p);
	assert(p->size > 0);//没有元素
	return p->a[0];
}

//判断是否为空
bool IsEmpty(HP* p)
{
	assert(p);
	return p->size == 0;
}
#include "Heap.h"

void Test()
{
	int a[] = { 65,100,70,32,50,60 };
	HP hp;
	HPInit(&hp);
	for (int i = 0; i < sizeof(a) / sizeof(a[0]); i++)
	{
		HPPush(&hp, a[i]);
	}
	HPPrint(&hp);
	int k = 5;

	while (!IsEmpty(&hp) && k--)
	{
		printf("%d ", HPTop(&hp));
		HPPop(&hp);
	}

	HPDestory(&hp);
}

int main()
{
	Test();
	return 0;
}

4、堆的应用

堆的应用有两个,一个是堆排序,另一个是TopK问题堆排序就不用说了,就是对一个数组进行排序的。TopK问题也是日常中很常见的问题!例如:你平时点外卖,显示当地烤鸭饭第几的那个排名就是TopK,以及你们全专业前十的人,都是TopK问题!

堆排序

我们上面的堆虽然已经能实现排序了!但我们说他不是堆排序,原因是,你平时对数组排序时没有堆这中数据结构啊!这个结构虽然简单但也有200行,手搓是不是并不划算呀!另一方面即使你搓了出来,堆也是要开辟空间的,会有空间消耗,我们一般的排序是给个数组排出来即可!如何操作呢?这里有两种方式:向上调整建堆+向下调整排序,向下调整建堆+向下调整排序!

关于升降序:升序--->建大堆        降序----->建小堆

我们一般想的是升序建小堆,但如果是小堆取走最小的那一个堆顶数据后其他的数据组成的不一定是堆,要想继续排序就得建堆,我们上面分析过,复杂度变高了!所以这里采用删除的思想把最大或最小的换到最后,然后对前N-i(i=1,2,3...n)个进行向下调整!

向上调整建堆

void HeapSort(HPDataType* a, int n)
{
	//向上调整建堆
	//O(N*lgN)
	for (int i = 1; i < n; i++)
	{
		AdjustUp(a, i);
	}

	//向下调整排序
	//O(N*lgN)
	int end = n - 1;
	while (end)
	{
		Swap(&a[0], &a[end]);
		AdjustDown(a, end, 0);
		--end;
	}
}

这里的向上调整建堆和上面堆的插入是一个思路!时间复杂度是O(lgN*N),向下调整排序的时间复杂度是:O(N*lgN)---->有n个元素,每排好一个一下下标,也就是上面的删除的思路!

向下调整建堆

void HeapSort(HPDataType* a, int n)
{
	//向下调整建堆
	//O(N)
	for (int i = (n - 1) / 2; i >= 0; i--)
	{
		AdjustDown(a, n, i);
	}

	//向下调整排序
	//O(N*lgN)
	int end = n - 1;
	while (end)
	{
		Swap(&a[0], &a[end]);
		AdjustDown(a, end, 0);
		--end;
	}
}

向下调整建堆,就是从倒数第一个元素的父节点开始向下调整为堆!这样越往上层节点的左右子树必定是堆!

向下调整建堆的空间复杂度和上面向上调整建堆的一样!也是O(N*lgN),关于向下调整建堆的时间复杂度是O(N),这里来推到一下!

其实向上调整建堆的时间的复杂度也是这样算的!我们也可以来算一下:

TopK问题

TopK顾名思义就是找到数据集合中的前K个最大或最小的元素,一般情况下数据量都很大!

例如:找世界企业500强,专业前5,游戏中活跃度最高的100位玩家等!而对于以上问题一般正常想到的就是对数据排序。但是如果量非常大的话,内存中可能根本存不下或数据一下子加载不到内存。此时排序就无法解决!这里最佳的解决方案是用堆!思路如下:

1、用数据中的前K个在内存中建堆(前K个最大的,建小堆; 前K个最小的,建大堆)

2、用剩下的n-k个元素依次与堆顶数据比较,不满足则进行替换堆顶元素

3、以上这两步做完后,堆中剩余的K个元素就是最大或最小的前K个

下面我在文件中写如10万个数据,查找前5个最大的为例:

创造数据

//创造数据
void CreateData()
{
	int n = 100000;
	srand((size_t)time(NULL));//产生随机数
	const char* file = "data.txt";//文件名
	FILE* fin = fopen(file, "w");//用fopen打开文件
	if (fin == NULL)
	{
		perror("fopen failed");//打开失败,直接终止程序
		exit(-1);
	}

	for (int i = 0; i < n; i++)
	{
		int x = (rand() + i) % 100000;//产生随机数
		fprintf(fin, "%d\n", x);//把x以%d的形式写入fin指向的文件中
	}

	fclose(fin);//关闭文件
}

可以来看看:

TopK

//TopK
void PrintTopK(const char* filename, int k)
{
	FILE* fout = fopen(filename, "r");//打开文件以读的形式
	if (fout == NULL)
	{
		perror("fopen failed");
		exit(-1);
	}

	int* minheap = (int*)malloc(sizeof(int) * k);//创建k个空间的数组
	if (minheap == NULL)
	{
		perror("malloc failed");
		exit(-1);
	}

	for (int i = 0; i < k; i++)
	{
		fscanf(fout, "%d", &minheap[i]);//从fout指向的文件中以%d形式读取k个,放到数组中
	}

	//向下调整建堆
	for (int i = (k - 2) / 2; i >= 0; i--)
	{
		AdjustDown(minheap, k, i);
	}

	int x = 0;
	while (~fscanf(fout, "%d", &x))//当fscanf没有读取失败即文件中还有数据时一直读取
	{
		//判断是否入堆
		if (x > minheap[0])
		{
			//替换入2堆
			minheap[0] = x;
			AdjustDown(minheap, k, 0);//向下调整
		}
	}

	//输出
	for (int i = 0; i < k; i++)
	{
		printf("%d ", minheap[i]);
	}
	printf("\n");
		
	free(minheap);//释放malloc的空间,防止内存泄露
	fclose(fout);//关闭文件
}

前面这些都是C语言文件那块的基础知识,我加了很详细的注释就不再赘述了!这里来解释一下,为什么最大的K个元素要建立小堆,而最小的K个元素要建立大堆!以及为什么最后剩下的就是TopK元素!

这是TopK最大元素,最小的也是同理!

OK,对上面的TopK测试一下:

但有个问题就是我们如何知道他这个是正确的呢?我们的解决方案就是在data.txt文件中随机位置添加5个较大的值,然后和运行结果对比!

OK,结果一致!这就是TopK问题。

二、二叉树的链式结构

二叉树的链式结构即用链表结构来存储二叉树,这里和完全二叉树不一样没有限制,所有的二叉树都可以用链式结构来存储!我们在上一期二叉树基础介绍过,链式二叉树的组成有三部分:根、左子树、右子树。这里根据二叉树的结构(左右子树又可以分为根和左右子树)可以看出他是很适合用递归结构处理的!

二叉树的遍历

二叉树的遍历即按照某种特殊的规则依次对二叉树的每个节点进行访问的操作(每个节点只能访问一次)!遍历方式有四种(递归):前序遍历、中序遍历、后序遍历和层序遍历!

在遍历之前得现有一棵二叉树,所以先得搞一个二叉树出来!

二叉树的节点声明:

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

二叉树的创建

创建这里我们采用二叉树的前序创建(如果不知道先序,可到下面先看先序)# 表示该节点为NULL,否则表示该节点的值!这里我们在遍历之前,必须得有树,我们可以输入二叉树的先序包含NULL(#)的字符串,然后在去依次遍历字符串创建!

思路:

依次遍历str指向的字符串,当时 # 时跳过,表示该节点为NULL,否则去给该值开节点,然后跳过该字符。并依次用同样的思路创建其左右子树!当左右子树都创建完了返回改根节点!

//创建
BT* CreateBT(char* str, int* i)
{
	if (str[*i] == '#')//如果是#即为NULL
	{
		++(*i);//跳过
		return NULL;//返回当前节点的值为空
	}

	BT* root = (BT*)malloc(sizeof(BT));//否则为该值开空间
	if (root == NULL)
	{
		perror("malloc failed");
		exit(-1);
	}

	root->data = str[*i];//吧改值赋给当前开的节点的数据域
	(*i)++;//跳过当前字符
	root->left = CreateBT(str,i);//继续构建该节点的左子树
	root->right = CreateBT(str,i);//继续构建该节点的右子树

	return root;//构建好了当前子树返回根节点
}

我来画个图解释一下:

前序遍历

二叉树的前序遍历(Preorder Traversal先访问根节点再访问左子树,最后访问右子树的遍历方式!

//前序遍历
void PrveOrder(BT* root)
{
	if (root == NULL)//如果该节点已经为空,直接返回NULL
		return;

	printf("%c ", root->data);//先访问该节点的值
	PrveOrder(root->left);//然后访问其左子树
	PrveOrder(root->right);//访问其右子树
}

这里前、中、后序的递归遍历很类似,我这里画一个前序的具体遍历递归展开图,中后序同理!

中序遍历

二叉树的中序遍历(Inorder Traversal先访问左子树再访问根节点,最后访问右子树的遍历方式!

//中序遍历
void InOrder(BT* root)
{
	if (root == NULL)
		return;

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

后序遍历

二叉树的后序遍历(Postorder Traversal先访问左子树再访问右子树,最后访问根节点的遍历方式!

//后序遍历
void PostOrder(BT* root)
{
	if (root == NULL)
		return;

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

OK,测试一下:

void Test()
{
	char str[100] = { 0 };
	printf("请输入二叉树序列的字符串:> ");
	gets(str);
	int i = 0;
	BT* root = CreateBT(str, &i);
	PrveOrder(root);
	printf("\n");
	InOrder(root);
	printf("\n");
	PostOrder(root);
}

OK,没有问题!我们来介绍一下另外一种遍历---层序遍历!

层序遍历

二叉树的层序遍历即从根节点开始,从上至下一层一层,每一层从左至右依次的访问每个节点的方式!要实现这个算法需要借助我们以前介绍过的一个数据结构--->队列来辅助是实现。

思路:根节点如果不为空,则入队。然后判断队列是否为空,不为空的话,取队头节点的系节点,然后判断取出来的队头节点的左右孩子是否为空,不为空则入队了,否则不入!然后pop一下队头节点,继续执行新队头节点的判断

举个例子画个图来理解一下:

代码实现

//层序遍历
void LevelOrder(BT* root)
{
	Queue q;
	QInit(&q);
	if (root)
		QPush(&q, root);

	while (!QEmpty(&q))
	{
		BT* front = QTop(&q);
		printf("%c ", front->data);

		if (front->left)
			QPush(&q, front->left);

		if (front->right)
			QPush(&q, front->right);

		QPop(&q);
	}
	printf("\n");

	QDestory(&q);
}

二叉树的基本问题

这里的基本问题主要是:求二叉树节点的个数、求二叉树叶子节点的个数、二叉树第k层的节点个数、二叉树的按值查找、二叉树的高度等基本问题。

求二叉树节点的个数

思路:左子树的总结点 + 右子树的总结点 + 根节点

//节点个数
int BTNodeSize(BT* root)
{
	if (root == NULL)
		return 0;
	
	return BTNodeSize(root->left) + BTNodeSize(root->right) + 1;
}

求叶子节点的个数

思路:叶子节点的特征是左右子树都为空。当一个节点的左右子树都为空时就是叶子节点

//叶子节点的个数
int BTLeafNodeSize(BT* root)
{
	if (root == NULL)
		return 0;

	if (!root->left && !root->right)
		return 1;

	return BTLeafNodeSize(root->left) + BTLeafNodeSize(root->right);
}

第K层节点的个数

思路:第K层的节点是根节点的k层,k的孩子的k-1层。所以依次把问题给孩子,逐层-1去找,直到k==1时即为K层的节点。注意的是:必须对K判断大于0

//第k层的节点个数
int BTLevelKSize(BT* root, int k)
{
	assert(k > 0);
	if (root == NULL)
		return 0;

	if (k == 1)
		return 1;

	return BTLevelKSize(root->left, k - 1) + BTLevelKSize(root->right, k -1);
}

二叉树的高度

思路:二叉树的整体高度为左右子树中较高的那一个+根节点

//二叉树的高度
int BTDepth(BT* root)
{
	if (root == NULL)
		return 0;

	int Left = BTDepth(root->left);
	int Right = BTDepth(root->right);

	return Left > Right ? Left + 1 : Right + 1;
}

二叉树按值查找

思路:从根节点开始查找,找到了返回节点地址,没有找到,继续去左右子树中去找

//按值查找
BT* BTFind(BT* root, BTDataType x)
{
	if (root == NULL)
		return NULL;

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

	return BTFind(root->left, x) ? BTFind(root->left, x) : BTFind(root->right, x);
}

这种写法可能可读性不好,也可以用下面这种!

BT* BTFind(BT* root, int x)
{
	if (root == NULL)
		return NULL;

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

	BT* ret = NULL;
	ret = BTFind(root->left, x);
	if (ret)
		return ret;

	ret = BTFind(root->right, x);
	if (ret)
		return ret;

	return NULL;
}

或这种也可以:

//按值查找
BT* BTFind(BT* root, BTDataType x)
{
	if (root == NULL)
		return NULL;

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

	BT* ret = BTFind(root->left, x);
	if (ret)
		return ret;

	return BTFind(root->right, x);
}

判断是否是完全二叉树

思路:完全二叉树的特点是前k-1层是满二叉树,第k层的节点是连续的(必须是先左孩子,在右孩子,没有左孩子一定没有右孩子)。所以根据这一特性,当我们用层序遍历节点时,只要遇到NULL直接终止层序遍历。然后再去看队列中是否还有非空的节点,如果有就是非完全二叉树,否则就是完全二叉树!注意:当找到对类中还有非空节点时,在返回false之前需要把队列的空间释放了,否则会造成内存泄漏!!

//判断是否是完全二叉树
bool IsCompleteBT(BT* root)
{
	Queue q;
	QInit(&q);
	if (root)
		QPush(&q, root);

	while (!QEmpty(&q))
	{
		BT* front = QTop(&q);

		if (front == NULL)
		{
			break;
		}

		QPush(&q, root->left);
		QPush(&q, root->right);

		QPop(&q);
	}

	while (!QEmpty(&q))
	{
		BT* front = QTop(&q);
		if (front)
		{
			QDestory(&q);
			return false;
		}
		QPop(&q);
	}

	QDestory(&q);
	return true;
}

二叉树的销毁

思路:先销毁左子树,然后销毁右子树,最后在销毁根

//二叉树的销毁
void BTDestory(BT* root)
{
	if (root == NULL)
		return;

	BTDestory(root->left);
	BTDestory(root->right);
	free(root);
}

测试一下:

链式存储的全部代码:

#define _CRT_SECURE_NO_WARNINGS 1
#pragma once
#include <stdio.h>
#include <stdlib.h>
#include <assert.h>
#include <stdbool.h>

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

//创建
BT* CreateBT(char* str, int* i);
//前序遍历
void PrveOrder(BT* root);
//中序遍历
void InOrder(BT* root);
//后序遍历
void PostOrder(BT* root);
//层序遍历
void LevelOrder(BT* root);
//节点个数
int BTNodeSize(BT* root);
//叶子节点的个数
int BTLeafNodeSize(BT* root);
//第k层的节点个数
int BTLevelKSize(BT* root, int k);
//二叉树的高度
int BTDepth(BT* root);
//按值查找
BT* BTFind(BT* root, BTDataType x);
//判断是否是完全二叉树
bool IsCompleteBT(BT* root);
//二叉树的销毁
void BTDestory(BT* root);
#include "BinaryTree.h"
#include "Queue.h"
//创建
BT* CreateBT(char* str, int* i)
{
	if (str[*i] == '#')
	{
		++(*i);
		return NULL;
	}

	BT* root = (BT*)malloc(sizeof(BT));
	if (root == NULL)
	{
		perror("malloc failed");
		exit(-1);
	}

	root->data = str[*i];
	(*i)++;
	root->left = CreateBT(str,i);
	root->right = CreateBT(str,i);

	return root;
}

//前序遍历
void PrveOrder(BT* root)
{
	if (root == NULL)
		return;

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

//中序遍历
void InOrder(BT* root)
{
	if (root == NULL)
		return;

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

//后序遍历
void PostOrder(BT* root)
{
	if (root == NULL)
		return;

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

//层序遍历
void LevelOrder(BT* root)
{
	Queue q;
	QInit(&q);
	if (root)
		QPush(&q, root);

	while (!QEmpty(&q))
	{
		BT* front = QTop(&q);
		printf("%c ", front->data);

		if (front->left)
			QPush(&q, front->left);

		if (front->right)
			QPush(&q, front->right);

		QPop(&q);
	}
	printf("\n");

	QDestory(&q);
}

//节点个数
int BTNodeSize(BT* root)
{
	if (root == NULL)
		return 0;
	
	return BTNodeSize(root->left) + BTNodeSize(root->right) + 1;
}

//叶子节点的个数
int BTLeafNodeSize(BT* root)
{
	if (root == NULL)
		return 0;

	if (!root->left && !root->right)
		return 1;

	return BTLeafNodeSize(root->left) + BTLeafNodeSize(root->right);
}

//第k层的节点个数
int BTLevelKSize(BT* root, int k)
{
	assert(k > 0);
	if (root == NULL)
		return 0;

	if (k == 1)
		return 1;

	return BTLevelKSize(root->left, k - 1) + BTLevelKSize(root->right, k -1);
}

//二叉树的高度
int BTDepth(BT* root)
{
	if (root == NULL)
		return 0;

	int Left = BTDepth(root->left);
	int Right = BTDepth(root->right);

	return Left > Right ? Left + 1 : Right + 1;
}

//按值查找
BT* BTFind(BT* root, BTDataType x)
{
	if (root == NULL)
		return NULL;

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

	BT* ret = BTFind(root->left, x);
	if (ret)
		return ret;

	return BTFind(root->right, x);
}


//BT* BTFind(BT* root, int x)
//{
//	if (root == NULL)
//		return NULL;
//
//	if (root->data == x)
//		return root;
//
//	BT* ret = NULL;
//	ret = BTFind(root->left, x);
//	if (ret)
//		return ret;
//
//	ret = BTFind(root->right, x);
//	if (ret)
//		return ret;
//
//	return NULL;
//}


//判断是否是完全二叉树
bool IsCompleteBT(BT* root)
{
	Queue q;
	QInit(&q);
	if (root)
		QPush(&q, root);

	while (!QEmpty(&q))
	{
		BT* front = QTop(&q);

		if (front == NULL)
		{
			break;
		}

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

		QPop(&q);
	}

	while (!QEmpty(&q))
	{
		BT* front = QTop(&q);
		if (front)
		{
			QDestory(&q);
			return false;
		}
		QPop(&q);
	}

	QDestory(&q);
	return true;
}

//二叉树的销毁
void BTDestory(BT* root)
{
	if (root == NULL)
		return;

	BTDestory(root->left);
	BTDestory(root->right);
	free(root);
}

队列代码

#include "Queue.h"

//初始化
void QInit(Queue* p)
{
	assert(p);
	p->head = p->tail = NULL;
	p->size = 0;
}

//销毁
void QDestory(Queue* p)
{
	assert(p);
	QNode* cur = p->head, *next = NULL;
	while (cur)
	{
		next = cur->next;
		free(cur);
		cur = next;
	}
	p->head = p->tail = NULL;
	p->size = 0;
}

//开一个新节点
QNode* BuyNode(QDataType x)
{
	QNode* newnode = (QNode*)malloc(sizeof(QNode));
	if (newnode == NULL)
	{
		perror("malloc failed");
		exit(-1);
	}
	newnode->data = x;
	newnode->next = NULL;

	return newnode;
}

//入队列
void QPush(Queue* p, QDataType x)
{
	assert(p);
	QNode* node = BuyNode(x);
	if (p->head == NULL)
	{
		p->head = p->tail = node;
	}
	else
	{
		p->tail->next = node;
		p->tail = node;
	}
	p->size++;
}

//出队列
void QPop(Queue* p)
{
	assert(p);
	assert(p->head);

	QNode* next = p->head->next;
	free(p->head);
	p->head = next;
	p->size--;
}

//获取队列头的数据
QDataType QTop(Queue* p)
{
	assert(p);
	assert(p->size > 0);

	return p->head->data;
}

//获取队列尾的数据
QDataType QTail(Queue* p)
{
	assert(p);
	assert(p->size > 0);

	return p->tail->data;
}

//是否为空
bool QEmpty(Queue* p)
{
	assert(p);
	return p->size == 0;
}

//获取队列的元素个数
int QSize(Queue* p)
{
	assert(p);
	return p->size;
}
#pragma once
#include <stdio.h>
#include <stdlib.h>
#include <assert.h>
#include <stdbool.h>

typedef struct BinaryTreeNode* QDataType;
typedef struct QListNode
{
	QDataType data;
	struct QListNode* next;
}QNode;

typedef struct Queue
{
	QNode* head;
	QNode* tail;
	int size;
}Queue;

//初始化
void QInit(Queue* p);

//销毁
void QDestory(Queue* p);

//开一个新节点
QNode* BuyNode(QDataType x);

//入队列
void QPush(Queue* p, QDataType x);

//出队列
void QPop(Queue* p);

//获取队列头的数据
QDataType QTop(Queue* p);

//获取队列尾的数据
QDataType QTail(Queue* p);

//是否为空
bool QEmpty(Queue* p);

//获取队列的元素个数
int QSize(Queue* p);

OK,本期分享就到这里!好兄弟,我们下期再见!

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

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

相关文章

性能压力测试的优势与重要性

性能压力测试是软件开发过程中至关重要的一环&#xff0c;它通过模拟系统在极限条件下的运行&#xff0c;以评估系统在正常和异常负载下的表现。这种测试为确保软件系统的可靠性、稳定性和可伸缩性提供了关键信息。下面将探讨性能压力测试的优势以及为什么在软件开发中它具有不…

GMS CTS测试命令汇总

目录 跑CTS之前的准备 样机环境要求 跑各模块版本要求 CTS 简介 复测上轮的失败项 多台设备测试 单跑指定模块和测试用例 GTS VTS STS GSI 获取fingerprint 跑CTS之前的准备 样机环境要求 1、打开stay wake&#xff08;保持屏幕常亮&#xff09;、OEM unlocking、…

Java-Review

题型分值总分分布简答 5 ∗ 8 ′ 5*8 5∗8′ 4 0 ′ 40 40′面向对象、异常处理、多线程、输入输出处理程序分析和补全 3 ∗ 1 0 ′ 3*10 3∗10′ 3 0 ′ 30 30′异常处理、Collection、图形化界面、输入输出处理编程 2 ∗ 1 5 ′ 2*15 2∗15′ 3 0 ′ 30 30′Collections、多线…

腾讯云服务器怎么样好用吗?腾讯云服务器好用吗?

大家好&#xff01;今天我们要来聊聊腾讯云服务器怎么样&#xff0c;好用吗&#xff1f;对于这个问题&#xff0c;我的答案是非常肯定的——好用&#xff01; 那么&#xff0c;腾讯云服务器究竟好在哪里呢&#xff1f; 首先&#xff0c;它的功能非常强大。它不仅能够提供云存…

2023.11.17-hive调优的常见方式

目录 0.设置hive参数 1.数据压缩 2.hive数据存储格式 3.fetch抓取策略 4.本地模式 5.join优化操作 6.SQL优化(列裁剪,分区裁剪,map端聚合,count(distinct),笛卡尔积) 6.1 列裁剪: 6.2 分区裁剪: 6.3 map端聚合(group by): 6.4 count(distinct): 6.5 笛卡尔积: 7…

2023年中国涂料树脂需求量、市场规模及行业竞争现状分析[图]

涂料用树脂是涂料的主要原材料&#xff0c;是涂料的主要成膜物&#xff0c;且了为涂料成品提供耐醇、耐磨、耐高温、耐高湿、减少涂料在涂装完成后的损耗、保持涂装后外观以及性状的稳定性等功能。 根据生产产品的性状不同&#xff0c;其下游产品&#xff0c;即涂料成品广泛应用…

云网络流量分析工具的关键优势有哪些?

在当今数字化的时代&#xff0c;企业依赖云计算和网络服务以实现高效运营。随着云网络的复杂性不断增加&#xff0c;对网络流量的分析变得至关重要。云网络流量分析工具应运而生&#xff0c;为管理员提供了深入洞察、实时监控的能力。本文将探讨此工具的关键优势以及它们在现代…

君正X2100 读取CHIP_ID

每个处理器会有一个唯一的ID&#xff0c;这个ID可用做产品序列号&#xff0c;或其它。 X21000的CHIP_ID存放于芯片内部的efuse中&#xff0c;efuse是一次性可可编程存储器&#xff0c;初始值为全0&#xff0c;只能将0改为1&#xff0c;不能将1改为0。芯片出厂前会被写入一些信…

修改YOLOv5的模型结构第二弹

&#x1f368; 本文为&#x1f517;365天深度学习训练营 中的学习记录博客&#x1f356; 原作者&#xff1a;K同学啊 | 接辅导、项目定制&#x1f680; 文章来源&#xff1a;K同学的学习圈子 上节说到了通过修改YOLOv5的common.py来修改模型的结构&#xff0c;修改的是模块的内…

2023年【陕西省安全员B证】考试题库及陕西省安全员B证找解析

题库来源&#xff1a;安全生产模拟考试一点通公众号小程序 陕西省安全员B证考试题库是安全生产模拟考试一点通生成的&#xff0c;陕西省安全员B证证模拟考试题库是根据陕西省安全员B证最新版教材汇编出陕西省安全员B证仿真模拟考试。2023年【陕西省安全员B证】考试题库及陕西省…

分布式事务seata的使用

分布式事务介绍 在微服务架构中&#xff0c;完成某一个业务功能可能需要横跨多个服务&#xff0c;操作多个数据库。这就涉及到到了分布式事务&#xff0c;需要操作的资源位于多个资源服务器上&#xff0c;而应用需要保证对于多个资源服务器的数据操作&#xff0c;要么全部成功&…

深度学习_14_单层|多层感知机及代码实现

单层感知机&#xff1a; 功能&#xff1a; 能完成二分类问题 问题&#xff1a; 模型训练采用X*W b训练出模型&#xff0c;对数据只进行了一层处理&#xff0c;也就是说训练出来的模型仅是个线性模型&#xff0c;它无法解决XOR问题&#xff0c;所以模型在训练效果上&#xf…

cookie机制 + java 案例

目录 为什么会有cookie?? cookie从哪里来的&#xff1f;&#xff1f; cookie到哪里去&#xff1f;&#xff1f; cookie有啥用&#xff1f;&#xff1f; session HttpServletRequest类中的相关方法 简单的实现cookie登录功能 实现登录页面 实现servlet逻辑 实现生成主…

【Spring】依赖注入方式,DI的方式

这里写目录标题 1. setter注入在一个类中注入引用类型在一个类中注入简单类型 2. 构造器注入在一个类中注入引用类型在一个类中注入简单类型 3. 依赖注入方式选择4. 依赖自动装配按类型注入按名称注入 5. 集合注入 1. setter注入 在一个类中注入引用类型 回顾一下之前setter注…

Python基础:输入输出详解-输出字符串格式化

Python中的输入和输出是编程中非常重要的方面。 1. 输入输出简单介绍 1.1 输入方式 Python中的输入可以通过input()函数从键盘键入&#xff0c;也可以通过命令行参数或读取文件的方式获得数据来源。 1&#xff09;input()示例 基本的input()函数&#xff0c;会将用户在终端&…

力扣栈与队列--总结篇

前言 八道题&#xff0c;没想到用了五天。当然需要时间的沉淀&#xff0c;但是一天不能啥也不干啊&#xff01; 内容 首先得熟悉特点和基本操作。 栈与队列在计算机底层中非常重要&#xff0c;这就是为什么要学好数据结构。 可视化的软件例如APP、网站之类的&#xff0c;都…

Kotlin原理+协程基本使用

协程概念 协程是Coroutine的中文简称&#xff0c;co表示协同、协作&#xff0c;routine表示程序。协程可以理解为多个互相协作的程序。协程是轻量级的线程&#xff0c;它的轻量体现在启动和切换&#xff0c;协程的启动不需要申请额外的堆栈空间&#xff1b;协程的切换发生在用…

AVL树的底层实现

文章目录 什么是AVL树&#xff1f;平衡因子Node节点插入新节点插入较高左子树的左侧新节点插入较高左子树的右侧新节点插入较高右子树的左侧新节点插入较高右子树的右侧 验证是否为平衡树二叉树的高度AVL的性能 什么是AVL树&#xff1f; AVL树又称平衡二叉搜索树&#xff0c;相…

腾讯云服务器便宜吗?腾讯云服务器怎么买便宜?附优惠链接

首先&#xff0c;咱们来看一下大家最关心的一个问题&#xff1a;“腾讯云服务器便宜吗&#xff1f;”我的答案是&#xff1a;“YES&#xff01;它真的很便宜&#xff01;”比如&#xff0c;轻量2核2G3M服务器&#xff0c;1年只需要88元&#xff0c;是不是很划算&#xff1f;再比…

实例解释遇到前端报错时如何排查问题

前端页面报错&#xff1a; 1、页面报错500&#xff0c;首先我们可以知道是服务端的问题&#xff0c;需要去看下服务端的报错信息&#xff1a; 2、首先我们查看下前端是否给后端传了id: 我们可以看到接口是把ID返回了&#xff0c;就需要再看下p_id是什么情况了。 3、我们再次请…