【05_1数据结构】【算法入门_分治】二叉树初阶的基本理解、堆的概念及结构(含二叉树经典笔试题~)

目录

本章重点:

1.树的概念及结构

1.1树的概念

1.2树的相关概念

1.3树的表示

左孩子有兄弟表示法

1.4树在实际中的应用

表示文件系统的目录树结构 

 思维导图

2.二叉树的概念及结构

2.1概念

2.2现实中的二叉树

2.3特殊的二叉树

两个满二叉树、完全二叉树的重要性质!

2.4二叉树的性质

小试牛刀(二叉树题目)

2.5二叉树的存储结构

1.顺序存储

2.链式存储

2.6二叉树父亲节点与孩子节点关系计算

3.二叉树的顺序结构及实现

3.1二叉树的顺序结构

3.2堆的概念及结构


本章重点:

  1. 树的概念及结构
  2. 二叉树概念及结构
  3. 二叉树顺序结构及实现
  4. 二叉树链式结构及实现

说明:

【05_1数据结构】【算法入门_分治】二叉树初阶的基本理解、堆的概念及结构(含二叉树经典笔试题~)_第1张图片

1.树的概念及结构

1.1树的概念

树是一种非线性的数据结构,它是由n(n>=0)个有限结点组成一个具有层次关系集合。把它叫做树是因为它看起来像一棵倒挂的树,也就是说它是根朝上,而叶朝下的。

  • 有一个特殊的结点,称为根结点,根节点没有前驱结点
  • 除根节点外,其余结点被分成M(M>0)个互不相交的集合T1、T2、……、Tm,其中每一个集合Ti(1 <= i <= m)又是一棵结构与树类似的子树。每棵子树的根结点有且只有一个前驱,可以有0个或多个后继。
  • 因此,树是递归定义的。

【05_1数据结构】【算法入门_分治】二叉树初阶的基本理解、堆的概念及结构(含二叉树经典笔试题~)_第2张图片

注意:树形结构中,子树之间不能有交集,否则就不是树形结构。
 

【05_1数据结构】【算法入门_分治】二叉树初阶的基本理解、堆的概念及结构(含二叉树经典笔试题~)_第3张图片

1.2树的相关概念

【05_1数据结构】【算法入门_分治】二叉树初阶的基本理解、堆的概念及结构(含二叉树经典笔试题~)_第4张图片

节点的:一个节点含有的子树的个数称为该节点的度; 如上图:A的为6


叶节点或终端节点:度为0的节点称为叶节点; 如上图:B、C、H、I...等节点为叶节点


非终端节点或分支节点度不为0的节点; 如上图:D、E、F、G...等节点为分支节点


双亲节点或父节点:若一个节点含有子节点,则这个节点称为其子节点的父节点; 如上图:A是B的父节点


孩子节点或子节点:一个节点含有的子树的根节点称为该节点的子节点; 如上图:B是A的孩子节点


兄弟节点:具有相同父节点的节点互称为兄弟节点; 如上图:B、C是兄弟节点


树的度:一棵树中,最大的节点的度称为树的度; 如上图:树的度为6


节点的层次:从根开始定义起,根为第1层,根的子节点为第2层,以此类推;


树的高度或深度树中节点的最大层次; 如上图:树的高度为4


堂兄弟节点双亲在同一层的节点互为堂兄弟;如上图:H、I互为兄弟节点


节点的祖先:从根到该节点所经分支上的所有节点;如上图:A是所有节点的祖先,A、E、G是P的祖先


子孙:以某节点为根的子树中任一节点都称为该节点的子孙。如上图:所有节点都是A的子孙


森林:由m(m>0)棵互不相交的树的集合称为森林;
 

注意:

树的层次有两种写法,都没有错误:

  1. 从1开始,依次类推
  2. 从0开始,依次类推 

【05_1数据结构】【算法入门_分治】二叉树初阶的基本理解、堆的概念及结构(含二叉树经典笔试题~)_第5张图片

习惯上,我们采用从1开始的层次;  如果题目不说,那么我们也采用第一种,从1开始的方式来定义树的层次

除非题目有特指是,层次从0层次开始(空树为 -1),才采用第二种。

1.3树的表示

树结构相对线性表就比较复杂了,要存储表示起来就比较麻烦了,既然保存值域,也要保存结点和结点之间的关系,实际中树有很多种表示方式如:

双亲表示法,孩子表示法以及左孩子右兄弟表示法等。我们这里就简单的了解其中最常用的左孩子右兄弟表示法

首先,我们想想,如何去表示树呢?代码如何实现树的结构呢?

【05_1数据结构】【算法入门_分治】二叉树初阶的基本理解、堆的概念及结构(含二叉树经典笔试题~)_第6张图片

 脑海中冒出第一个想法————定义一个结构体,里面有数据data,有存储子结点的指针,那么问题来了!需要定义多少指针呢???

【05_1数据结构】【算法入门_分治】二叉树初阶的基本理解、堆的概念及结构(含二叉树经典笔试题~)_第7张图片

但是,我们发现了缺陷:

  1. 可能会存在不少的空间浪费
  2. 万一没有限定树的度为多少呢?

那么对于不知道树的度数,我们有改进方案2:

不知道N,没关系,来一个节点,往顺序表里放,不够了就扩容

【05_1数据结构】【算法入门_分治】二叉树初阶的基本理解、堆的概念及结构(含二叉树经典笔试题~)_第8张图片

结构复杂,所以,也不考虑方式2,

除此之外,我们可以考虑结构数组存储结点(数据+父亲节点所在下标) 

【05_1数据结构】【算法入门_分治】二叉树初阶的基本理解、堆的概念及结构(含二叉树经典笔试题~)_第9张图片

但是,上面的方式,各有优缺点,而表示树结构的最优方法,是左孩子右兄弟表示法。

左孩子右兄弟表示法

typedef int DataType;
struct Node
{
	DataType data; // 结点中的数据域
	struct Node* firstChild; // 永远只指向第一个孩子结点
	struct Node* NextBrother; // 指向其下一个兄弟结点
};

【05_1数据结构】【算法入门_分治】二叉树初阶的基本理解、堆的概念及结构(含二叉树经典笔试题~)_第10张图片

 即:

【05_1数据结构】【算法入门_分治】二叉树初阶的基本理解、堆的概念及结构(含二叉树经典笔试题~)_第11张图片

1.4树在实际中的应用

表示文件系统的目录树结构 

【05_1数据结构】【算法入门_分治】二叉树初阶的基本理解、堆的概念及结构(含二叉树经典笔试题~)_第12张图片

 思维导图

【05_1数据结构】【算法入门_分治】二叉树初阶的基本理解、堆的概念及结构(含二叉树经典笔试题~)_第13张图片

2.二叉树的概念及结构

2.1概念

一棵二叉树是结点的一个有限集合,该集合:

  1. 或者为空
  2. 由一个根节点加上两棵别称为左子树和右子树的二叉树组成

【05_1数据结构】【算法入门_分治】二叉树初阶的基本理解、堆的概念及结构(含二叉树经典笔试题~)_第14张图片

从上图可以看出:

  1. 二叉树不存在大于2的结点
  2. 二叉树的子树有左右之分,次序不能颠倒,因此二叉树是有序树

注意:对于任意的二叉树都是由以下几种情况复合而成的:

【05_1数据结构】【算法入门_分治】二叉树初阶的基本理解、堆的概念及结构(含二叉树经典笔试题~)_第15张图片

2.2现实中的二叉树

【05_1数据结构】【算法入门_分治】二叉树初阶的基本理解、堆的概念及结构(含二叉树经典笔试题~)_第16张图片

2.3特殊的二叉树

  1. 满二叉树:一个二叉树,如果每一个层的结点数都达到最大值,则这个二叉树就是满二叉树。                                                        也就是说,如果一个二叉树的层数为K,且结点总数是 2^K - 1,则它就是满二叉树。
  2. 完全二叉树:完全二叉树是效率很高的数据结构,完全二叉树是由满二叉树而引出来的。                                                              对于深度为K的,有n个结点的二叉树,当且仅当其每一个结点都与深度为K的满二叉树中编号从1至n的结点一一对应时称之为完全二叉树。 要注意的是满二叉树是一种特殊的完全二叉树。

【05_1数据结构】【算法入门_分治】二叉树初阶的基本理解、堆的概念及结构(含二叉树经典笔试题~)_第17张图片

由图可得到:

两个满二叉树、完全二叉树的重要性质!

(1)满二叉树中,

  1. 所有的叶子节点都在最后一层。
  2. 所有的分支节点都有两个孩子。

问个问题?满二叉树,给定树的高度,怎么计算有多少结点?

高度是h的满二叉树,共有2^h - 1个结点。

【05_1数据结构】【算法入门_分治】二叉树初阶的基本理解、堆的概念及结构(含二叉树经典笔试题~)_第18张图片

 反过来同理:给出满二叉树的结点个数,求出树的高度

【05_1数据结构】【算法入门_分治】二叉树初阶的基本理解、堆的概念及结构(含二叉树经典笔试题~)_第19张图片

(2)完全二叉树:

  1. 前N - 1层都是满的。
  2. 最后一层不满,但是最后一层从左到右是连续的
  3. 最多只有一个度为1的结点。

 连续性说明:

【05_1数据结构】【算法入门_分治】二叉树初阶的基本理解、堆的概念及结构(含二叉树经典笔试题~)_第20张图片

最多只有一个度为1的结点说明:

【05_1数据结构】【算法入门_分治】二叉树初阶的基本理解、堆的概念及结构(含二叉树经典笔试题~)_第21张图片

2.4二叉树的性质

【05_1数据结构】【算法入门_分治】二叉树初阶的基本理解、堆的概念及结构(含二叉树经典笔试题~)_第22张图片

【05_1数据结构】【算法入门_分治】二叉树初阶的基本理解、堆的概念及结构(含二叉树经典笔试题~)_第23张图片

小试牛刀(二叉树题目)

【05_1数据结构】【算法入门_分治】二叉树初阶的基本理解、堆的概念及结构(含二叉树经典笔试题~)_第24张图片

【05_1数据结构】【算法入门_分治】二叉树初阶的基本理解、堆的概念及结构(含二叉树经典笔试题~)_第25张图片

解释:

【05_1数据结构】【算法入门_分治】二叉树初阶的基本理解、堆的概念及结构(含二叉树经典笔试题~)_第26张图片

【05_1数据结构】【算法入门_分治】二叉树初阶的基本理解、堆的概念及结构(含二叉树经典笔试题~)_第27张图片

【05_1数据结构】【算法入门_分治】二叉树初阶的基本理解、堆的概念及结构(含二叉树经典笔试题~)_第28张图片

2.5二叉树的存储结构

二叉树一般可以使用两种结构存储,一种顺序结构,一种链式结构。

1.顺序存储

顺序结构存储就是使用数组来存储,一般使用数组只适合表示完全二叉树,因为不是完全二叉树会有空间的浪费。而现实中使用中只有堆才会使用数组来存储,关于堆我们后面的章节会专门讲解。二叉树顺序存储在物理上是一个数组,在逻辑上是一颗二叉树

【05_1数据结构】【算法入门_分治】二叉树初阶的基本理解、堆的概念及结构(含二叉树经典笔试题~)_第29张图片

 注意,顺序存储,顺序二字很重要,即使没有数据,数组那个位置空着,也要遵守。

【05_1数据结构】【算法入门_分治】二叉树初阶的基本理解、堆的概念及结构(含二叉树经典笔试题~)_第30张图片

2.链式存储

二叉树的链式存储结构是指,用链表来表示一棵二叉树,即用来指示元素的逻辑关系。 通常的方法是链表中每个结点由三个域组成,数据域和左右指针域,左右指针分别用来给出该结点左孩子和右孩子所在的链结点的存储地址 。链式结构又分为二叉链和三叉链,当前我们学习中一般都是二叉链,后面课程学到高阶数据结构如红黑树等会用到三叉链。

【05_1数据结构】【算法入门_分治】二叉树初阶的基本理解、堆的概念及结构(含二叉树经典笔试题~)_第31张图片

2.6二叉树父亲节点与孩子节点关系计算

假设parent是父亲节点在数组中的下标

  • leftchild = parent * 2 + 1
  • rightchild = parent * 2 + 2

假设孩子下标是child,不管是左孩子还是右孩子

  • parent = (chile - 1) / 2

【05_1数据结构】【算法入门_分治】二叉树初阶的基本理解、堆的概念及结构(含二叉树经典笔试题~)_第32张图片

对于非完全二叉树,也可以使用数组顺序存储,但是可能会浪费空间!!

【05_1数据结构】【算法入门_分治】二叉树初阶的基本理解、堆的概念及结构(含二叉树经典笔试题~)_第33张图片

3.二叉树的顺序结构及实现

3.1二叉树的顺序结构

普通的二叉树是不适合用数组来存储的,因为可能会存在大量的空间浪费。而完全二叉树更适合使用顺序结构存储。现实中我们通常把堆(一种二叉树)使用顺序结构的数组来存储,需要注意的是这里的堆和操作系统虚拟进程地址空间中的堆是两回事一个是数据结构,一个是操作系统中管理内存的一块区域分段。

【05_1数据结构】【算法入门_分治】二叉树初阶的基本理解、堆的概念及结构(含二叉树经典笔试题~)_第34张图片

3.2堆的概念及结构

事先说明数据结构的堆与操作系统的堆不是一个概念!!!:

【05_1数据结构】【算法入门_分治】二叉树初阶的基本理解、堆的概念及结构(含二叉树经典笔试题~)_第35张图片

堆的概念

【05_1数据结构】【算法入门_分治】二叉树初阶的基本理解、堆的概念及结构(含二叉树经典笔试题~)_第36张图片

详细说明:

  • 大堆:树中一个树及子树中,任何一个父亲的值都大于等于孩子的值。
  • 小堆:树中一个树及子树中,任何一个父亲的值都小于等于孩子的值。

应用:

  1. 堆排序
  2. topK问题。在N个数中,找出最大的前K个/找出最小的前K个。

典型案例:

【05_1数据结构】【算法入门_分治】二叉树初阶的基本理解、堆的概念及结构(含二叉树经典笔试题~)_第37张图片

堆的实现

拿最大堆举例(树中一个树及子树中,任何一个父亲的值都大于等于孩子的值。)

定义堆

typedef int HPDataType;
typedef struct Heap
{
	HPDataType* a;
	int size;
	int capacity;
}HP;

初始化堆

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

交换父子结点

//交换父子结点数据
void Swap(HPDataType* px, HPDataType* py)
{
	HPDataType tmp = *px;
	*px = *py;
	*py = tmp;
}

打印堆内数据

//打印堆内数据
void HeapPrint(HP* hp)
{
	for (int i = 0; i < hp->size; ++i)
	{
		printf("%d ", hp->a[i]);
	}
	printf("\n");
}

堆是否为空

//堆为空
bool HeapEmpty(HP* hp)
{
	assert(hp);

	return hp->size == 0;
}

对于插入堆来说,最大堆、最小堆都是向上调整

区别:

最大堆向上调整,插入后的结点数据,要不改变最大堆(ps:定义  最大堆是每个节点的数据值都大于等于其子树数据值),影响了插入节点到根节点那段路径上的结点( 产生变化!):将插入结点作为child,与其父亲parent比较,如果a[child] > a[parent],就交换节点数据值,直到child是根节点,才截至!

最小堆向上调整,插入后的结点数据,要不改变最小堆(ps:定义  最小堆是每个节点的数据值都小于等于其子树数据值),影响了插入节点到根节点那段路径上的结点( 产生变化!):将插入结点作为child,与其父亲parent比较,如果a[child] < a[parent],就交换节点数据值,直到child是根节点,才截至!

代码以最大堆,作为插入堆、删除根节点为例子,后续不再说明:

数据插入堆(拿最大堆举例)

//数据插入堆(   堆的定义:最大堆or最小堆   )
void HeapPush(HP* hp, HPDataType x)
{
	assert(hp);
	if (hp->size == hp->capacity)
	{
		size_t newCapacity = hp->capacity == 0 ? 4 : hp->capacity * 2;
		HPDataType* tmp = realloc(hp->a, sizeof(HPDataType) * newCapacity);
		if (tmp == NULL)
		{
			printf("realloc fail\n");
			exit(-1);
		}
		hp->a = tmp;
		hp->capacity = newCapacity;

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

向上调整

//向上调整   (变成父亲)
void AdjustUp(int* a, int child)
{
	assert(a);

	int parent = (child - 1) / 2;
	//while (parent >= 0)    错误写法,仔细体会!!
	while (child > 0)
	{
		if (a[child] > a[parent])
		{
			/*HPDataType tmp = a[child];
			a[child] = a[parent];
			a[parent] = tmp;*/
			Swap(&a[child], &a[parent]);

			child = parent;
			parent = (child - 1) / 2;
		}
		else
		{
			break;
		}
	}
}

对于删除堆顶数据来说,最大堆、最小堆都是需要向下调整

需要注意的是,根节点,也就是堆顶结点,我们直接删除是不行的!!      需要将最后一个结点与根节点位置互换,因为我们删除尾结点(堆本质上是数组嘛~~~~~)是方便的!!!        然后再进行“  根结点  ” 向下调整...................

区别:

最大堆删除堆顶数据,看是否此时还仍然符合最大堆的要求,若不符合,那么就需要向下调整,调成符合最大堆的形式。

考虑到完全二叉树中,任意一个父亲节点左孩子存在,而右孩子不一定存在(细品!!!),所以我们需要判断,避免造成非法访问数组元素。如果右孩子存在,即:child + 1 < n ,右孩子大于左孩子,即:a[child + 1] > a[ child ]。那么就child指向右孩子。

(即:该代码段是用child指向左右孩子中较大孩子)

如果说,a[child] > a[parent],那么就交换父子结点,然后继续向下调整。

最小堆删除堆顶数据,看是否此时还仍然符合最小堆的要求,若不符合,那么就需要向下调整,调成符合最小堆的形式。

考虑到完全二叉树中,任意一个父亲节点左孩子存在,而右孩子不一定存在(细品!!!),所以我们需要判断,避免造成非法访问数组元素。如果右孩子存在,即:child + 1 < n ,右孩子小于左孩子,即:a[child + 1] < a[ child ]。那么就child指向右孩子。

(即:该代码段是用child指向左右孩子中较小孩子)

如果说,a[child] < a[parent],那么就交换父子结点,然后继续向下调整。

删除堆顶数据

//删除堆顶数据
void HeapPop(HP* hp)
{
	assert(hp);
	assert(!HeapEmpty(hp));

	Swap(&hp->a[0], &hp->a[hp->size - 1]);
	hp->size--;

	//向下调整
	AdjustDown(hp->a, hp->size, 0);
}

向下调整

//向下调整
void AdjustDown(int* a, int n, int parent)
{
	//int left = parent * 2 + 1;
	//int right = parent * 2 + 2;
	//while (parent < n)
	//{
	//	if (a[left] > a[right])
	//	{
	//		Swap(&a[left], &a[parent]);
	//		parent = left;
	//	}
	//	else if (a[left] < a[right])
	//	{
	//		Swap(&a[right], &a[parent]);
	//		parent = right;
	//	}
	//	else
	//		break;
	

	//完全二叉树中,左孩子存在,右孩子有可能不存在,要避免越界访问,需要判断
	int child = parent * 2 + 1;
	//左孩子小于n
	while (child < n)
	{
		//选出左右孩子中大的那一个
		//如果右孩子存在,并且右孩子大于左孩子
		if (child + 1 < n && a[child + 1] > a[child])
		{
			++child;
		}

		//如果大的孩子大于父亲,则交换,并继续向下调整
		if (a[child] > a[parent])
		{
			Swap(&a[child], &a[parent]);
			parent = child;
			child = parent * 2 + 1;
		}
		else
		{
			break;
		}
	}
}

TopK问题(1.建立小堆    2.若比堆顶数据大,则push)

(注:此时的push、pop都是小堆操作,注意区别于上述我们讲的代码(大堆)~~~~)

首先想想为什么要有堆排序?

【05_1数据结构】【算法入门_分治】二叉树初阶的基本理解、堆的概念及结构(含二叉树经典笔试题~)_第38张图片

【05_1数据结构】【算法入门_分治】二叉树初阶的基本理解、堆的概念及结构(含二叉树经典笔试题~)_第39张图片

【05_1数据结构】【算法入门_分治】二叉树初阶的基本理解、堆的概念及结构(含二叉树经典笔试题~)_第40张图片

Heap.h文件代码:

#pragma once
#include
#include
#include
#include

typedef int HPDataType;
typedef struct Heap
{
	HPDataType* a;
	int size;
	int capacity;
}HP;

//初始化堆
void HeapInit(HP* hp);

//交换父子结点数据
void Swap(HPDataType* px, HPDataType* py);

//打印堆内数据
void HeapPrint(HP* hp);

//堆为空
bool HeapEmpty(HP* hp);

//数据插入堆(   堆的定义:最大堆or最小堆   )
void HeapPush(HP* hp, HPDataType x);

//向上调整   (变成父亲)
void AdjustUp(int* a, int child);

//删除堆顶数据
void HeapPop(HP* hp);

//向下调整
void AdjustDown(int* a, int n, int parent);

//获取堆顶数据
HPDataType HeapTop(HP* hp);

Heap.c文件代码

#define _CRT_SECURE_NO_WARNINGS
#include"Heap.h"

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

//交换父子结点数据
void Swap(HPDataType* px, HPDataType* py)
{
	HPDataType tmp = *px;
	*px = *py;
	*py = tmp;
}

//打印堆内数据
void HeapPrint(HP* hp)
{
	for (int i = 0; i < hp->size; ++i)
	{
		printf("%d ", hp->a[i]);
	}
	printf("\n");
}

//堆为空
bool HeapEmpty(HP* hp)
{
	assert(hp);

	return hp->size == 0;
}

//数据插入堆(   堆的定义:最大堆or最小堆   )
void HeapPush(HP* hp, HPDataType x)
{
	assert(hp);
	if (hp->size == hp->capacity)
	{
		size_t newCapacity = hp->capacity == 0 ? 4 : hp->capacity * 2;
		HPDataType* tmp = realloc(hp->a, sizeof(HPDataType) * newCapacity);
		if (tmp == NULL)
		{
			printf("realloc fail\n");
			exit(-1);
		}
		hp->a = tmp;
		hp->capacity = newCapacity;

	}
	hp->a[hp->size] = x;
	hp->size++;

	//向上调整
	AdjustUp(hp->a, hp->size - 1);
}

//向上调整 (小堆)
void AdjustUp(int* a, int child)
{
	assert(a);

	int parent = (child - 1) / 2;
	//while (parent >= 0)    错误写法,仔细体会!!
	while (child > 0)
	{
		//如果孩子小于父亲
		if (a[child] < a[parent])
		{
			/*HPDataType tmp = a[child];
			a[child] = a[parent];
			a[parent] = tmp;*/
			Swap(&a[child], &a[parent]);

			child = parent;
			parent = (child - 1) / 2;
		}
		else
		{
			break;
		}
	}
}

//删除堆顶数据
void HeapPop(HP* hp)
{
	assert(hp);
	assert(!HeapEmpty(hp));

	Swap(&hp->a[0], &hp->a[hp->size - 1]);
	hp->size--;

	//向下调整
	AdjustDown(hp->a, hp->size, 0);
}

//向下调整(小堆)
void AdjustDown(int* a, int n, int parent)
{
	//完全二叉树中,左孩子存在,右孩子有可能不存在,要避免越界访问,需要判断
	int child = parent * 2 + 1;
	//左孩子小于n
	while (child < n)
	{
		//选出左右孩子中小的那一个
		//如果右孩子存在,并且右孩子小于左孩子
		if (child + 1 < n && a[child + 1] < a[child])
		{
			++child;
		}

		//如果小的孩子比父亲还要小,则交换,并继续向下调整
		if (a[child] < a[parent])
		{
			Swap(&a[child], &a[parent]);
			parent = child;
			child = parent * 2 + 1;
		}
		else
		{
			break;
		}
	}
}

//获取堆顶数据
HPDataType HeapTop(HP* hp)
{
	assert(hp);
	assert(!HeapEmpty(hp));

	return hp->a[0];
}

Test.c文件代码

#define _CRT_SECURE_NO_WARNINGS
#include"Heap.h"

void PrintTopK(int* a, int n, int k)
{
	HP hp;
	HeapInit(&hp);
	// 1. 建堆--用a中的k个数创建一个小堆
	for (int i = 0; i < k; ++i)
	{
		HeapPush(&hp, a[i]);//小堆的HeapPush
	}
	
	// 2. 将剩余n-k个元素依次与堆顶数据比较,比他大,就替换他,进堆
	for (int i = k; i < n; ++i)
	{
		if (a[i] > HeapTop(&hp))
		{
			//方法1
			//HeapPop(&hp);
			//HeapPush(&hp, a[i]);

			//方法2
			hp.a[0] = a[i];
			AdjustDown(hp.a, hp.size, 0);
		}
	}
	HeapPrint(&hp);

	//HeapDestroy(&hp);
}
void TestTopk()
{
	int n = 10000;
	int* a = (int*)malloc(sizeof(int) * n);
	srand(time(0));
	for (size_t i = 0; i < n; ++i)
	{
		//随机生成数字,%100w就保证了数字小于100w
		a[i] = rand() % 1000000;
	}
	//手动设置10个大于100w的数字
	a[5] = 1000000 + 1;
	a[1231] = 1000000 + 2;
	a[531] = 1000000 + 3;
	a[5121] = 1000000 + 4;
	a[115] = 1000000 + 5;
	a[2335] = 1000000 + 6;
	a[9999] = 1000000 + 7;
	a[76] = 1000000 + 8;
	a[423] = 1000000 + 9;
	a[3144] = 1000000 + 10;
	PrintTopK(a, n, 10);
}
int main()
{
	TestTopk();
	return 0;
}

运行结果:

堆排序

(升序中的Push、Pop都是对小堆操作,如上述代码;降序即是建立大堆,只需要把Push、Pop对堆操作即可)

//堆排序 - 升序  - 空间复杂度( O(N) )      
void HeapSort(int* a, int n)
{
	HP hp;
	HeapInit(&hp);
	//建立一个小堆
	for (int i = 0; i < n; ++i)
	{
		HeapPush(&hp, a[i]);
	}

	//Pop  N 次
	for (int i = 0; i < n; ++i)
	{
		a[i] = HeapTop(&hp);
		HeapPop(&hp);
	}
	//HeapDestroy(&hp);
}
int main()
{
	int a[] = { 70,56,30,25,15,10,75 };
	for (int i = 0; i < sizeof(a) / sizeof(a[0]); ++i)
	{
		printf("%d ", a[i]);
	}
	printf("\n");

	HeapSort(a, sizeof(a) / sizeof(a[0]));

	for (int i = 0; i < sizeof(a) / sizeof(a[0]); ++i)
	{
		printf("%d ", a[i]);
	}
	printf("\n");
	return 0;
}

该程序空间复杂度是O(N),那么能否优化到O(1)呢??

4.二叉树的链式结构及实现

4.1为什么要存在链式二叉树?

因为并不是所有的二叉树都是特殊的完全二叉树或者满二叉树,普通的二叉树是没有规定的,即:一个节点会在保证二叉树的前提下,后续连接很多节点,如下图所示。这种结构是很复杂的!对于一些增删查改的操作来说,并没有存在的必要,因为链表就可以解决,何必要那么费劲去造一棵树呢?

所以,我们在此不去研究普通二叉树的增删查改操作,而是在普通二叉树的基础上,增加一些性质,去研究它,方便后续更难的树的学习!!

【05_1数据结构】【算法入门_分治】二叉树初阶的基本理解、堆的概念及结构(含二叉树经典笔试题~)_第41张图片

4.2二叉树的遍历

4.2.1前序、中序、后序遍历

学习二叉树结构,最简单的方式就是遍历。所谓二叉树遍历(Traversal)是按照某种特定的规则,依次对二叉树中的节点进行相应的操作,并且每个节点只操作一次。访问结点所做的操作依赖于具体的应用问题。 遍历是二叉树上最重要的运算之一,也是二叉树上进行其它运算的基础。

【05_1数据结构】【算法入门_分治】二叉树初阶的基本理解、堆的概念及结构(含二叉树经典笔试题~)_第42张图片

按照规则,二叉树的遍历有:前序/中序/后序的递归结构遍历:

  1. 前序遍历(Preorder Traversal 亦称先序遍历)——根、左子树、右子树       
  2. 中序遍历(Inorder Traversal)——左子树、根、右子树
  3. 后序遍历(Postorder Traversal)——左子树、右子树、根

【05_1数据结构】【算法入门_分治】二叉树初阶的基本理解、堆的概念及结构(含二叉树经典笔试题~)_第43张图片

【05_1数据结构】【算法入门_分治】二叉树初阶的基本理解、堆的概念及结构(含二叉树经典笔试题~)_第44张图片

【05_1数据结构】【算法入门_分治】二叉树初阶的基本理解、堆的概念及结构(含二叉树经典笔试题~)_第45张图片

注意,遍历就是针对根、左子树、右子树而言!   

前序、中序、后序遍历代码实现(不难,但是递归过程要理解!!)

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

//造一个节点
BTNode* BuyNode(BTDataType x)
{
	BTNode* node = (BTNode*)malloc(sizeof(BTNode));
	if (node == NULL)
	{
		printf("malloc fail!\n");
		exit(-1);
	}
	node->data = x;
	node->left = node->right = NULL;

	return node;
}
//造一棵树
BTNode* CreatBinaryTree()
{
	BTNode* nodeA = BuyNode('A');
	BTNode* nodeB = BuyNode('B');
	BTNode* nodeC = BuyNode('C');
	BTNode* nodeD = BuyNode('D');
	BTNode* nodeE = BuyNode('E');
	BTNode* nodeF = BuyNode('F');
	BTNode* nodeG = BuyNode('G');
	BTNode* nodeH = BuyNode('H');
	BTNode* nodeI = BuyNode('I');
	BTNode* nodeJ = BuyNode('J');
	nodeA->left = nodeB;
	nodeA->right = nodeC;

	nodeB->left = nodeD;
	nodeB->right = nodeE;

	nodeC->left = nodeF;
	nodeC->right = nodeG;

	nodeD->left = nodeH;
	nodeD->right = nodeI;

	nodeG->right = nodeJ;

	return nodeA;
}

// 二叉树前序遍历
void PreOrder(BTNode* root)
{
	if (root == NULL)
	{
		//printf("NULL ");//加上该句,其实是真实的走的过程,因为访问到NULL才算一条路走完!
		return;
	}
	printf("%c ", root->data);
	PreOrder(root->left);
	PreOrder(root->right);
}

// 二叉树中序遍历
void InOrder(BTNode* root)
{
	if (root == NULL)
	{
		//printf("NULL ");//加上该句,其实是真实的走的过程,因为访问到NULL才算一条路走完!
		return;
	}
	InOrder(root->left);
	printf("%c ", root->data);
	InOrder(root->right);
}
// 二叉树后序遍历
void PostOrder(BTNode* root)
{
	if (root == NULL)
	{
		//printf("NULL ");//加上该句,其实是真实的走的过程,因为访问到NULL才算一条路走完!
		return;
	}
	PostOrder(root->left);
	PostOrder(root->right);
	printf("%c ", root->data);
}
int main()
{
	BTNode* root = CreatBinaryTree();
	//PreOrder(root);//前
	//InOrder(root);//中
	PostOrder(root);//后

	return 0;
}

注意:递归过程要深刻理解啊!!!!

【05_1数据结构】【算法入门_分治】二叉树初阶的基本理解、堆的概念及结构(含二叉树经典笔试题~)_第46张图片

4.2.2层序遍历

4.3二叉树节点计数

先说结论:用遍历思想做二叉树的计数是错误的!

代码1错误点:count变量是局部变量,根据函数栈帧的知识,每次调用函数都需要创建此变量count,所以count始终是只执行一次++,即:count = 1!!不能完成计数!

——————啥?改成全局变量或者用static修饰变量变成可以随着调用而改变的静态变量,可行??

//二叉树的节点个数
int BinaryTreeSize(BTNode* root)
{
	if (root == NULL)
	{
		return;
	}
	int count = 0;
	++count;

	BinaryTreeSize(root->left);
	BinaryTreeSize(root->right);
	return count;
}
int main()
{
	BTNode* root = CreatBinaryTree();
	//PreOrder(root);//前
	//InOrder(root);//中
	PostOrder(root);//后
	printf("\n");

	//ABCDEFGHIJ 共10个字母,所以TreeSize = 10才对!我们来验证一下
	printf("TreeSize = %d \n", BinaryTreeSize(root));//1

	return 0;
}

代码2错误点:static修饰,变成静态变量后,的确可以随着函数调用而改变值,我们试了一次,发现可以输出正确结果10。但是我们再次试一次,却发现打印输出了20?           同样的一棵树,我去遍历两次怎么节点数还增加??       两次应该是一样的哇!!

——————static修饰的变量是静态变量,在静态区,不会随着栈帧创建和销毁而去销毁,所以第二次再去变量这棵树,节点个数当然会增加,并且是翻一倍增加,就相当于重复遍历这棵树把节点又加了一遍;而不是仍然保持正确结果,输出10!!

——————可见,静态变量也是不合适的!        难道我们去计数仅仅去计数一次,后面不再去计数了吗?    不合适不合适!这个遍历思想方法去计数思想是错误的!!!!!

//二叉树的节点个数
int BinaryTreeSize(BTNode* root)
{
	if (root == NULL)
	{
		return;
	}
	static int count = 0;
	++count;

	BinaryTreeSize(root->left);
	BinaryTreeSize(root->right);
	return count;
}
int main()
{
	BTNode* root = CreatBinaryTree();
	//PreOrder(root);//前
	//InOrder(root);//中
	PostOrder(root);//后
	printf("\n");

	//ABCDEFGHIJ 共10个字母,所以TreeSize = 10才对!我们来验证一下
	printf("TreeSize = %d \n", BinaryTreeSize(root));//10
	printf("TreeSize = %d \n", BinaryTreeSize(root));//20!?    答案错了!

	return 0;
}

那该怎么去写二叉树计数的代码呢??

——————显然,用遍历思想去计数是不对的。那么我们去传地址就可以!!

//二叉树的节点个数
void BinaryTreeSize(BTNode* root,int* pcount)
{
	if (root == NULL)
	{
		return;
	}
	++(*pcount);
	BinaryTreeSize(root->left,pcount);
	BinaryTreeSize(root->right,pcount);
}
int main()
{
	BTNode* root = CreatBinaryTree();
	//PreOrder(root);//前
	//InOrder(root);//中
	PostOrder(root);//后
	printf("\n");

	int count = 0;
	BinaryTreeSize(root, &count);
	//ABCDEFGHIJ 共10个字母,所以TreeSize = 10才对!我们来验证一下
	printf("TreeSize = %d \n", count);//10
	printf("TreeSize = %d \n", count);//还是10

	return 0;
}

最优化的计算二叉树节点个数的算法(   画递归调用图来理解!!!       要深刻理解!!    )(分治思想

//二叉树的节点个数
int BinaryTreeSize(BTNode* root)
{
	return root == NULL ? 0 : BinaryTreeSize(root->left) + BinaryTreeSize(root->right) + 1;
}
int main()
{
	BTNode* root = CreatBinaryTree();
	//PreOrder(root);//前
	//InOrder(root);//中
	PostOrder(root);//后
	printf("\n");

	//ABCDEFGHIJ 共10个字母,所以TreeSize = 10才对!我们来验证一下
	printf("TreeSize = %d \n", BinaryTreeSize(root));//10
	printf("TreeSize = %d \n", BinaryTreeSize(root));//还是10

	return 0;
}

同样的道理,二叉树的叶子节点也可以这样写:

4.4 二叉树叶子节点计数

//二叉树的叶子节点个数
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);
}
int main()
{
	BTNode* root = CreatBinaryTree();
	//PreOrder(root);//前
	//InOrder(root);//中
	PostOrder(root);//后
	printf("\n");

	//ABCDEFGHIJ 共10个字母,所以TreeSize = 10才对!我们来验证一下
	printf("TreeSize = %d \n", BinaryTreeSize(root));//10
	printf("TreeSize = %d \n", BinaryTreeSize(root));//还是10

	//叶子节点应该是5个
	printf("TreeLeafSize = %d \n", BinaryTreeLeafSize(root));//5

	return 0;
}

4.5二叉树第K层节点个数

比较难以理解!!    思想如下:

【05_1数据结构】【算法入门_分治】二叉树初阶的基本理解、堆的概念及结构(含二叉树经典笔试题~)_第47张图片

比如要求对于这棵树,我们想要求A的第3层,可以抓换成求左子树第2层+右子树第2层的节点数量

左子树第2层,即:求B的第2层,转换成求其(B)左子树的第1层+求其右子树的第1层

.............................

//二叉树第K层节点个数
int BinaryTreeLevelKSize(BTNode* root, int k)
{
	//if (k < 1)
	//{
	//	return 0;
	//}
	assert(k >= 1);

	if (root == NULL)
	{
		return 0;
	}
	if (k == 1)
	{
		return 1;
	}

	//root不等于空,k也不等于1,说明root这棵树的第k节点在子树里面
	//转换成求左右子树的第k-1层的节点数量
	return BinaryTreeLevelKSize(root->left, k - 1) + BinaryTreeLevelKSize(root->right, k - 1);
}
int main()
{
	BTNode* root = CreatBinaryTree();
	//PreOrder(root);//前
	//InOrder(root);//中
	PostOrder(root);//后
	printf("\n");

	//ABCDEFGHIJ 共10个字母,所以TreeSize = 10才对!我们来验证一下
	printf("TreeSize = %d \n", BinaryTreeSize(root));//10
	printf("TreeSize = %d \n", BinaryTreeSize(root));//还是10

	//叶子节点应该是5个
	printf("TreeLeafSize = %d \n", BinaryTreeLeafSize(root));//5

	//二叉树第K层节点的数量    第4层应该是3个节点
	printf("TreeLevelKSizeSize = %d \n", BinaryTreeLevelKSize(root, 4));//3

	return 0;
}

很难理解!!       画递归展开图来深刻理解一下!!

【05_1数据结构】【算法入门_分治】二叉树初阶的基本理解、堆的概念及结构(含二叉树经典笔试题~)_第48张图片

4.6二叉树的深度/高度

思想依旧是分治思想(大问题分解成小问题)(层层往下调结果!最后汇总!

树的高度/深度 = 左子树深度和右子树深度中较大的那个 + 1

想想看这样写,代码的缺陷在哪里??

//二叉树的深度/高度
int BinartTreeDepth(BTNode* root)
{
	if (root == NULL)
	{
		return 0;
	}
	return BinartTreeDepth(root->left) > BinartTreeDepth(root->right) ? BinartTreeDepth(root->left) + 1 : BinartTreeDepth(root->right) + 1;
}

解释:

很浪费的函数栈帧调用,对于一个BinartTreeDepth(root->left)就得调用到底,不断地在栈区积累;后面的BinartTreeDepth(root->right)也是需要不断在栈区调用。然后最后的BinartTreeDepth(root->left) + 1 : BinartTreeDepth(root->right) + 1也是还需要再次调用函数栈帧,因为起初的调用并没有被保存,函数栈帧随着调用结束而被销毁!!      所以说,极大地恶劣地浪费了栈的内存,而且大量的重复计算!   所以,我们可以将调用后的先保存一下,方便后面再去重复计算,浪费栈帧

优化后的求二叉树的深度/高度的算法

//二叉树的深度/高度
int BinartTreeDepth(BTNode* root)
{
	if (root == NULL)
	{
		return 0;
	}

	int leftDepth = BinartTreeDepth(root->left);
	int rightDepth = BinartTreeDepth(root->right);

	return leftDepth > rightDepth ? leftDepth + 1 : rightDepth + 1;
}
int main()
{
	BTNode* root = CreatBinaryTree();
	//PreOrder(root);//前
	//InOrder(root);//中
	PostOrder(root);//后
	printf("\n");

	//ABCDEFGHIJ 共10个字母,所以TreeSize = 10才对!我们来验证一下
	printf("TreeSize = %d \n", BinaryTreeSize(root));//10
	printf("TreeSize = %d \n", BinaryTreeSize(root));//还是10

	//叶子节点应该是5个
	printf("TreeLeafSize = %d \n", BinaryTreeLeafSize(root));//5

	//二叉树第K层节点的数量    第4层应该是3个节点
	printf("TreeLevelKSizeSize = %d \n", BinaryTreeLevelKSize(root, 4));//3

	//二叉树的深度
	printf("TreeDepth = %d \n", BinartTreeDepth(root));//

	return 0;
}

4.7二叉树查找值为x的节点

左数有值为x的节点就停止查找了,不用再去找了!

//二叉树查找值为x的节点
BTNode* BinaryTreeFind(BTNode* root, BTDataType x)
{
	if (root == NULL)
		return NULL;
	if (root->data == x)
		return root;
	BTNode* leftRet = BinaryTreeFind(root->left, x);
	if (leftRet)
		return leftRet;
	BTNode* rightRet = BinaryTreeFind(root->right, x);
	if (rightRet)
		return rightRet;
	return NULL;
}

【05_1数据结构】【算法入门_分治】二叉树初阶的基本理解、堆的概念及结构(含二叉树经典笔试题~)_第49张图片

 注意:尽量不要出现如下这种情况的代码!      因为这种情况的代码,左子树和右子树都找了,找的效率不高,而且没必要都找了,我们需要找到就不用再去找了,比如:先找左子树有无x值,有则停止继续找;没有,则再去找右子树。     

5.二叉树的OJ题

5.1单值二叉树

【05_1数据结构】【算法入门_分治】二叉树初阶的基本理解、堆的概念及结构(含二叉树经典笔试题~)_第50张图片

 总体思想还是分治思想:大事化小,去递归调用

(交换律)

/**
 * Definition for a binary tree node.
 * struct TreeNode {
 *     int val;
 *     struct TreeNode *left;
 *     struct TreeNode *right;
 * };
 */

bool isUnivalTree(struct TreeNode* root)
{
    if(root == NULL)
        return true;
    
    //如果root->left不等于NULL,即:有值;但是值不等于val
    if(root->left && root->left->val != root->val)
    {
        return false;
    }

    //如果root->right不等于NULL,即:有值;但是值不等于val
    if(root->right && root->right->val != root->val)
    {
        return false;
    }

    return isUnivalTree(root->left) && isUnivalTree(root->right);
}

另一个思路:前序遍历,逐一比较

int PrevOrder(struct TreeNode* root , int val)
{
	if (root == NULL)
	{
		return 0;
	}
	if (root->val != val)
	{
		return 1;
	}
	return PrevOrder(root->left, val) + PrevOrder(root->right, val);
}
bool isUnivalTree(struct TreeNode* root)
{
	//递归计数法,遇到不相等的就返回1,相等就继续
	int data = root->val;
	int tmp = PrevOrder(root, data);
	if (tmp == 0)
	{
		return true;
	}
	else
	{
		return false;
	}
}

5.2二叉树的前序遍历

【05_1数据结构】【算法入门_分治】二叉树初阶的基本理解、堆的概念及结构(含二叉树经典笔试题~)_第51张图片

【05_1数据结构】【算法入门_分治】二叉树初阶的基本理解、堆的概念及结构(含二叉树经典笔试题~)_第52张图片

思想:基本的前序遍历:根、左、右

需要注意的是:

  1. 返回值要求是malloc出来的,所以malloc多大的空间是个问题。空间大了浪费甚至超时,空间小了不够用。所以我们想到了先用TreeSize计算出节点个数,精准开辟空间大小
  2. 前序遍历,我们要思考是否能直接递归调用preorderTraversal()函数呢?————递归调用的话,其中的输出型参数int* returnSize该怎么办?所以,我们构造出一个前序遍历递归的子函数——_preorderTraversal()【子函数最好是原函数前面加个_】,来方便前序遍历将数放进数组中,而先不用考虑输出型参数int* returnSize的问题。
  3. 注意,输出型参数*returnSize的作用是返回的时候,事先确定好返回的参数有几个。因为我们返回值仅仅能返回一个值,而加上输出型参数*returnSize后,即可在return a后确定a数组种返回的数据个数。

参考代码,试想一下这样写的bug在哪??

【05_1数据结构】【算法入门_分治】二叉树初阶的基本理解、堆的概念及结构(含二叉树经典笔试题~)_第53张图片

【05_1数据结构】【算法入门_分治】二叉树初阶的基本理解、堆的概念及结构(含二叉树经典笔试题~)_第54张图片

 原因我们可以通过递归调用图来深究一下:

【05_1数据结构】【算法入门_分治】二叉树初阶的基本理解、堆的概念及结构(含二叉树经典笔试题~)_第55张图片

所以,我们看出,i这个参数,在每次调用的时候,要有所区别,他是形参,不会随着值得改变而改变。

因为,我们考虑传指针来避免这个错误!

正确代码

/**
 * Definition for a binary tree node.
 * struct TreeNode {
 *     int val;
 *     struct TreeNode *left;
 *     struct TreeNode *right;
 * };
 */

/**
 * Note: The returned array must be malloced, assume caller calls free().
 */
 //计算出节点个数,方便后续开辟节点空间,避免过多开辟或过少开辟
int TreeSize(struct TreeNode* root)
{
    return root == NULL ? 0 : TreeSize(root->left) + TreeSize(root->right) + 1;
} 
//前序遍历,将值放在数组里面
void _preorderTraversal(struct TreeNode* root, int* a, int* pi)
{
    if(root == NULL)
        return ;

    a[(*pi)++] = root->val;
    _preorderTraversal(root->left, a, pi);
    _preorderTraversal(root->right, a, pi);
}
int* preorderTraversal(struct TreeNode* root, int* returnSize)
{
    int size = TreeSize(root);
    int* a = malloc(sizeof(int)*size);
    int i = 0;
    _preorderTraversal(root, a, &i);
    *returnSize = size;
    return a;
}

【05_1数据结构】【算法入门_分治】二叉树初阶的基本理解、堆的概念及结构(含二叉树经典笔试题~)_第56张图片

5.3相同的树

【05_1数据结构】【算法入门_分治】二叉树初阶的基本理解、堆的概念及结构(含二叉树经典笔试题~)_第57张图片

思想:

两棵树先比较根是否都为空?是则return true;再判断两棵树是否其中有一个为空?是则return false;既然满足都不为空,那么再去判断是否根的值相同?不是则return false;既然根不为空且根的值也相同,再去判断两棵树左子树与左子树之间是否相同?右子树与右子树之间是否相同?

/**
 * Definition for a binary tree node.
 * struct TreeNode {
 *     int val;
 *     struct TreeNode *left;
 *     struct TreeNode *right;
 * };
 */

bool isSameTree(struct TreeNode* p, struct TreeNode* q)
{
    //根
    //两棵树根都是空,则两棵树相等
    if(p == NULL && q == NULL)
    {
        return true;
    }
    //两棵树其中仅有一棵树的根是空,另一棵树的根不是空,则不相等
    if(p == NULL || q == NULL)
    {
        return false;
    }
    
    //根都不为空,判断根的值是否相同
    if(p->val != q->val)
    {
        return false;
    }
    //根不为空且相同,判断左右子树是否相同
    return isSameTree(p->left,q->left) && isSameTree(p->right, q->right);
}

5.4对称二叉树

【05_1数据结构】【算法入门_分治】二叉树初阶的基本理解、堆的概念及结构(含二叉树经典笔试题~)_第58张图片

【05_1数据结构】【算法入门_分治】二叉树初阶的基本理解、堆的概念及结构(含二叉树经典笔试题~)_第59张图片

思想:

我们将一棵树拆开两部分来看->

首先,对于整棵树的根节点进行判断:根节点是NULL,则return true;

然后再去判断根节点的左右节点是否相同?所以我们运用对称二叉树的思想,建立了一个子函数用来判断是否对称相等(具体思想方法见对称二叉树oj);

然后去将参数左子树的根节点和右子树的根节点传给子函数,子函数中运用对称二叉树思想来判断是否对称。注意最后递归的情况是,将   左子树的根节点的左子树节点  与  右子树的根节点的右子树节点  比较

【05_1数据结构】【算法入门_分治】二叉树初阶的基本理解、堆的概念及结构(含二叉树经典笔试题~)_第60张图片

/**
 * Definition for a binary tree node.
 * struct TreeNode {
 *     int val;
 *     struct TreeNode *left;
 *     struct TreeNode *right;
 * };
 */

bool _isSymmetricTree(struct TreeNode* root1 ,struct TreeNode* root2)
{
    if(root1 == NULL && root2 == NULL)
       return true;
    if(root1 == NULL || root2 == NULL)
       return false;
    if(root1->val != root2->val)
       return false;
    return _isSymmetricTree(root1->left,root2->right) && _isSymmetricTree(root1->right,root2->left);
}

bool isSymmetric(struct TreeNode* root)
{
    if(root == NULL)
        return true;
    
    return _isSymmetricTree(root->left, root->right);
}

5.5另一棵树的子树

【05_1数据结构】【算法入门_分治】二叉树初阶的基本理解、堆的概念及结构(含二叉树经典笔试题~)_第61张图片

思想:用root的每一个子树都和sub比一下

/**
 * Definition for a binary tree node.
 * struct TreeNode {
 *     int val;
 *     struct TreeNode *left;
 *     struct TreeNode *right;
 * };
 */

//判断两棵树相不相同
bool isSameTree(struct TreeNode* p, struct TreeNode* q)
{
    //根
    //两棵树根都是空,则两棵树相等
    if(p == NULL && q == NULL)
    {
        return true;
    }
    //两棵树其中仅有一棵树的根是空,另一棵树的根不是空,则不相等
    if(p == NULL || q == NULL)
    {
        return false;
    }
    
    //根都不为空,判断根的值是否相同
    if(p->val != q->val)
    {
        return false;
    }
    //根不为空且相同,判断左右子树是否相同
    return isSameTree(p->left,q->left) && isSameTree(p->right, q->right);
}


bool isSubtree(struct TreeNode* root, struct TreeNode* subRoot)
{
    if(root == NULL)
        return false;
    if(isSameTree(root,subRoot))
        return true;
    
    return isSubtree(root->left,subRoot) || isSubtree(root->right,subRoot);
}

你可能感兴趣的:(数据结构与算法(C语言版),二叉树,AVL,完全二叉树,平衡搜索树)