DS二叉树的存储

前言

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

本期内容介绍

二叉树的顺序结构

堆的概念以及结构

堆的实现

堆的应用

二叉树的链式结构

二叉树的遍历

二叉树的基本问题

一、二叉树的顺序结构

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,本期分享就到这里!好兄弟,我们下期再见!

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

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

相关文章

Unity优化(1)——合并Mesh的两种方法

在某些移动端项目中&#xff0c;对于DrawCall的要求是很严格的&#xff0c;我们一般查看DrawCall可以通过Statistics里面的Batches进行查看&#xff0c;一般对于移动设备的Batches要控制在200左右比较合适&#xff0c;所以降低Batches是很重要的。 我们常常会遇到一个物体下挂载…

【Mysql学习笔记】1 - Mysql入门

一、Mysql5.7安装配置 下载后会得到zip 安装文件解压的路径最好不要有中文和空格这里我解压到 D:\hspmysql\mysql-5.7.19-winx64 目录下 【根据自己的情况来指定目录,尽量选择空间大的盘】 添加环境变量 : 电脑-属性-高级系统设置-环境变量&#xff0c;在Path 环境变量增加mysq…

基于模拟退火算法优化概率神经网络PNN的分类预测 - 附代码

基于模拟退火算法优化概率神经网络PNN的分类预测 - 附代码 文章目录 基于模拟退火算法优化概率神经网络PNN的分类预测 - 附代码1.PNN网络概述2.变压器故障诊街系统相关背景2.1 模型建立 3.基于模拟退火优化的PNN网络5.测试结果6.参考文献7.Matlab代码 摘要&#xff1a;针对PNN神…

如何使你的软文更有质感?媒介盒子教你几招

软文作为企业推广的常用方式能够让企业以较低成本获得高转化率&#xff0c;然而在推广过程中&#xff0c;企业因为撰写的文案质量不高&#xff0c;无法吸引用户关注&#xff0c;甚至没给用户任何印象&#xff0c;因此如何写出质感文案是软文推广过程中需要着重关注的点&#xf…

MySQL事务特性原理

文章目录 事务四特性预备知识checkpoint机制redo日志redo的流程事务提交后什么时候进行刷盘 undo日志&#xff1a;数据还未被修改、也是备份Undo日志的作用undo的存储结构回滚段与事务回滚段中的数据分类undo的类型undo log的生命周期 MVCC一、 原子性原理如何通过undo日志实现…

菲律宾shopee怎么推广?shopee菲律宾站点什么好卖?——站斧浏览器

菲律宾shopee怎么推广 首先&#xff0c;要想在Shopee上成功推广自己的店铺&#xff0c;关键是提升店铺的曝光率。有多种方式可以增加店铺的曝光率&#xff0c;其中之一是使用Shopee提供的广告服务。 Shopee广告分为首页广告和搜索广告两种形式。商家可以根据自己的需求选择适…

Postman:API测试之Postman使用完全指南

Postman是一个可扩展的API开发和测试协同平台工具&#xff0c;可以快速集成到CI/CD管道中。旨在简化测试和开发中的API工作流。 Postman工具有Chrome扩展和独立客户端&#xff0c;推荐安装独立客户端。 Postman有个workspace的概念&#xff0c;workspace分personal和team类型…

【计算机网络学习之路】网络基础1

文章目录 前言一. 计算机网络发展局域网和广域网 二. 网络协议三. OSI七层模型四. TCP/IP四层&#xff08;五层&#xff09;模型五. 计算机体系结构与网络协议栈六. 协议形式及局域网通信数据包封装与分用 七. 跨网络通信八. MAC地址与网络通信的理解结束语 前言 本系列文章是…

plantuml最原始的主题如何设置

在startuml下一行添加 skin rose startuml skin rose:Hello world; :This is defined on several **lines**;enduml 效果如下&#xff1a; plantuml官网地址如下&#xff1a; ​​​​​​使用简单的文字描述画UML图的开源工具。轻松从简单的文字说明创建UML图。也有许多种可…

【proverif】proverif的语法3-认证协议的验证代码-案例分析

proverif-系列文章目录 【proverif】proverif的下载安装和初使用【proverif】proverif的语法1-解决中间人攻击-代码详解【proverif】proverif的语法2-各种密码原语的编码【proverif】proverif的语法3-认证协议的验证代码-案例分析 (本文) 文章目录 proverif-系列文章目录前言一…

【JVM】内存区域划分、类加载机制(双亲委派模型图解)、垃圾回收(可达性分析、分代回收)

一、JVM简介 JVM (Java虚拟机) 是执行Java字节码的虚拟机。它是Java平台的核心&#xff0c;并且为Java代码提供了跨平台的能力。JVM 是一种虚拟的计算机&#xff0c;在其上运行的程序是Java字节码&#xff0c;它提供了Java代码在不同操作系统和硬件平台上执行的能力。JVM 将Ja…

Selenium操作已经打开的Chrome浏览器窗口

Selenium操作已经打开的Chrome浏览器窗口 0. 背景 在使用之前的代码通过selenium操作Chrome浏览器时&#xff0c;每次都要新打开一个窗口&#xff0c;觉得麻烦&#xff0c;所以尝试使用 Selenium 获取已经打开的浏览器窗口&#xff0c;在此记录下过程 本文使用 chrome浏览器来…

云端力量:探索亚马逊云服务器,提升您的业务无限可能

文章目录 前言亚马逊云服务器简介亚马逊云服务器的优势使用步骤Amazon EC2 实例类型常见问题 总结 前言 随着大数据、人工智能和物联网等技术的飞速发展&#xff0c;企业对于高效、灵活和可扩展的计算需求日益增强。在这样的背景下&#xff0c;亚马逊云服务器 以其卓越的性能、…

ubuntu提高 github下载速度

Github一般用于Git的远程仓库&#xff0c;由于服务器位于国外&#xff0c;国内访问速度比较慢&#xff0c;为了提高访问速度&#xff0c;决定绕过DNS域名解析。 获取Github的IP地址 按下ctrl&#xff0b;alt&#xff0b;T打开命令终端&#xff0c;输入&#xff1a; nslookup gi…

电路综合-基于简化实频的集总参数电路匹配2-得出解析解并综合

电路综合-基于简化实频的集总参数电路匹配2-得出解析解并综合 电路综合-基于简化实频的集总参数电路匹配1中介绍了从要匹配的电路结构得到所对应的均衡器的阻抗数值解的过程。我们下一步需要将数值解拟合成正实函数的形式&#xff0c;从而进行电路综合。此处接着这个教程继续。…

redis运维(八)数据类型(一)字符串

一 字符串 说明&#xff1a; 不需要精通,但是得有一个粗略的认识,然后利用help command查看具体使用仅做记录查询 ① 基础概念 说明&#xff1a; ex是用来收敛内存使用率备注&#xff1a; 早期set是不带ex的默认&#xff1a; 不设置ex,是常驻内存 key和value的命名规范 …

<C++> 反向迭代器

我们知道正向迭代器的设计&#xff1a;begin迭代器指向第一个数据&#xff0c;end迭代器指向最后一个数据的下一个位置 。移向下一个数据&#xff0c;解引用得到数据的值&#xff0c;并根据容器储存方式的不同&#xff0c;容器有不同类型的迭代器。 注意&#xff1a;rbegin迭代…

SSH-远程连接服务器

一、理论知识 目前远程连接服务器的主要类型&#xff1a; 文字接口明文传输&#xff1a;Telnet、RSH 等为主&#xff0c;目前非常少用。文字接口加密&#xff1a;SSH 为主&#xff0c;已经取代上述的 Telnet、RSH 等明文传输方式。图形接口&#xff1a;XDMCP&#xff08;X Di…

数据结构树与二叉树的实现

目录 一、普通树的存储结构 1、双亲表示法 2.孩子表示法 二、二叉树 1.二叉树的顺序存储&#xff08;必须是完全二叉树&#xff0c;否则很浪费空间&#xff09; 1&#xff09;结构体 2.二叉树的链式存储 1&#xff09;结构体 2&#xff09;操作 1.创建一颗二叉树 2.创…

window 搭建 MQTT 服务器并使用

1. 下载 安装 mosquitto 下载地址&#xff1a; http://mosquitto.org/files/binary/ win 使用 win32 看自己电脑下载相应版本&#xff1a; 一直安装&#xff1a; 记住安装路径&#xff1a;C:\Program Files\mosquitto 修改配置文件&#xff1a; allow_anonymous false 设置…