二叉树的实现(使用C语言)

完整代码链接:DataStructure: 基本数据结构的实现。 (gitee.com)

目录

一、树的概念及结构:

1.树的定义:

2.树的基本术语:

3.树的表示:

二、二叉树的概念及结构:

1.概念:

2.二叉树的性质:

3.特殊的二叉树:

4.二叉树的存储结构:

三、二叉树的顺序结构及实现:

1.二叉树的顺序结构:

2.堆的概念及结构: 

3.堆的实现:

3.1 结构体设计:

3.2 堆向下调整算法:

3.3 堆的创建:

3.4 建堆时间复杂度: 

3.5 堆的初始化:

3.6 堆的销毁:

3.7 堆的插入:

 3.8 堆的删除:

 3.9 取堆顶的数据:

4.堆的应用:

4.1 堆排序:

4.2 TOP-K问题:

4.2.1 TOP-K问题OJ题: 

4.2.1.1  原题链接:

4.2.1.2 代码实现:

4.2.1.3 提交结果:

4.2.1.4 更多题目:

四、二叉树的链式结构及实现:

1.结构体设计:

2.遍历:

2.1 前序遍历:

2.2 中序遍历:

2.3 后序遍历:

2.4 层序遍历:

3.统计个数:

3.1 总的结点个数:

3.2 叶子结点的个数: 

3.3 第K层结点个数 :

4.创建:

5.销毁:

6.查找值为x的结点:

7.判断一棵二叉树是否为完全二叉树 :

五、二叉树基础入门题目:

144. 二叉树的前序遍历

94. 二叉树的中序遍历

145. 二叉树的后序遍历

965. 单值二叉树

104. 二叉树的最大深度

226. 翻转二叉树

100. 相同的树

101. 对称二叉树

572. 另一棵树的子树

110. 平衡二叉树


一、树的概念及结构:

1.树的定义:

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

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

2.树的基本术语:

结点:树中的一个独立单元。包含一个数据元素及若干指向其子树的分支;如上图:A、B、C等。

结点的度:一个结点含有的子树的个数称为该结点的度; 如上图:A的度为3,B的度为2,C的度为1。 

叶结点(终端结点):度为0的结点称为叶结点; 如上图:F,G,K等结点为叶结点。

非终端结点或分支结点:度不为0的结点; 如上图:D、E、H等结点为分支结点。

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

孩子结点(子结点):一个结点含有的子树的根结点称为该结点的子结点; 如上图:B是A的孩子结点。

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

树的度:一棵树中,最大的结点的度称为树的度; 如上图:树的度为3。

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

树的高度(深度):树中结点的最大层次; 如上图:树的高度为4。

堂兄弟结点:双亲在同一层的结点互为堂兄弟;如上图:G与E、F、H、I、J互为堂兄弟结点。

结点的祖先:从根到该结点所经分支上的所有结点;如上图:A是所有结点的祖先。

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

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

3.树的表示:

树结构相对线性表就比较复杂了,要存储表示起来就比较麻烦了,既要保存值域,又要保存结点和结点之间的关系,实际中树有很多种表示方式如:双亲表示法,孩子表示法、孩子双亲表示法以及孩子兄弟表示法等。我们这里就简单的了解其中最常用的孩子兄弟表示法

孩子兄弟表示法:其又称二叉树表示法,或二叉链表表示法,即以二叉链表作为数的存储结构。链表中的结点的两个链域分别指向该结点的第一个孩子结点和下一个兄弟结点,分别命名为_firstChild域和_pNextBrother域,其结点形式如下图所示:

typedef int DataType;
typedef struct Node
{
	struct Node* _firstChild1;//指向该结点的第一个孩子
	struct Node* _pNextBrother;//指向该结点的下一个兄弟结点
	DataType _data;//该结点的数据域
}Node;

二、二叉树的概念及结构:

1.概念:

二叉树是n(n>=0)个结点所构成的集合,它或为空树(n=0),或为非空树。对于非空树T:
①.有且仅有一个称之为根的结点
②.除根结点以外的其余结点分为两个互不相交的子集T₁和T₂,分别称为T的左子树右子树,且T₁和T₂本身又都是二叉树。
二叉树与树一样具有递归性质,二叉树与树的区别主要有以下两点:
①.二又树每个结点至多只有两棵子树(二叉树中不存在度大于2的结点);

②.二又树的子树有左右之分,其次序不能任意颠倒

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

2.二叉树的性质:

①. 若规定根结点的层数为1,则一棵非空二叉树的第i层上最多有 2^{i-1}个结点。

②. 若规定根结点的层数为1,则深度为h的二叉树的最大结点数是2^{h}-1

③. 对任何一棵二叉树, 如果度为0其叶结点个数为n0, 度为2的分支结点个数为n2,则有n0=n2+1

④. 若规定根结点的层数为1,具有n个结点的满二叉树的深度h=\displaystyle \log_{2}n+1

⑤. 对于具有n个结点的完全二叉树,如果按照从上至下从左至右的数组顺序对所有结点从0开始编号,则对于序号为i的结点有:

①. 若i>0,i位置结点的双亲序号:(i-1)/2;i=0,i为根结点编号,无双亲结点

②. 若2i+1=n,左孩子序号:2i+1,2i+1>=n否则无左孩子

③. 若2i+2=n,右孩子序号:2i+2,2i+2>=n否则无右孩子

3.特殊的二叉树:

①. 满二叉树:一个二叉树,如果每一个层的结点数都达到最大值,则这个二叉树就是满二叉树。也就是说,如果一个二叉树的层数为k,且结点总数是(2^k)-1,则它就是满二叉树。

②. 完全二叉树:完全二叉树是效率很高的数据结构,完全二叉树是由满二叉树而引出来的。对于深度为k的,有n个结点的二叉树,当且仅当其每一个结点都与深度为k的满二叉树中编号从1至n的结点一一对应时称之为完全二叉树。 要注意的是满二叉树是一种特殊的完全二叉树 。

4.二叉树的存储结构:

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

①. 顺序存储

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

②. 链式存储

二叉树的链式存储结构是指用链表来表示一棵二叉树,即用链来指示元素的逻辑关系。 通常的方法是链表中每个结点由三个域组成,数据域和左右指针域,左右指针分别用来给出该结点左孩子和右孩子所在的链结点的存储地址 。链式结构又分为二叉链和三叉链,这篇文章主要讲解二叉链,三叉链后面再讲解。

//二叉链
typedef int BinaryTreeDataType;
typedef struct BinaryTreeNode
{
	BinaryTreeDataType _data;//当前结点值域
	struct BinaryTreeNode* _pLeft;//当前结点的左孩子
	struct BinaryTreeNode* _pRight;//当前结点的右孩子
}BinaryTreeNode;
//三叉链
typedef int TernaryTreeDataType;
typedef struct TernaryTreeNode
{
	TernaryTreeDataType _data;//当前结点值域
	struct TernaryTreeNode* _pLeft;//当前结点的左孩子
	struct TernaryTreeNode* _pRight;//当前结点的右孩子
	struct TernaryTreeNode* _parent;//当前结点的双亲
}TernaryTreeNode;

三、二叉树的顺序结构及实现:

1.二叉树的顺序结构:

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

2.堆的概念及结构: 

如果有一个关键码的集合K = { _{}k_{0},k_{1},k_{2},...,k_{n-1} },把它的所有元素按完全二叉树的顺序存储方式存储在一个一维数组中,并满足:K_{i}<=K_{2*i+1} 且 K_{i}<=K_{2*i+2}(或K_{i}>=K_{2*i+1}K_{i}>=K_{2*i+2}),i=0,1,2,…,(也就是父结点大于(或小于)子结点),则称为小堆(或大堆)。将根结点最大的堆叫做最大堆或大根堆根结点最小的堆叫做最小堆或小根堆

堆的性质:

  • 堆中某个结点的值总是不大于或不小于其父结点的值;
  • 堆总是一棵完全二叉树。

 

3.堆的实现:

我们这里以小堆为例。

3.1 结构体设计:

typedef int HeapDataType;
typedef struct Heap
{
	HeapDataType* _a;
	int _size;
	int _capacity;
}Heap;

3.2 堆向下调整算法:

我们这里给出一个数组,逻辑上将其看做一颗完全二叉树,我们通过从根结点开始的向下调整算法可以把它调整成一个小堆。但是,向下调整算法有一个前提:左右子树必须是一个堆,才能调整

void Swap(HeapDataType* x, HeapDataType* y)
{
	HeapDataType tmp;
	tmp = *x;
	*x = *y;
	*y = tmp;
}
//前提:左右子树都是小堆
void AdjustDown(HeapDataType* a, int n, int root)
{
	int parent = root;
	int child = parent * 2 + 1;
	
	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;
		}
	}
}

3.3 堆的创建:

当然,我们不可能保证每次构建小堆(大堆)的时候,其左右子树都是小堆(大堆)。下面我们给出一个数组,这个数组逻辑上可以看做一颗完全二叉树,但是还不是一个堆,现在我们通过算 法,把它构建成一个堆。根结点左右子树不是堆,我们怎么调整呢?这里我们可以从倒数的第一个非叶子结点的子树开始调整(从叶子结点开始调整也可以,但没必要),一直调整到根结点的树,就可以调整成堆。

int a[] = { 7,5,4,2,3,1 };

3.4 建堆时间复杂度: 

 则需要移动结点总的移动步数为:

T(n)=2^{0}*(h-1)+2^{1}*(h-2)+2^{2}*(h-3)+...+2^{h-3}*2+2^{h-2}*1       

                  2*T(n)=2^{1}*(h-1)+2^{2}*(h-2)+2^{3}*(h-3)...+2^{h-2}*2+2^{h-1}*1  ②  

②-①,得:(错位相减)

T(n)=1-h+2^{1}+2^{2}+2^{3}+...2^{h-2}+2^{h-1}

T(n)=2^{0}+2^{1}+2^{2}+2^{3}+...2^{h-2}+2^{h-1}-h

T(n)=2^{h}-1-h

n=2^{h}-1,得:

h=\log_{2}(n+1)

所以,T(n)=n-\log_{2}(n+1)\approx n

因此,建堆的时间复杂度为O(N)

3.5 堆的初始化:

堆的初始化其实就是将一个数组构造成一个堆。

void HeapInit(Heap* php, HeapDataType* a, int n)
{
	php->_a = (HeapDataType*)malloc(sizeof(HeapDataType) * n);//创建与a数组同等大小的空间
	if (php->_a==NULL)
	{
		exit(-1);
	}
	memcpy(php->_a, a, sizeof(HeapDataType) * n);//将外面传进来的数组内的数据拷贝到我们自己的数组内
	php->_size = n;
	php->_capacity = n;

	//构建堆
	for (int i = (n - 1 - 1) / 2;i >= 0;i--)//n-1是最后一个结点所在的下标,所以(n-1-1)/ 2是找到最后一个父结点
	{
		AdjustDown(php->_a, php->_size, i);
	}
}

3.6 堆的销毁:

void HeapDestory(Heap* php)
{
	assert(php);
	free(php->_a);
	php->_a = NULL;
	php->_size = 0;
	php->_capacity = 0;
}

3.7 堆的插入:

我们这里的插入是从后面插入的(前面插入会挪动数据),但是插入之后不能保证当前的堆依然为小堆(大堆),由于新插入的结点在逻辑上是二叉树的最后一个叶子结点,所以我们可以执行向上调整,将插入的结点放在正确的位置。

向上调整算法: 时间复杂度:O(logN)

void AdjustUp(HeapDataType* a, int n, int child)
{
	int parent = (child - 1) / 2;
	while (child > 0)
	{
		if (a[child] < a[parent])
		{
			Swap(&a[child], &a[parent]);
			child = parent;
			parent = (child - 1) / 2;
		}
		else
		{
			break;
		}
	}
}
void HeapPush(Heap* php, HeapDataType x)
{
	assert(php);
	if (php->_size == php->_capacity)
	{
		php->_capacity *= 2;
		HeapDataType* tmp = (HeapDataType*)realloc(php->_a, sizeof(HeapDataType) * php->_capacity);
		if (tmp == NULL)
		{
			exit(-1);//开辟空间失败
		}
		php->_a = tmp;
	}
	php->_a[php->_size++] = x;
	AdjustUp(php->_a, php->_size, php->_size - 1);//向上调整算法,传参为了和“向下调整算法”一致。从最后一个位置开始调整
}

 3.8 堆的删除:

堆的删除是删除堆顶的数据,将堆顶的数据跟最后一个数据交换,然后删除数组最后一个数据,再进行向下调整算法。

void HeapPop(Heap* php)//头删可以删除最小的,选出次小的
{
	assert(php);
	assert(php->_size > 0);
	Swap(php->_a[0], php->_a[php->_size - 1]);
	php->_size--;
	AdjustDown(php->_a, php->_size, 0);
}

 3.9 取堆顶的数据:

HeapDataType HeapTop(Heap* php)
{
	assert(php);
	assert(php->_size > 0);
	return php->_a[0];
}

4.堆的应用:

4.1 堆排序:

堆排序即利用堆的思想来进行排序,总共分为两个步骤:

1.建堆

  • 升序:建小堆;
  • 降序:建大堆。

2.利用堆删除思想排序。

为什么排降序要建小堆呢?

我们可以从上面事例看出,每次和尾结点交换时,就可以选出一个小的值 ,这样排出来就是一个降序。 这是的时间复杂度:建堆O(n),有n个结点需要调整(其实也可以是n-1个结点,向下调整到只剩一个结点时,这个结点可以不调),每次调整的时间复杂度为logn(高度次),所以,总的时间复杂度为O(n)+O(nlogn)≈O(nlogn)

其实,排降序也可以建大堆,但是,当建好堆后,只能找到最大的,然后又需要对后面的结点进行建堆,找出次小的,这样,此时的时间复杂度为O(n^{2})。都这复杂度了,我为什么不直接遍历数组,每次找到值更大的呢?

void HeapSort(int* a, int n)
{
	//建堆
	for (int i = (n - 1 - 1) / 2;i >= 0;i--)
	{
		AdjustDown(a, n, i);
	}
	//排降序,建小堆
	int end = n - 1;
	while (end > 0)
	{
		Swap(&a[0], &a[end]);
		AdjustDown(a, end, 0);
		end--;
	}
}

4.2 TOP-K问题:

概念:TOP-K问题,即求数据集合中前K个最大的元素或者最小的元素一般情况下数据量都比较大。

比如:国产游戏中的最好玩的10款,崩铁中持有率最高的3位五星角色,我国最美丽的5个景区等等。

对于Top-K问题,假设有N个数据,我们能想到的最简单直接的方式就是排序(堆排序的时间复杂度为O(NlogN) )

但是,其实当我们要找最大(小)的K个数时,后面的N-K个数都不需要排序,所以,当我们要找到最大的K个数时,可以建一个有N个数据的大堆,通过HeapPop()函数,找到最大的K个数,由于HeapPop()函数的时间复杂度为logN,所以,此种方法的时间复杂度为:建堆:O(N) + K-1次HeapPop():O( (K-1)logN ) \Rightarrow O(N+(K-1)*logN)

但是,如果数据量非常大,排序或使用堆删除函数就不太可取了(可能数据都不能一下子全部加载到内存中)。最佳的方式就是用一个包含K个元素的堆来解决,基本思路如下:

1. 用数据集合中前K个元素来建堆

  • 前k个最大的元素,则建小堆
  • 前k个最小的元素,则建大堆

2. 用剩余的N-K个元素依次与堆顶元素来比较,不满足则替换堆顶元素,将剩余N-K个元素依次与堆顶元素比完之后,堆中剩余的K个元素就是所求的前K个最小或者最大的元素(根结点的值是第K大(小))。

4.2.1 TOP-K问题OJ题: 
4.2.1.1  原题链接:

面试题 17.14. 最小K个数

4.2.1.2 代码实现:
void Swap(int* p1,int* p2)
{
    int tmp=*p1;
    *p1=*p2;
    *p2=tmp;
}
void AjustDown(int* retArr,int n,int root)
{
    int parent=root;
    int child=parent*2+1;
    while(child<n)
    {
        if(child+1<n && retArr[child+1]>retArr[child])
        {
            child++;
        }
        if(retArr[child]>retArr[parent])
        {
            Swap(&retArr[child],&retArr[parent]);
            parent=child;
            child=parent*2+1;
        }
        else
        {
            break;
        }
    }
}
int* smallestK(int* arr, int arrSize, int k, int* returnSize){
    *returnSize=k;//别忘了*
    if(k==0)
        return NULL;
    //开辟k个空间,用于存储最小的k的数
    int* retArr=(int*)malloc(sizeof(int)*k);
    for(int i=0;i<k;i++)//把前k个数先放入结果数组中
    {
        retArr[i]=arr[i];
    }
    for(int i=(k-1-1)/2;i>=0;i--)//将前k个数 建成大堆 ,i==0时,从根结点调
    {
        AjustDown(retArr,k,i);
    }
    for(int i=k;i<arrSize;i++)//后面的数据依次入堆
    {
        if(arr[i]<retArr[0])//与新入堆的数据与根结点的值比较,比根结点的值小就入堆
        {
            retArr[0]=arr[i];//更新根结点的数据
            AjustDown(retArr,k,0);
        }
    }
    return retArr;
}
4.2.1.3 提交结果:

4.2.1.4 更多题目:

BM47 寻找第K大

四、二叉树的链式结构及实现:

我们先回顾一下:对于任何一个二叉树,都要看做三个部分:根结点左子树右子树,其子树又可以分成:根结点、左子树、右子树,这样,我们才能更清楚的知道二叉树是递归定义的

注意:为了学习起来更简单,我们先讲二叉树的遍历,这里就先手动创建一个二叉树,后面再讲二叉树的创建。

1.结构体设计:

typedef char BinaryTreeDataType;
typedef struct BinaryTreeNode
{
	BinaryTreeDataType _data;//当前结点值域
	struct BinaryTreeNode* _pLeft;//当前结点的左孩子
	struct BinaryTreeNode* _pRight;//当前结点的右孩子
}BinaryTreeNode;

2.遍历:

2.1 前序遍历:

void PreOrder(BinaryTreeNode* root)
{
	if (root == NULL)
	{
		printf("NULL ");
		return;
	}
	printf("%c ", root->_data);
	PreOrder(root->_pLeft);
	PreOrder(root->_pRight);
}

2.2 中序遍历:

void InOrder(BinaryTreeNode* root)
{
	if (root == NULL)
	{
		printf("NULL ");
		return;
	}
	InOrder(root->_pLeft);
	printf("%c ", root->_data);
	InOrder(root->_pRight);
}

2.3 后序遍历:

void PostOrder(BinaryTreeNode* root)
{
	if (root == NULL)
	{
		printf("NULL ");
		return;
	}
	PostOrder(root->_pLeft);
	PostOrder(root->_pRight);
	printf("%c ", root->_data);
}

2.4 层序遍历:

对于层序遍历的实现,我们需要一个队列来实现,队列里面存储每个结点的指针(存指针比存结点占用的内存小)。

具体步骤:父结点先入队列,父结点出队列时,将其左右结点放入队列

void LevelOrder(BinaryTreeNode* root)
{
	Queue q;
	QueueInit(&q);
	if (root == NULL)
		return;
	QueuePush(&q, root);//根结点入队列
	while (!QueueEmpty(&q))//当队列为空,二叉树所有结点遍历完
	{
		BinaryTreeNode* front = QueueFront(&q);
		printf("%c ", front->_data);
		QueuePop(&q);
        //左右结点的 指针 入栈
		if (front->_pLeft)
			QueuePush(&q, front->_pLeft);
		if (front->_pRight)
			QueuePush(&q, front->_pRight);
	}
	QueueDestory(&q);
}

3.统计个数:

3.1 总的结点个数:

分治大问题分解成小的子问题,小的子问题再进一步分解为更小的子问题,直到子问题不能再分解,依据最小子问题求解题

这里可以利用分治的思想,要求总的结点个数,即求左子树的结点个数+右子树的结点个数+根结点。

int TreeSize(BinaryTreeNode* root)
{
	if (root == NULL)
		return 0;
	else
		return 1 + TreeSize(root->_pLeft) + TreeSize(root->_pRight);
}

3.2 叶子结点的个数: 

同样,利用分治思想,总的叶子结点个数=左子树的叶子结点个数+右子树的叶子结点个数

int TreeLeafSize(BinaryTreeNode* root)
{
	if (root == NULL)
		return 0;
	if (root->_pLeft == NULL && root->_pRight == NULL)
		return 1;
	return TreeLeafSize(root->_pLeft) + TreeLeafSize(root->_pRight);
}

3.3 第K层结点个数 :

要求二叉树的第K层结点个数,即求其左右子树的第K-1层结点的个数,要求其左右子树的第K-1层结点的个数,即求其左右子树的左右子树的K-2层结点的个数,依此类推,直到K=1,即到达了要求的那层。

int BinaryTreeLevelKSize(BinaryTreeNode* root, int k)
{
	//以根结点作为第一层
   //二叉树的第K层等于其左子树与右子树的第K-1层
	if (root == NULL)
		return 0;
	if (k == 1)
		return 1;
	return BinaryTreeLevelKSize(root->_pLeft, k - 1) + BinaryTreeLevelKSize(root->_pRight, k - 1);
}

4.创建:

我们这里用一个题来实现二叉树的创建。

二叉树的创建及遍历

#include<stdio.h>
#include<stdlib.h>
typedef struct TreeNode {
    char val;
    struct TreeNode *left;
    struct TreeNode *right;
}TreeNode;
//依据前序序列构建二叉树
TreeNode* CreateTree(char* str,int* pi)
{
    if(str[*pi]=='#')
    {
        (*pi)++;
        return NULL;
    }
    else 
    {
        TreeNode* root=(TreeNode*)malloc(sizeof(TreeNode));
        root->val=str[*pi];
        (*pi)++;
        root->left=CreateTree(str,pi);
        root->right=CreateTree(str,pi);
        return root;
    }
}
//中序遍历
void Inorder(TreeNode* root)
{
    if(root==NULL)
        return;
    Inorder(root->left);
    printf("%c ",root->val);
    Inorder(root->right);
}
int main() 
{
    char str[100];
    scanf("%s",str);
    int i=0;
    TreeNode* root=CreateTree(str,&i);
    Inorder(root);
    return 0;
}

5.销毁:

void BinaryTreeDestory(BinaryTreeNode** root)
{
	if (*root == NULL)
		return;
	BinaryTreeDestory(&(*root)->_pLeft);//(*root)->_pLeft为一级指针
	BinaryTreeDestory(&(*root)->_pRight);
	free(*root);
	*root = NULL;
}

6.查找值为x的结点:

BinaryTreeNode* BinaryTreeFind(BinaryTreeNode* root, BinaryTreeDataType x)
{
	if (root == NULL)
		return NULL;
	if (root->_data == x)
		return root;
	BinaryTreeNode* node = BinaryTreeFind(root->_pLeft, x);
	if (node)
		return node;
	node = BinaryTreeFind(root->_pRight, x);
	if (node)
		return node;
}

7.判断一棵二叉树是否为完全二叉树 :

我们这里可以利用层序遍历的特性,完全二叉树的有值结点与空结点是分开的(有值结点在前,空结点在后),而非完全二叉树中间会存在空结点。

bool BinaryTreeComplete(BinaryTreeNode* root)
{
	if (root == NULL)
		return true;
	Queue q;
	QueueInit(&q);
	QueuePush(&q, root);
	while (!QueueEmpty(&q))
	{
		BinaryTreeNode* front = QueueFront(&q);
		QueuePop(&q);
		if (front == NULL)//队列的头为空结点时,结束出入队列
			break;
		QueuePush(&q, front->_pLeft);
		QueuePush(&q, front->_pRight);
	}
	while (!QueueEmpty(&q))
	{
		BinaryTreeNode* front = QueueFront(&q);
		QueuePop(&q);
		if (front)//当队列里面存在有值结点,此二叉树不是完全二叉树
		{
			QueueDestory(&q);
			return false;
		}
	}
	QueueDestory(&q);
	return true;
}

五、二叉树基础入门题目:

对于刚入门学习二叉树的朋友,可能会对一些题解不是很理解,毕竟很多二叉树的题目都是用递归的方式实现的,递归最开始又不是很好理解,前期我们可以把递归展开图画出来理解,每次做二叉树的题时,都先用分治的思想去想怎么做,把一个问题分解成一个相同的子问题,找到最小子问题,从最小子问题着眼,通过解决最小子问题,完成整个问题的解决。实在不知道怎么做时,可以去看看题解,如果题解还是不理解,就将递归展开图画出来,慢慢理解(仅限于刚开始学习递归,对递归不熟悉),毕竟,后期做题的时候,我们不可能每次都把展开图画出来噻!加油吧,毕竟我也是小白,愿我们共同进步,不要怂,干就对了

144. 二叉树的前序遍历

typedef struct TreeNode TreeNode;
int treeSize(TreeNode* root)
{
    if(root==NULL)
        return 0;
    else
        return 1+treeSize(root->left)+treeSize(root->right);
}
void _preorderTraversal(TreeNode* root,int* array,int* pi)
{
    if(root==NULL)
        return;
    array[(*pi)++]=root->val;//将遍历到的结点的值放入数组
    _preorderTraversal(root->left,array,pi);//递归遍历左子树
    _preorderTraversal(root->right,array,pi);//递归遍历右子树
}
int* preorderTraversal(struct TreeNode* root, int* returnSize) {
    int size=treeSize(root);//计算结点个数,为了计算要动态开辟的数组的大小
    *returnSize=size;
    int* array=(int*)malloc(sizeof(int)*size);//创建返回数组
    int i=0;//返回数组的下标
    _preorderTraversal(root,array,&i);//i要传地址,不然进入函数出来后不会发生变化(不是在同一i上加的)
    return array;
}

中序与后序就不写了哦,就换换位置就行。

94. 二叉树的中序遍历

145. 二叉树的后序遍历

965. 单值二叉树

//将二叉树分为当前树与左右子树,看是否相等,再递归到左右子树,看递归后的当前树与左右子树是否相等
bool isUnivalTree(struct TreeNode* root) {
    if(root==NULL)
        return true;
     //判断当前树与左右子树是否相等
    //不要用满足的情况作为返回值,
   //即:不能是
  //if(root->left&&root->val==root->left->val);
 //    return true;
//因为当左子树满足单值二叉树的情况时,此时如果返回true,右子树不一定是单值二叉树
    if(root->left&&root->val!=root->left->val)
        return false;
    if(root->right&&root->val!=root->right->val)
        return false;
    //递归到左右子树,判断是否相等
    return isUnivalTree(root->left)&&isUnivalTree(root->right);
}

104. 二叉树的最大深度

 //最大深度=max(左子树深度,右子树深度)+1
 //即:要求二叉树的最大深度,就是求其左子树与右子树中的最大深度+1
int maxDepth(struct TreeNode* root) {
    if(root==NULL)
        return 0;
    int leftDepth=maxDepth(root->left);
    int rightDepth=maxDepth(root->right);
    return leftDepth>rightDepth?leftDepth+1:rightDepth+1;
}

226. 翻转二叉树

//要翻转整个二叉树,即翻转其左右子树
typedef struct TreeNode TreeNode;
struct TreeNode* invertTree(struct TreeNode* root) {
    if(root==NULL)
        return NULL;
    else
    {
        //简明版
        /*
        //翻转当前子树
        TreeNode* tmp=root->left;
        root->left=root->right;
        root->right=tmp;
        //递归翻转左子树
        invertTree(root->left);
        //递归翻转右子树
        invertTree(root->right);
        */
        //精简版
        TreeNode* right=root->right;
        root->right=invertTree(root->left);
        root->left=invertTree(right);
        return root;
    }
}

100. 相同的树

//要看两棵树是否相同,即看当前树与左右子树是否相同
bool isSameTree(struct TreeNode* p, struct TreeNode* q) {
    if(p==NULL&&q==NULL)
        return true;
    //结构不相同
    if(p==NULL&&q!=NULL)
        return false;
    //结构不相同
    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);
}

101. 对称二叉树

这个题可以利用上一个题来求解。

bool isSameTree(struct TreeNode* Left, struct TreeNode* Right) {
    if(Left==NULL&&Right==NULL)
        return true;
    //结构不相同
    if(Left==NULL&&Right!=NULL)
        return false;
    //结构不相同
    if(Left!=NULL&&Right==NULL)
        return false;
    if(Left->val!=Right->val)
        return false;
    return isSameTree(Left->left,Right->right)&&isSameTree(Left->right,Right->left);
}
//要求整个树是否对称,即求其左右子树是否对称
bool isSymmetric(struct TreeNode* root) {
    //利用相同的树,递归看左右子树是否对称
    return isSameTree(root->left,root->right);
}

572. 另一棵树的子树

bool isSameTree(struct TreeNode* p, struct TreeNode* q) {
    if(p==NULL&&q==NULL)
        return true;
    //结构不相同
    if(p==NULL&&q!=NULL)
        return false;
    //结构不相同
    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);
}
//先看另一棵树(subRoot)是否是当前树的子树,再看另一棵树(subRoot)是否是当前树的左右子树的子树
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);
}

110. 平衡二叉树

这棵树不是平衡二叉树,高度差大于1了。

 int treeDepth(struct TreeNode* root)
 {
    if(root==NULL)
        return 0;
    int leftDepth=treeDepth(root->left);
    int rightDepth=treeDepth(root->right);
    return leftDepth>rightDepth?leftDepth+1:rightDepth+1;
 }//时间复杂度O(N),遍历完了整颗树

 //要求整个树是否是平衡二叉树,看其左右子树是否为平衡二叉树   
bool isBalanced(struct TreeNode* root) {
    if(root==NULL)
        return true;
    int gap=treeDepth(root->left) - treeDepth(root->right);//当前树的高度差
    if(abs(gap)>1)
        return false;
    return isBalanced(root->left)&&isBalanced(root->right);//左右子树是否平衡
}
//由于每个结点都要判断是否是平衡二叉树,
//所以最好情况是O(N):一进来算出这棵树的左子树的高度与右子树高度差就不满足平衡二叉树
//最坏情况 O(N^2) :到了最后几个结点才发现不是平衡二叉树

优化:时间复杂度O(N)

由于要求一颗二叉树是否为平衡二叉树,就要求每个结点的左子树的高度与右子树数的高度之差,所以会存在大量的重复计算高度(前序判断,导致大量高度计算)。

我们如果使用后序判断,每次返回到根结点时,都把左右子树的高度带回来,就可以解决这个问题。

bool _isBalanced(struct TreeNode* root,int* pDepth)
{
    if(root==NULL)
    {
        *pDepth=0;
        return true;
    }
    else
    {
        //左子树不满足
        int leftDepth = 0;
        if(_isBalanced(root->left,&leftDepth)==false)
            return false;
        //右子树不满足
        int rightDepth = 0;
        if(_isBalanced(root->right,&rightDepth)==false)
            return false;
        //当前树不满足
        if(abs(leftDepth-rightDepth)>1)
            return false;
        //满足    
        *pDepth=leftDepth>rightDepth?leftDepth+1:rightDepth+1;//将当前树的高度往上带
        return true;
    }
}
bool isBalanced(struct TreeNode* root) {
    int depth=0;
    return _isBalanced(root,&depth);
}

  • 30
    点赞
  • 29
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值