【数据结构】二叉树

在这里插入图片描述

欢迎来到Cefler的博客😁
🕌博客主页:那个传说中的man的主页
🏠个人专栏:题目解析
🌎推荐文章:题目大解析2

在这里插入图片描述


👉🏻树概念和结构

1.树的概念

定义:树是一种非线性的数据结构,它是有n(n>=0)个结点组成的一个具有层次的集合。
在这里插入图片描述

树的一些常见概念:

  • 节点的度:一个节点含有的子树的个数称为该节点的度; 如上图: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的子孙
  • 森林:由m(m>0)棵互不相交的树的集合称为森林

2.树的结构

树的结构可以有很多表示法,但是目前有一个极简便的表示法:左孩子右兄弟表示法
即在结构中存下该结点第一个孩子的结点地址和该孩子右边的兄弟地址。
代码表示如下👇🏻

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

👉🏻二叉树概念和结构

二叉树其实就是树的一种特殊情况:
1.二叉树不存在大于2的结点
2.二叉树的子树是分左右的,是有序的
在这里插入图片描述
二叉树一般都是由以下几种情况复合而成的:👇🏻
在这里插入图片描述
⭐️特殊的二叉树

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

🌟二叉树的一些结论(规定根节点的层数为1)
1.满二叉树第n层的结点数为2^(n-1)
2.满二叉树n层结点总数为2^n-1
3.完全二叉树n层结点总数的值域为[2^(n-1),2 ^n-1]
4.对任何一棵二叉树, 如果度为0其叶结点个数为n0 , 度为2的分支结点个数为n2 ,则有 n0=n2 +1
5 对于具有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否则无右孩子

1.二叉树结构体创建和初始化

二叉树本质其实就是个顺序表,而且在结构体创建上其实和栈的创建一样,
结构体元素有:

  • 数组
  • 容量
  • 数组长度
    代码如下👇🏻
typedef int HeapTypeData;
typedef struct Heap
{
	HeapTypeData* a;
	int capacity;
	int sz;
}HP;

2.二叉树插入数据以及向上调整法

代码如下👇🏻

void PushHeap(HP* php, HeapTypeData x)
{
	assert(php);
	if (php->sz == php->capacity)//容量满了要进行扩容
	{
		int newcapacity = php->capacity == 0 ? 4 : php->capacity * 2;
		HeapTypeData* AdAfter = (HeapTypeData*)realloc(php->a, sizeof(HeapTypeData) * newcapacity);
		if (AdAfter == NULL)
		{
			printf("realloc fail");
			return;
		}
		php->a = AdAfter;
		php->capacity = newcapacity;
	}
	php->a[php->sz] = x;
	php->sz++;
	//做完插入后,要进行向上调整
	AdjustUp(php->a, php->sz - 1);
}

声明📢:我们以下建立的二叉树类型为小堆

前面的内容我就不再赘述,其原理和栈的插入一样,而我们的关注点在插入后的动作。
由于我们建立的二叉树是小堆,那么我们插入后就要将我们所插入的这个数据x放在整个序列中合适的位置,我们这里采取的方法是向上调整法
向上调整法的思想
x为孩结点,将x与其父结点对比,若小于父结点,则与父结点进行交换,交换完一次后,此时新的孩子结点为刚刚的父结点,此时在以该结点为基点,向上找父结点,在进行对比,直到孩子结点到堆顶处时停止交换循环,但如果中间若有不满足孩结点小于父结点的情况,则直接退出循环,因为此时x已经在合适的位置了。

这里我们有一个关于孩子结点和父节点关系的重要规律:parent=(child-1)/2
在这里插入图片描述
代码具体实现如下👇🏻

void AdjustUp(HeapTypeData* a, int child)
{
	int parent = (child - 1) / 2;//父亲的结点
	while (child)//若到child==0时停止循环
	{
		if (a[child] <a[parent])//<为小堆,>为大堆
		{
			/*int tmp = php->a[child];
			php->a[child] = php->a[parent];
			php->a[parent] = tmp;*/
			Swap(&a[child], &a[parent]);
			child = parent;
			parent=(child - 1) / 2;
		  }
		else
		{
			break;
		}
	}
}

3.删除堆顶元素以及向下调整法

删除堆顶元素,我想很多人一开始都会想着这么做:
把n-1个元素前移,然后sz–即可,可是假如这样会出现什么弊端呢?我给个图大家就明白了。
在这里插入图片描述
问题显而易见了,出现了乱辈份情况
3 和 2本来是兄弟关系,没有比较关系,现在3变成了2的父亲,这不就离大谱了吗,
我们辛辛苦苦建立的小堆,就因为这样乱辈份,导致堆不再符合小堆的规定,而有人还抱了侥幸心理,说如果4和2位置换一下还是符合小堆的,但是我只能说,侥幸心理不能有,常在河边走哪有不湿鞋。
在这里插入图片描述
那么我们该采取什么思想进行删栈顶呢?
很简单,既然在头部数据难删,我们就直接将堆顶元素和堆尾元素交换,在尾部进行尾删,我们可是毫无负担,sz- -后,此时我们再从堆顶开始从上往下进行向下调整法即可。
口诀可为:首尾交换,向下调整
在这里插入图片描述
代码如下👇🏻

void PopHeap(HP* php)
{
	assert(php);
	assert(!EmptyHeap(php));//为空不能再删了
	//进行堆顶和堆尾的交换
	int tmp = php->a[0];
	php->a[0] = php->a[php->sz - 1];
	php->a[php->sz - 1] = tmp;
	//删尾
	php->sz--;
	//接下来向下进行调整
	AdjustDown(php->a,php->sz,0);
}

向下调整法思路
在左右孩子中,先选中对象为左孩子,假设左孩子为最小,后再判断真假,如果假,child++,此时选中对象变为右孩子。而后如果孩子小于父结点,二者进行交换。
一次交换后,新的parent结点为刚刚选中的孩子结点,而孩子结点为:child=2*parent+1
向下调整法代码如下👇🏻

void AdjustDown(HeapTypeData* a,int n, int parent)
{
	//使用向上或向下调整法前提是下/上的子树均为堆
	// 我们这里按小堆来
	//先假设左孩子比较小
	int child = parent * 2 + 1;
	while (parent<n)
	{
		if (child + 1 <n && a[child + 1] < a[child])//如果右孩子小,则改变(child+1<php->sz考虑只有左孩子情况)
		{
			child++;//此时转为右孩子
		}
		if (child < n &&a[parent] > a[child])
		{
			/*int tmp = a[parent];
			a[parent] = php->a[child];
			php->a[child] = tmp;*/
			Swap(&a[parent], &a[child]);
			parent = child;
			child = parent * 2 + 1;
		}
		else
		{
			break;
		}
	 }
}

小点👾:
1.无论是向上还是向下调整法都,下/上方的子树都必须是堆。
2.向上/下调整法的调整逻辑跟堆本身类型有关,比如是小堆,那么必须是小堆逻辑,这么说可能更清楚,向上/下调整法的作用就是用来保持堆的类型,我是小堆,你在尾部插入一个比堆顶都小的数,我肯定要用小堆逻辑的调整法把你送到堆顶位置

4.堆排序

法一:

思路:
接收堆顶数据,而后将其插入数组当中,此时堆进行删顶,此时的堆顶的数值大小为次小(第二小),如此反复循环进行

void HeapSort1(HP* php,int* a,int sz)
{
	assert(php);
	int i = 0;
	while (!EmptyHeap(php))
	{
		int top = TopHeap(php);
		a[i++] = top;
		PopHeap(php);
	}
}

弊端:

  • 前提需要php为堆,我们不能寄希望于php,存在不确定性
  • 其时间复杂度太大为:N*logN

法二:

思路:
既然前提必须为堆,我们就得先建堆。
这里我们需要记住:
1.降序->建小堆
2.升序->建大堆

比如我们要降序,我们建立小堆
此时我们将首尾交换,sz- -,进行向下调整法,这样我们最后就会得到降序序列了。
这个思想也很简单,因为堆顶元素肯定是序列最小,每次将堆顶的元素和堆尾交换,最小元素到了尾部,如此重复进行,所有小的元素都能有序的来到尾部位置,到后面堆顶的元素就是序列最大了。
但该方法仍然存在弊端
比如我给你一个数组。让你在这基础上去建立小堆,我们若从第二层开始向上调整,但是向上调整的前提是:上方子树为堆,这样又存在不确定性了,而我们并不能保证该数组为堆
代码如下👇🏻

void HeapSort2(int* a, int sz)
{
	//降/升序,先建小/大堆
	//我们要降序,故建小堆
	int i = 0;
	for (i = 1; i < sz; i++)//建立小堆
	{
		AdjustUp(a, i);
	}
	int end = sz - 1;
	while (end)
	{
		Swap(&a[0], &a[end]);
		end--;
		AdjustDown(a, end, 0);
	}
}

这里一定有人有个问题了,为啥不能降序建大堆呢?
在这里插入图片描述
我们顺着这个思想进行思考
1.我们要降序建大堆,好,我们建立了一个大堆
2.堆顶一定为序列最大,所以它保持不动
3.我要从剩下元素中找出次大,就要将剩下的子树当作堆进行向下调整法,此时的堆顶就是次大,如此反复进行,我就能排序得到降序

思路看似很顺,嗯,如果你也这么认为并觉得泰裤辣,那么,少年,你已经中道了
在这里插入图片描述
你怎么敢把剩下的子树当作堆来处理呢,如果我们这么做,我们就犯和上面删顶操作一样的问题,就是乱辈份情况
在这里插入图片描述
所以可千万不能这么做。

法三:

与法二对比做出改变的就是我们在进行交换前,在建堆环节中我们将向上调整法改为了向下调整法,从倒数第二层向上出发,向下调整子树。
代码如下👇🏻

void OptimizeHeapSort2(int* a, int sz)
{
	int i = 0;
	//从尾父结点开始向上调整树
	for (i = (sz-1-1)/2; i >=0; i--)
	{
		AdjustDown(a, sz, i);

	}
	int end = sz - 1;
	while (end)
	{
		Swap(&a[0], &a[end]);
		end--;
		AdjustDown(a, end, 0);
	}
}

🥢向上调整法和向下调整法时间复杂度对比

1.向上调整法建堆(从第二层开始向下出发)
在这里插入图片描述
2.向下调整法建堆(从尾父结点开始向上出发)
在这里插入图片描述
3.如下代码的时间复杂度计算(每次从堆顶开始向下调整)

	while (end)
	{
		Swap(&a[0], &a[end]);
		end--;
		AdjustDown(a, end, 0);
	}

在这里插入图片描述

5.给定N个数,如何找到前K个最大/小的数?

我们这里先以找出前K个最大的数为目标。
实现思路
1.我们要找前K个最大的数,所以我们将前K个数建成小堆
2.而后将后N-K个数依次与堆顶对比,若比堆顶大,则替换入堆,并进行向下调整法
3.全部遍历对比后,此时K个数就是最大的那一批数了。

为何找前K个最大的数一定要建小堆,如果为大堆,我们替换后,进行向下调整,此时该数据仍然处于堆顶位置,根本不会发生变化。那么有人就会说,那我与堆尾数据进行对比行不行?

解释之前,我们了解一下为啥在上方堆排序中我们要专门让堆顶数据和堆尾数据交换,这个好处是什么?
好处就在于:堆顶数据一定是最大/小,所以在堆排序中,我与堆尾数据交换后,我可以很确认,最大/小,次大/小,次次大/小……的数据一定统统都到了后面,因此我们能进行逆向排序。

回到与堆尾数据进行对比行不行问题上,同样如此,我如果跟堆尾数据比,你能确定这个堆尾数据就是最大/小?因为你不能确定它和它兄弟的关系,兄弟间没有对比关系。
所以这个时候你大概能明白为什么一定要与堆顶数据进行对比
而如果我们要与堆顶数据对比,并且要找前K个最大的数,我们就不得不建立小堆,这个数据才能在堆中流动起来

代码实现如下👇🏻

void CreatFILE()
{
	FILE* pf = fopen("data.txt", "w");
	int i = 0;
	//写入数据
	for (i = 0; i < 10; i++)
	{
		int a = rand() % 24;
		fprintf(pf, "%d\n", a);
	}

	fclose(pf);
	pf = NULL;

}
void BinaryTreeTest2()
{
	CreatFILE();
	FILE* pf = fopen("data.txt", "r");

	//读k个数据到数组中,然后建立堆
	int k = 5, i;
	int arr[5] = { 0 };
	for (i = 0; i < k; i++)
	{
		fscanf(pf, "%d", &arr[i]);
	}
	for (i = (5 - 1 - 1) / 2; i >= 0; i--)
	{
		AdjustDown(arr, 5, i);
	}
	//然后将剩下的N-K=5个数据于堆顶对比
	int val = 0;
	while (!feof(pf))
	{
		fscanf(pf, "%d", &val);
		if (val > arr[0])
		{
			arr[0] = val;
			AdjustDown(arr, 5, 0);
		}
	}
	
	//打印一下
	for (i = 0; i < 5; i++)
	{
		printf("%d\n", arr[i]);
	}
	fclose(pf);
	pf = NULL;
	
}

在这里插入图片描述
这个时间复杂度为(N-K)*logk=N.

🍒 BinaryTree.h

#pragma once
#include <stdio.h>
#include <stdlib.h>
#include <assert.h>
#include <stdbool.h>
typedef int HeapTypeData;
typedef struct Heap
{
	HeapTypeData* a;
	int capacity;
	int sz;
}HP;
void InitHeap(HP* php);//初始化

void DestroyHeap(HP* php);//销毁

void PushHeap(HP* php,HeapTypeData x);//插入数据

void AdjustUp(HeapTypeData* a, int child);//向上调整

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

void Swap(int* p1, int* p2);


void PopHeap(HP* php);//删掉堆顶

void HeapSort1(HP* php, int* a, int sz);//堆排序1:弊端前提需要php为堆,其次其时间复杂度太大为:N*logN

void HeapSort2(int* a, int sz);//堆排序2,其时间复杂度为:N*logN

void OptimizeHeapSort2(int* a, int sz);

HeapTypeData TopHeap(HP* php);//返回堆顶元素

int SzHeap(HP* php);//返回堆的长度

bool EmptyHeap(HP* php);//判断堆是否为空

🍒BinaryTree.c

#define _CRT_SECURE_NO_WARNINGS 1
#include "BinaryTree.h"
void InitHeap(HP* php)
{
	assert(php);
	php->a = NULL;
	php->sz =  php->capacity = 0;
}
bool EmptyHeap(HP* php)
{
	assert(php);
	return php->sz == 0;
}
int SzHeap(HP* php)
{
	assert(php);
	return php->sz;
}
void DestroyHeap(HP* php)
{
	assert(php);
	free(php->a);
	php->a = NULL;
	php->sz = php->capacity = 0;
}
void Swap(int* p1, int* p2)
{
	int tmp = *p1 ;
	*p1  = *p2;
	*p2 = tmp;
}
void AdjustUp(HeapTypeData* a, int child)
{
	int parent = (child - 1) / 2;//父亲的结点
	while (child)//若到child==0时停止循环
	{
		if (a[child] <a[parent])//<为小堆,>为大堆
		{
			/*int tmp = php->a[child];
			php->a[child] = php->a[parent];
			php->a[parent] = tmp;*/
			Swap(&a[child], &a[parent]);
			child = parent;
			parent=(child - 1) / 2;
		  }
		else
		{
			break;
		}
	}
}

void AdjustDown(HeapTypeData* a,int n, int parent)
{
	//使用向上或向下调整法前提是下/上的子树均为堆
	// 我们这里按小堆来
	//先假设左孩子比较小
	int child = parent * 2 + 1;
	while (parent<n)
	{
		if (child + 1 <n && a[child + 1] < a[child])//如果右孩子小,则改变(child+1<php->sz考虑只有左孩子情况)
		{
			child++;//此时转为右孩子
		}
		if (child < n &&a[parent] > a[child])
		{
			/*int tmp = a[parent];
			a[parent] = php->a[child];
			php->a[child] = tmp;*/
			Swap(&a[parent], &a[child]);
			parent = child;
			child = parent * 2 + 1;
		}
		else
		{
			break;
		}
	 }
}
void PushHeap(HP* php, HeapTypeData x)
{
	assert(php);
	if (php->sz == php->capacity)//容量满了要进行扩容
	{
		int newcapacity = php->capacity == 0 ? 4 : php->capacity * 2;
		HeapTypeData* AdAfter = (HeapTypeData*)realloc(php->a, sizeof(HeapTypeData) * newcapacity);
		if (AdAfter == NULL)
		{
			printf("realloc fail");
			return;
		}
		php->a = AdAfter;
		php->capacity = newcapacity;
	}
	php->a[php->sz] = x;
	php->sz++;
	//做完插入后,要进行向上调整
	AdjustUp(php->a, php->sz - 1);
}
HeapTypeData TopHeap(HP* php)
{
	assert(php);
	return php->a[0];
}

void PopHeap(HP* php)
{
	assert(php);
	assert(!EmptyHeap(php));//为空不能再删了
	//进行堆顶和堆尾的交换
	int tmp = php->a[0];
	php->a[0] = php->a[php->sz - 1];
	php->a[php->sz - 1] = tmp;
	//删尾
	php->sz--;
	//接下来向下进行调整
	AdjustDown(php->a,php->sz,0);
}

void HeapSort1(HP* php,int* a,int sz)
{
	assert(php);
	int i = 0;
	while (!EmptyHeap(php))
	{
		int top = TopHeap(php);
		a[i++] = top;
		PopHeap(php);
	}
}

void HeapSort2(int* a, int sz)
{
	//降/升序,先建小/大堆
	//我们要降序,故建小堆
	int i = 0;
	for (i = 1; i < sz; i++)
	{
		AdjustUp(a, i);
	}
	int end = sz - 1;
	while (end)
	{
		Swap(&a[0], &a[end]);
		end--;
		AdjustDown(a, end, 0);
	}
}

void OptimizeHeapSort2(int* a, int sz)
{
	int i = 0;
	//从尾父结点开始向上调整树
	for (i = (sz-1-1)/2; i >=0; i--)
	{
		AdjustDown(a, sz, i);

	}
	int end = sz - 1;
	while (end)
	{
		Swap(&a[0], &a[end]);
		end--;
		AdjustDown(a, end, 0);
	}
}

👉🏻二叉树链式结构的实现

⭐️前序、中序、后序概念

前序、中序、后序访问原则
1.前序:根->左根数->右根数
2.中序:左根树->根->右根数
3.后序:左根数->右根数->根
在这里插入图片描述
上图中前序、中序、后序的访问顺序如下:

  • 前序:1->2->3->N->N->N->4->5->N->N->6->N->N
  • 中序:N->3->N->2->N->1->N->5->N->4->N->6->N
  • 后序:N->N->3->N->2->N->N->5->N->N->6->4->1

如何巧妙理解原则去访问呢?🤗

牢记一句话:每个节点的访问顺序必须和原则中规定的一样,每尝试要不要访问一个新的节点时,口中默念原则顺序。

中序为例子来实操一遍:
1.1,默念:左根树->根->右根数,1为根,其不满足第一个为左子树条件,不能访问,接下来访问1的左子树再看看
2. 2,默念:左根树->根->右根数,2也为根,其不满足第一个为左子树条件,不能访问,接下来访问2的左子树再看看
3. 3,默念:左根树->根->右根数,2也为根,其不满足第一个为左子树条件,不能访问,接下来访问3的左子树再看看
4. 3的左子树为NULL,到此为止,3的左子树已经访问完了,终于可以访问3的根了,及其本身,而后是其右子树,目前已访问有(N->3->N)
5. 再回到2,2的左子树也访问完了,访问其本身,再访问其的右子树,NULL,到此为止(N->3->N->2->N).
.
.
.
按照这样的思路,我们就能正确访问,最终为:N->3->N->2->N->1->N->5->N->4->N->6->N

前序访问二叉树

typedef int BTDataType;
typedef struct BinaryTreeNode
{
	BTDataType _data;
	struct BinaryTreeNode* _left;
	struct BinaryTreeNode* _right;
}BTNode;

BTNode* BuyNode(BTDataType x)
{
	BTNode* newnode = (BTNode*)malloc(sizeof(BTNode));
	if (newnode == NULL)
	{
		perror("malloc fail");
		return NULL;
	}
	newnode->_data = x;
	newnode->_left = NULL;
	newnode->_right = NULL;
	return newnode;
}
BTNode* CreatBinaryTree()
{
	BTNode* node1 = BuyNode(1);
	BTNode* node2 = BuyNode(2);
	BTNode* node3 = BuyNode(3);
	BTNode* node4 = BuyNode(4);
	BTNode* node5 = BuyNode(5);
	BTNode* node6 = BuyNode(6);

	node1->_left = node2;
	node1->_right = node4;
	node2->_left = node3;
	node4->_left = node5;
	node4->_right = node6;
	return node1;
}
void PrevOrder(BTNode* root)
{
	//前序为例
	if (root == NULL)
	{
		printf("N ");
		return;
	}
	printf("%d ", root->_data);
	PrevOrder(root->_left);

	PrevOrder(root->_right);
}
int main()
{
	BTNode* root = CreatBinaryTree();
	PrevOrder(root);
	return 0;
}

实现效果👇🏻
在这里插入图片描述

中序访问二叉树

void BinaryTreeInOrder(BTNode* root)
{
	if (root == NULL)
	{
		printf("N ");
		return;
	}
	
	BinaryTreeInOrder(root->_left);
	printf("%d ", root->_data);
	BinaryTreeInOrder(root->_right);
}

实现效果👇🏻
在这里插入图片描述

后序访问二叉树

void BinaryTreePostOrder(BTNode* root)
{
	if (root == NULL)
	{
		printf("N ");
		return;
	}
	
	BinaryTreePostOrder(root->_left);
	BinaryTreePostOrder(root->_right);
	printf("%d ", root->_data);
}

✏️前中序推断计算

已知前、中序复原二叉树

已知前、中序复原二叉树的思路是:以前序为基,先得出根节点(这一步很重要),而后就可以在中序中判断出根节点的位置而后分成左右子树,此时在从前序中找左右子树的根节点,依次进行下去,就可以复原整个二叉树。
这里有个例题配合我们进行实例讲解:

二叉树的先序遍历和中序遍历如下:先序遍历:EFHIGJK;中序遍历:HFIEJKG.则二叉树根结点为()
A E
B F
C G
D H

在这里插入图片描述

已知中、后序复原二叉树

已知中、后序复原二叉树思想:通过上面的例子,我们知道,先得出根节点是最重要的,所以,我们得从已知的两个序列中找到一个可以马上确定根节点的序列,而中序列显然不行,但后序我们可以立马确定根节点在序列的最后一个,由此在中序中我们又可以进行左右子树的划分。
这里有个例题配合我们进行实例讲解:

设一课二叉树的中序遍历序列:badce,后序遍历序列:bdeca,则二叉树前序遍历序列为____。
A adbce
B decab
C debac
D abcde

在这里插入图片描述

实现效果👇🏻
在这里插入图片描述
前、中、后序访问小结

本质上就是一个递归过程,而我们要访问的真正对象是根本身,但是我们要按照序的顺序去依次访问,如中序:左根树->根->右根数。你得先把左根数访问完之后,才能再访问根。

求二叉树节点数

我们的递归思想是什么?我们将求二叉树节点数功能专门设置一个函数BinaryTreeSize()
把众多个节点看作单个节点来分析,从堆顶节点开始,把堆顶节点BinaryTreeSize(),让它帮忙分析下能不能求出当前节点数,若此时若堆顶节点不为空,是不是就满足1个节点数。
这个时候我们再将左子树和右子树的节点传给BinaryTreeSize(),同理再让该函数算一下当前节点数,而当我们一直递归下去后
.
.
.直到遇到节点为空时,说明当前节点数肯定为0了,我们直接返回0即可。
代码如下👇🏻

int BinaryTreeSize(BTNode* root)
{
	if (root == NULL)
		return 0;
	return BinaryTreeSize(root->_left) + 1 + BinaryTreeSize(root->_right);
}

求二叉树叶子节点数(即最后一层的节点数)

最后一层节点和其它节点不同的地方是什么?这是我们的突破口
没错:最后一层节点的左子树和右子树均为空。
也就是说我们可以用函数递归到节点的左子树和右子树均为空时,当满足该条件时,我们返回1.
如果不满足,则继续函数递归下去(左、右子树均得一起递归)
代码如下👇🏻

int BinaryTreeLeafSize(BTNode* root)
{
	if (root == NULL)//如果开局为空,肯定就是为0了
		return 0;
	if (root->_left == NULL && root->_right == NULL)
		return 1;
	else
	return  BinaryTreeLeafSize(root->_left) + BinaryTreeLeafSize(root->_right);
}

求二叉树第K层节点数

写了以上几个函数功能,其实如何递归很容易,但是终止条件需要我们去思考。
这里我们的终止条件就是当我们递归到第K层时停下来。
那问题又来了,什么条件能证明我们递归到第K层呢?
其实也很容易理解,比如我要递归到第2层,一开始我们传参进来的K= =2,
我们向下递归1次,k- -一次,然后你看这边如果我们向下递归一次后,此时k - -完后k= =1
由此我们就可以以小见大,当k==1时,递归条件停止,此时我们判断一下该层节点,如果不为空的返回1,若为空则返回0.
代码如下👇🏻

int BinaryTreeLevelKSize(BTNode * root,int k)
	{
		if (root == NULL)
			return 0;
		if (k == 1)
			return 1;
		return  BinaryTreeLevelKSize(root->_left, k - 1) + BinaryTreeLevelKSize(root->_right, k - 1);
	}

求二叉树高度

求二叉树的高度,其实就是看看子树里面哪个延伸的最长。
同理我创建一个函数BinaryTreeHigh()去求二叉树的高度
在这里插入图片描述
上图中我只画了左子树求高度的图,右子树也同理,最后我们会得出来左子树求出来的高度肯定要比右子树高,而我们只采纳最高那个子树的数据,所以我们这里需要对比左右子树的高度,再返回较大值。
代码如下👇🏻
版本 1

int BinaryTreeHigh1(BTNode* root)
{
	if (root == NULL)
		return 0;
	if (BinaryTreeHigh1(root->_left) > BinaryTreeHigh1(root->_right))
		return BinaryTreeHigh1(root->_left)+1;
	else
		return BinaryTreeHigh1(root->_right)+1;
}

此版本弊端在于哪里?
因为如果树的高度越高,此递归函数的递归次数就会越恐怖
你看看,我每次要做这两个步骤
1.比较左子树和右子树的高度
2.返回较大值的高度
因为我每次递归,当前高度我并没有记载下来。
所以,我好不容易递归比较完左子树和右子树的高度时,你比较完了,现在我需要将最大值返回回来,你跟我说没有记载数据?
在这里插入图片描述

我这个时候又要再递归回去找数据,而更可怕的是你每次递归回去要数据的时候,每次数据都没有在递归过程中保存下来,所以你要不停的递归递归再递归。
所以有了版本2
版本2

int BinaryTreeHigh2(BTNode* root)
{
	if (root == NULL)
		return 0;
	int leftheight = BinaryTreeHigh2(root->_left);
	int rightheight = BinaryTreeHigh2(root->_right);
	return leftheight > rightheight ? leftheight + 1 :rightheight + 1;
}

效果如下👇🏻
![在这里插入图片描述](https://img-blog.csdnimg.cn/d19ecce2b8904a938f19ccddfa63f348.png

判断两颗树是否相同

我们这里运用前序访问思想去做,先判断根,再判断左子树,最后再判断右子树,若全部相等,返回真。
而这里,如果先判断根为真才能再去判断左子树,若根已经为假,就不用再往后判断了,即全真则真,一假则假
代码如下👇🏻
LeetCode链接相同的树

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

判断是否为单值树

LeetCode链接单值二叉树

bool isUnivalTree(struct TreeNode* root){
     if(root==NULL)
     return 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);
}

判断对称二叉树

LeetCode链接对称二叉树
MyCode:

bool  _isSymmetric(struct TreeNode* leftleaf, struct TreeNode* rightleaf)
 {
       if(leftleaf==NULL&&rightleaf== NULL)
       return true;
       if(leftleaf==NULL|| rightleaf == NULL)
       return false;
       if(leftleaf->val!=rightleaf->val)
       return false;
       return _isSymmetric(leftleaf->left,rightleaf ->right)&&_isSymmetric(leftleaf->right,rightleaf->left);
 }
bool isSymmetric(struct TreeNode* root){
    if(root==NULL)
    return true;
     return _isSymmetric(root->left,root->right);
}

二叉树的前序遍历

LeetCode链接二叉树的前序遍历
MyCode:

struct Arr
{
      int* a;
      int sz;
      int capacity;
};
// arr  array;
// array->a = NULL;
// array->sz=array->capacity = 0;
void Init(struct Arr* p)
{
    p->a=NULL;
    p->sz=p->capacity=0;
}
void PushArr(struct Arr* p,int x)
{
    // assert(p);
    if(p->sz==p->capacity)
    {
        int newcapacity = p->capacity == 0?4:p->capacity*2;
      int* new = (int*)realloc(p->a,sizeof(int)*newcapacity);
        p->a=new;
        p->capacity=newcapacity;
    }
    p->a[p->sz] = x;
    p->sz++;
}
void PushRootVal(struct TreeNode* root,struct Arr* array)
{
    if(root == NULL)
     {
         return ;
     }
     PushArr(array,root->val);
     PushRootVal(root->left,array);
     PushRootVal(root->right,array);
}
int* preorderTraversal(struct TreeNode* root, int* returnSize){
    struct Arr array;
    Init(&array);
     PushRootVal(root,&array);
     *returnSize = array.sz;
     return array.a;
}

示例代码

int TreeSize(struct TreeNode* root)
 {
     return root == NULL ? 0 : TreeSize(root->left) + TreeSize(root->right) + 1;
 }

 void _preOrder( struct TreeNode* root, int* a, int* pi)//这里的i你必须使用指针,因为他后面会多线程全部都加i,你每一次都直接掉i过来就会在后面家的不是同一个i,后面会乱套了,所以我们要用i的地址
 {
    if(root == NULL)
        return;
    a[*pi] = root->val;
    ++*pi;

    _preOrder(root->left, a, pi);
    _preOrder(root->right, a, pi);
 }
int* preorderTraversal(struct TreeNode* root, int* returnSize){
    int size = TreeSize(root);
    int* a = (int*)malloc(size*sizeof(int));
    int i = 0;
    _preOrder(root, a, &i);
    
    *returnSize = size;

    return a;
}

另一棵树的子树

LeetCode链接另一棵树的子树
MyCode:

 int BinaryTreeSize(struct TreeNode* root)
{
	if (root == NULL)
		return 0;
	return BinaryTreeSize(root->left) + 1 + BinaryTreeSize(root->right);
}
bool isSameTree(struct TreeNode* p, struct TreeNode* q){
   
     if(p==NULL||q==NULL)
      {
          if(p==q)
          return true;
          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,int* sz)
{
    if(root==NULL)
    return false;
    if(isSameTree(root,subRoot))
    return true;
      if(sz == 0)
    return false;
    *sz--;
    return _isSubtree(root->left,subRoot,sz)||_isSubtree(root->right,subRoot,sz);
}
bool isSubtree(struct TreeNode* root, struct TreeNode* subRoot){
    int sz=BinaryTreeSize(root);
    return _isSubtree(root,subRoot,&sz);
}

示例代码

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

二叉树的构建以及遍历

原题链接二叉树的构建以及遍历

#include <stdio.h>

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

BTNode* BuyNode(BTDataType x)
{
	BTNode* newnode = (BTNode*)malloc(sizeof(BTNode));
	if (newnode == NULL)
	{
		perror("malloc fail");
		return NULL;
	}
	newnode->data = x;
	newnode->left = NULL;
	newnode->right = NULL;
	return newnode;
}
BTNode* CreatBTNode(char* a,int* pi)
{
    if(a[*pi] == '#')
    {
        (*pi)++;
        return NULL;
    }
    BTNode* root = BuyNode(a[*pi]);
    (*pi)++;
    root->left = CreatBTNode(a,pi);
    root->right = CreatBTNode(a,pi);
    return root;

}

void BinaryTreeInOrder(BTNode* root)
{
	if (root == NULL)
	{
		// printf("N ");
		return;
	}

	BinaryTreeInOrder(root->left);
	printf("%c ", root->data);
	BinaryTreeInOrder(root->right);
}
int main() {
   char arr[100];
   int i=0;
   scanf("%s",arr);
    BTNode* root = CreatBTNode(arr,&i);
   BinaryTreeInOrder(root);
   
    return 0;
}

翻转二叉树

原题链接翻转二叉树

struct TreeNode* invertTree(struct TreeNode* root){
       if(root == NULL){
        return NULL;
    }
    struct TreeNode *temp;
    temp = invertTree(root->left);
    root->left = invertTree(root->right);
    root->right = temp;
    return root;
}

平衡二叉树

原题链接:平衡二叉树

 int BinaryTreeHigh(struct TreeNode* root)
{
	if (root == NULL)
		return 0;
	int leftheight = BinaryTreeHigh(root->left);
	int rightheight = BinaryTreeHigh(root->right);
	return leftheight > rightheight ? leftheight + 1 :rightheight + 1;
}

bool isBalanced(struct TreeNode* root){
      if(root ==NULL)
      return true;
      if(root->left==NULL&&root->right==NULL)
      return true;
      int lefthigh = BinaryTreeHigh(root->left);
      int righthigh = BinaryTreeHigh(root->right);
      if(fabs(lefthigh-righthigh)>1)
      return false;
      return isBalanced(root->left)&&isBalanced(root->right);

}

⭐️层序遍历

遵循原则
队列:先进先出,上一层出时,带下一层入列
在这里插入图片描述
简单代码实现

#define _CRT_SECURE_NO_WARNINGS 1
#include "Queue.h"
typedef int BTDataType;
typedef struct BinaryTreeNode
{
	BTDataType data;
	struct BinaryTreeNode* left;
	struct BinaryTreeNode* right;
}BTNode;

BTNode* BuyNode(BTDataType x)
{
	BTNode* newnode = (BTNode*)malloc(sizeof(BTNode));
	if (newnode == NULL)
	{
		perror("malloc fail");
		return NULL;
	}
	newnode->data = x;
	newnode->left = NULL;
	newnode->right = NULL;
	return newnode;
}

//层序遍历
void LevelOrder(BTNode* root)
{
	Queue q;
	QueueInit(&q);
	//先将堆顶插入队列
	if (root)
		QueuePush(&q, root);
	while (!QueueEmpty(&q))
	{
		BTNode* front = QueueFront(&q);
		QueuePop(&q);
		printf("%d->", front->data);
		if(front->left)
			QueuePush(&q, front->left);
		if (front->right)
			QueuePush(&q, front->right);
	}

	QueueDestroy(&q);
}
BTNode* CreatBinaryTree()
{
	BTNode* node1 = BuyNode(1);
	BTNode* node2 = BuyNode(2);
	BTNode* node3 = BuyNode(3);
	BTNode* node4 = BuyNode(4);
	BTNode* node5 = BuyNode(5);
	BTNode* node6 = BuyNode(6);

	node1->left = node2;
	node1->right = node4;
	node2->left = node3;
	node4->left = node5;
	node4->right = node6;
	return node1;
}
int main()
{
	BTNode* root = CreatBinaryTree();
	LevelOrder(root);
	return 0;
}

二叉树的销毁(后序思想)

void  BinaryTreeDestroy(BTNode* root)
{
	if (root == NULL)
	{
		return;
	}
	BinaryTreeDestroy(root->_left);
	BinaryTreeDestroy(root->_right);
	free(root);

}

为什么不前序或中序呢?如果是前序,我们把根节点先free掉后,左右子树节点的地址我们就没有了,如果是中序,左子树和根free完后,右子树的地址也就没有了,所以只能后序。

判断二叉树是否为完全二叉树(层序思想)

完全二叉树就是满二叉树的一种特殊情况,通过层序遍历,我们可以发现,层序遍历的顺序和完全二叉树的节点顺序相吻合
在这里插入图片描述
所以我们的思路就是用层序思想遍历一下二叉树,而一旦遇到NULL,此时停止遍历(此时已遍历的节点都以及出队列了),我们去判断剩下还未出列的节点,如果有节点为非空,则说明并非所有节点都在树的最左边,证明不是完全二叉树,反之,则是完全二叉树。

bool IsBinaryTreeCompletey(BTNode* root)
{
	Queue q;
	QueueInit(&q);
	//先将堆顶插入队列
	if (root)
		QueuePush(&q, root);
	while (!QueueEmpty(&q))
	{
		BTNode* front = QueueFront(&q);
		QueuePop(&q);
		if (front == NULL)//遇到NULL停止出列
			break;
		
			QueuePush(&q, front->left);
	
			QueuePush(&q, front->right);
	}
	//接下来检查剩下还在队列的成员
	while (!QueueEmpty(&q))
	{
		BTNode* front = QueueFront(&q);
		QueuePop(&q);
		if (front)
		{
			QueueDestroy(&q);
			return false;
		}
	}

	QueueDestroy(&q);
	return true;
}

如上便是本期的所有内容了,如果喜欢并觉得有帮助的话,希望可以博个点赞+收藏+关注🌹🌹🌹❤️ 🧡 💛,学海无涯苦作舟,愿与君一起共勉成长
在这里插入图片描述
在这里插入图片描述


评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值