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,画个图来解释一下:

DS二叉树的存储_第1张图片

DS二叉树的存储_第2张图片

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位置时就不能在调了(再调就越界了)即就结束了!

DS二叉树的存储_第3张图片

具体调整过程:

DS二叉树的存储_第4张图片

代码实现:

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

DS二叉树的存储_第5张图片

具体调整过程:

DS二叉树的存储_第6张图片

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

DS二叉树的存储_第7张图片

代码实现:


//向下调整算法
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);
}

DS二叉树的存储_第8张图片

时间复杂度: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);
}

DS二叉树的存储_第9张图片

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

堆的全部代码

#pragma once
#include 
#include 
#include 
#include 


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)个进行向下调整!

DS二叉树的存储_第10张图片

向上调整建堆

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

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

DS二叉树的存储_第11张图片

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

DS二叉树的存储_第12张图片

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

DS二叉树的存储_第13张图片

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);//关闭文件
}

可以来看看:

DS二叉树的存储_第14张图片

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元素!

DS二叉树的存储_第15张图片

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

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

DS二叉树的存储_第16张图片

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

DS二叉树的存储_第17张图片

DS二叉树的存储_第18张图片

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

二、二叉树的链式结构

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

DS二叉树的存储_第19张图片

二叉树的遍历

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

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

二叉树的节点声明:

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;//构建好了当前子树返回根节点
}

我来画个图解释一下:

DS二叉树的存储_第20张图片

前序遍历

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

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

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

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

DS二叉树的存储_第21张图片

中序遍历

二叉树的中序遍历(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);
}

DS二叉树的存储_第22张图片

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

层序遍历

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

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

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

DS二叉树的存储_第23张图片

代码实现

//层序遍历
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);
}

DS二叉树的存储_第24张图片

二叉树的基本问题

这里的基本问题主要是:求二叉树节点的个数、求二叉树叶子节点的个数、二叉树第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);
}

测试一下:

DS二叉树的存储_第25张图片

DS二叉树的存储_第26张图片

DS二叉树的存储_第27张图片

链式存储的全部代码:

#define _CRT_SECURE_NO_WARNINGS 1
#pragma once
#include 
#include 
#include 
#include 

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 
#include 
#include 
#include 

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

你可能感兴趣的:(DS初阶,算法,数据结构)