【手撕代码·二叉树】堆 和 二叉链的实现 + 算法题

文章目录


前言

        本文内容:树的介绍、二叉树、顺序二叉树---堆的实现、堆排序、Top-K算法、链式二叉树的实现、二叉树算法题、二叉树常见选择题。内容较多,希望对大家有所帮助。


❤️感谢支持,点赞关注不迷路❤️


一、树

1.树的概念与结构

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

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

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

以下为非树形结构示例:

  • 子树是不相交的(如果存在相交就是图了)
  • 除了根结点外,每个结点有且仅有⼀个父结点
  • 一棵N个结点的树有N-1条边(边为两个节点之间的连线)


2.树相关术语

  • 父结点/双亲结点:若⼀个结点含有子结点,则这个结点称为其子结点的父结点;如上图:A是B的父结点
  • 子结点/孩子结点:⼀个结点含有的子树的根结点称为该结点的子结点;如上图:B是A的孩子结点
  • 结点的度:⼀个结点有几个孩子,他的度就是多少;比如A的度为6,F的度为2,K的度为0
  • 树的度:⼀棵树中,最大的结点的度称为树的度;如上图:树的度为 6
  • 叶子结点/终端结点:度为 0 的结点称为叶子结点;如上图: B 、 C 、 H 、 I... 等结点为叶子结点
  • 分支结点/非终端结点:度不为 0 的结点;如上图: D 、 E 、 F 、 G... 等结点为分支结点
  • 兄弟结点:具有相同父结点的结点互称为兄弟结点(亲兄弟);如上图: B 、 C 是兄弟结点
  • 结点的层次:从根开始定义起,根节点为第 1 层,根的子结点为第 2 层,以此类推;
  • 树的高度或深度:树中结点的最大层次;如上图:树的高度为 4
  • 结点的祖先:从根到该结点所经分支上的所有结点;如上图: A 是所有结点的祖先
  • 路径:一条从树中任意节点出发,沿父节点 - 子节点连接,达到任意节点的序列;比如A到Q的路径为: A-E-J-Q;H到Q的路径H-D-A-E-J-Q
  • 子孙:以某结点为根的子树中任⼀结点都称为该结点的子孙。如上图:所有结点都是A的子孙
  • 森林:由 m ( m>0 ) 棵互不相交的树的集合称为森林;


3.树的表示

孩子兄弟表示法:

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

struct TreeNode 
{ 
    struct Node* child;   // 左边开始的第⼀个孩⼦结点
    struct Node* brother; // 指向其右边的下⼀个兄弟结点
    int data;             // 结点中的数据域            
}; 
  • child:指向孩子节点中的左边第一个节点
  • brother:指向兄弟节点,没有则为空

例如以下树:

孩子兄弟表示法:


4.树形结构实际运用场景

文件系统是计算机存储和管理文件的⼀种方式,它利用树形结构来组织和管理文件和文件夹。在文件系统中,树结构被广泛应用,它通过父结点和子结点之间的关系来表示不同层级的文件和文件夹之间的关联。


二、二叉树

1.概念与结构

在树形结构中,我们最常用的就是二叉树,⼀棵二叉树是结点的⼀个有限集合,该集合由⼀个根结点加上两棵别称为左子树和右子树的二叉树组成或者为空。

二叉树的特点:

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

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


2.特殊的二叉树

1.满二叉树

一个二叉树,如果每一个层的结点数都达到最大值,则这个二叉树就是满二叉树。也就是说,如果一个二叉树的层数为 K ,且结点总数是,则它就是满二叉树。


2.完全二叉树

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


3.二叉树性质*

根据满二叉树的特点可知:

  1. 若规定根结点的层数为 1 ,则一棵非空二叉树的第 i 层上最多有  个结点
  2. 若规定根结点的层数为 1 ,则深度为 h 的二叉树的最大结点数是 
  3. 若规定根结点的层数为 1 ,具有 n 个结点的满二叉树的深度

第三条由第二条转化过来


三、二叉树存储结构

二叉树一般可以使用两种结构存储:

  • ⼀种顺序结构
  • ⼀种链式结构

1.顺序结构:

顺序结构存储就是使用数组来存储,一般使用数组只适合表示完全⼆叉树,因为不是完全二叉树会有空间的浪费,完全二叉树更适合使用顺序结构存储。


2.链式结构:

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

注:在本文目前只实现了二叉链表


四、实现顺序结构二叉树 ---- 堆

一般堆使用顺序结构的数组来存储数据,堆是⼀种特殊的二叉树,具有二叉树的特性的同时,还具备其他的特性。

需要注意的是这里的堆和操作系统虚拟进程地址空间中的堆是两回事,⼀个是数据结构,⼀个是操作系统中管理内存的一块区域分段。

1.堆的概念与结构

如果有一个关键码的集合 K = {k1 ,k2 ,k3 ,... , k(n-1) } ,把它的所有元素按完全二叉树的顺序存储方式存储,在⼀个⼀维数组中,并满足 小堆或大堆 的特性

小堆:每一个父节点或分支节点都小于其孩子节点的数值,堆顶(根节点)为最小数据

大堆:每一个父节点或分支节点都大于其孩子节点的数值,堆顶(根节点)为最大数据


2.堆的性质

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

堆会使用到的二叉树性质:

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

  1. 若 i > 0,i 位置节点的父节点下标为:( i -1 ) / 2;如果 i=0,i为根节点下标,无双亲节点,所以 i 要大于 0
  2. 若 2*i+1 < n,左孩子下标:2*i+1;如果2*i+1 >= n,则无左孩子
  3. 若 2*i+2 < n,右孩子下标:2*i+2;如果2*i+2 >= n,则无右孩子

以上公式可以让我们找到 i 下标处节点的父节点或者左右孩子节点,需要记住


五.堆的实现

1.声明

堆的结构声明:

typedef int HpDataType;

//定义堆结构
typedef struct Heap
{
	HpDataType* arr;
	int size;
	int capacity;
}HP;

堆为顺序结构的二叉树,底层采用数组结构存储数据,因此结构声明与顺序表相似,但是使用方法与顺序表不相同

函数声明:

堆命名Heap,声明文件Heap.h

#pragma once
#include <stdio.h>
#include <stdlib.h>
#include <assert.h>
#include <stdbool.h>

typedef int HpDataType;

//定义堆结构
typedef struct Heap
{
	HpDataType* arr;
	int size;
	int capacity;
}HP;

//初始化
void HPInit(HP* php);

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

//入堆,直接往尾部插入
void HPPush(HP* php, HpDataType x);

//出堆,出的是堆顶元素
void HPPop(HP* php);

//取堆顶元素
HpDataType HPTop(HP* php);

//判空
bool HPEmpty(HP* php);

//向上调整算法
void AdjustUp(HpDataType* arr, int child);

//向下调整算法
void AdjustDown(HpDataType* arr, int parent, int n);

//交换
void Swap(HpDataType* x, HpDataType* y);

堆的操作方法有:

  1. 堆的初始化方法
  2. 堆的销毁方法
  3. 入堆,往堆中插入方法,即尾插
  4. 出堆,删除堆中元素方法,堆只能在堆顶出元素
  5. 取堆顶元素
  6. 判空
  7. 堆的向上调整算法与向下调整算法属于入堆和出堆时的调整方法,放在头文件中是为后续堆排序使用,包括交换方法。


2.函数实现:

Heap.c文件

#include "Heap.h"

//初始化
void HPInit(HP* php)
{
	assert(php);

	php->arr = NULL;
	php->capacity = php->size = 0;
}

//销毁
void HPDesTroy(HP* php)
{
	assert(php);

	if (php->arr)
	{
		free(php->arr);
	}

	php->arr = NULL;
	php->capacity = php->size = 0;
}

//交换
void Swap(HpDataType* x, HpDataType* y)
{
	HpDataType tmp = *x;
	*x = *y;
	*y = tmp;
}

//向上调整算法
void AdjustUp(HpDataType* arr, int child)
{
	int parent = (child - 1) / 2;

	while (child > 0)
	{
		//小堆:孩子节点 < 父节点,交换
		//大堆:孩子节点 > 父节点,交换
		if (arr[child] < arr[parent])
		{
			Swap(&arr[child], &arr[parent]);
			child = parent;
			parent = (child - 1) / 2;
		}
		else
		{
			break;
		}
	}
}

//入堆,直接往尾部插入
void HPPush(HP* php, HpDataType x)
{
	assert(php);

	//空间是否足够
	if (php->size == php->capacity)
	{
		//扩容
		int newCapacity = php->capacity == 0 ? 4 : 2 * php->capacity;
		HpDataType* tmp = (HpDataType*)realloc(php->arr, sizeof(HpDataType) * newCapacity);
		if (tmp == NULL)
		{
			perror("realloc fail!");
			exit(1);
		}

		php->arr = tmp;
		php->capacity = newCapacity;
	}

	php->arr[php->size++] = x;

	//小堆,向上调整算法
	AdjustUp(php->arr, php->size - 1);
}

//向下调整算法
void AdjustDown(HpDataType* arr, int parent, int n)
{
	//左孩子
	int child = 2 * parent + 1;

	while (child < n)
	{
		//小堆:找左右孩子中小的值
		//大堆:找左右孩子中大的值
		if (child + 1 < n && arr[child] > arr[child + 1])
		{
			child++;
		}

		//小堆:孩子节点 < 父节点, 交换
		//大堆:孩子节点 > 父节点,交换
		if (arr[child] < arr[parent])
		{
			Swap(&arr[child], &arr[parent]);
			parent = child;
			child = 2 * parent + 1;
		}
		else
		{
			break;
		}
	}
}

//出堆,出的是堆顶元素
void HPPop(HP* php)
{
	assert(php && php->size);

	//交换堆顶与堆底数据
	Swap(&php->arr[0], &php->arr[php->size - 1]);
	--php->size;

	//新堆顶需向下调整,保证小堆属性
	AdjustDown(php->arr, 0, php->size);
}

//取堆顶元素
HpDataType HPTop(HP* php)
{
	assert(php && php->size);

	return php->arr[0];
}

//判空
bool HPEmpty(HP* php)
{
	assert(php);

	return php->size == 0;
}

各函数讲解:

  1. HPInit函数:初始化堆,将堆结构中的成员赋值为NULL和0
  2. HPDesTroy函数:销毁堆,判断堆中数组结构是否有动态空间开辟,有就释放掉,并将堆的结构恢复初始化状态。初始化和销毁这两个堆的方法都是使用堆时必备的
  3. Swap函数:耳熟能详的交换两变量值的函数,这里是方便其他方法函数调用
  4. AdjustUp函数:向上调整算法,这个算法是干什么的呢,当然是在我们往堆中插入数据时,为了保证堆是小堆或者大堆的特性,因此我们应该在每次插入数据后,使用该算法对堆进行调整,注意堆底层是数组,插入数据是尾插。算法的实现:需要两个参数,堆的底层数组和新插入的结点下标。通过新结点的下标我们就能找到其父结点下标,我们以小堆为例,小堆是孩子结点大于父结点,因此将孩子结点与其父结点比较,孩子结点小于父结点,则需要交换两值,交换完之后,我们继续往上判断这一路径上的所有父结点,因此我们需要更新孩子结点为父结点,然后重新计算父结点,直到孩子结点走到了根结点,循环停止,所以循环条件就是这个,如果判断孩子结点是大于父结点的,那就直接结束循环,因为此时堆结构就是小堆,无需调整。这就是向上调整算法
  5. HPPush函数:堆的插入函数,我们完成了堆的向上调整算法,那么往堆中插入数据时,我们首先就是判断堆中数组容量够不够,不够就需要扩容,扩容想必大家很熟悉了,这里不赘述,顺序表中详细说明了,扩容后往堆中尾部插入数据。然后调用向上调整算法,注意传入下标时size要 -1,因为前面插入数据时size +1了。
  6. AdjustDown函数:堆的向下调整算法,我们已经知道向上调整算法是插入数据时使用,因此向下调整算法就是删除数据时使用了。注意,堆删数据是头删,而堆又是数组结构,头删后移动数据效率太低了,因此头删采用的是将头部数据与尾部数据交换,size减1就删除数据了,但是此时堆的结构特性就可能不是小堆或者大堆了,因此为了保证堆的特性,我们就需要调整结构,这就是向下调整算法的来历。实现方法:我们还是以小堆为例,传入的参数有3个,堆的底层数组,根节点下标,结点总数。向下调整我们就需要找根结点的孩子结点,使用二叉树性质公式即可求得,我们只需要与其中,接下来一样通过循环调整,我们需要比较根结点与其孩子结点数据大小,但是我们不需要和两个孩子结点都比较,小堆根结点是整个堆结构中最小的值,小堆根结点的左右孩子结点的值又是它们所在子树的最小值,因此我们只需要将根结点与其左右孩子结点中较小着进行比较交换即可,因此我们先进行一个判断,判断左右孩子结点中哪个最小,这里需要注意的是,在判断左右孩子之前,我们需要确保左右孩子存在,因此循环条件我们需要保证左孩子存在,因此左孩子下标因该小于数据总数,这就是循环条件 child<n 的来历,在比较左右孩子中较小者时,先要确保右孩子存在,因此加条件 child+1<n。如果右孩子存在并且比左孩子大,直接让 child++ 即可。剩下的就和向上调整算法类似,判断父结点与孩子结点大小,父节点比孩子结点大,则交换两者,并且更新父结点到孩子结点,并更新孩子结点。如果父结点比孩子结点小,直接结束循环。这就是堆的向下调整算法
  7. HPPop函数:删除堆顶元素,在向下调整算法中已经说明了过程,先交换堆顶与堆底的数据,然后进行向下调整算法即可
  8. HPTop函数:取堆顶元素,返回下标为0处的结点数据即可
  9. HPEmpty函数:判空,判断堆中数据是否为空,判断堆中size成员是否为0即可。


3.使用演示

#include "Heap.h"

int main()
{
	//创建一个堆
	HP hp;
	//初始化
	HPInit(&hp);
	//插入数据建堆,为了方便演示使用循环插入
	int arr[6] = { 18,15,20,23,11,10 };
	for (int i = 0; i < 6; i++)
	{
		HPPush(&hp, arr[i]);
	}

	//以上为小堆的创建
	//需要创建大堆修改向上和向下调整算法中的大于小于符号即可

	//循环出堆打印
	while (!HPEmpty(&hp))
	{
		//取堆顶元素
		printf("%d ", HPTop(&hp));
		//出堆
		HPPop(&hp);
	}
	printf("\n");
}

运行结果:

画图展示:

满足孩子结点大于父节点,这就是小堆


六、堆排序

介绍:

        根据使用演示和堆的特性,很明显,使用堆结构可以用来排序,例如小堆,堆顶就是堆的最小元素。大堆,堆顶是堆的最大元素。并且每次插入或取出数据堆会自行调整,堆顶又是最小或最大的数据。利用这一特性,我们就可以实现堆排序。

思想概述:

首先我们应该了解冒泡排序,这个比较基础,这里不详细介绍,我们知道冒泡排序参数只需要一个数组和数组大小即可,而相对的,把堆排序写成一个函数,那么堆排序也只要这两个参数,并且为了不必要的内存开销,我们不用创建真正的堆结构,那样空间复杂度为O(n),连冒泡排序空间复杂度都为O(1),因此我们不创建真正的堆,而是直接对参数中的数组操作,毕竟堆结构底层就是数组,所以对数组直接建堆也是可以的,只需要用好向上和向下这两种算法即可。

排序分为升序和降序,那么首先我们就得搞清楚哪种堆对应哪种排序,先说结论:

  • 小堆:排降序
  • 大堆:排升序

1.两种建堆方式:

堆排序(向上调整建堆):

//堆排序
void HeapSort(int* arr, int n)
{
	//小堆 -- 降序
	//大堆 -- 升序

	//向上调整算法建堆
	for (int i = 0; i < n; i++)
	{
		AdjustUp(arr, i);
	}

	//排序
	int end = n - 1;
	while (end > 0)
	{
		Swap(&arr[0], &arr[end]);
		AdjustDown(arr, 0, end);
		--end;
	}
}

 堆排序(向下调整建堆)

//堆排序
void HeapSort(int* arr, int n)
{
	//小堆 -- 降序
	//大堆 -- 升序

    //向下调整算法建堆
	for (int i = (n - 1 - 1) / 2; i >= 0; i--)
	{
		AdjustDown(arr, i, n);
	}

	//排序
	int end = n - 1;
	while (end > 0)
	{
		Swap(&arr[0], &arr[end]);
		AdjustDown(arr, 0, end);
		--end;
	}
}

  排序过程:

  1. 以小堆为例,向上调整建堆:堆顶总是最小值,我们要直接对原数组建堆,从数组首元素下标为0开始,对其使用向上调整算法,第一次传入数组和下标0,模拟堆的插入,模拟此时只有一个元素,向上调整算法不会进行,循环插入第二个元素开始,对向上调整算法传入数组和下标1,会对原数组中的前两个数据进行调整,保证下标为0处的数据是最小值,如此循环,是不是像模拟堆插入,这样就可以直接对原数组建堆了。
  2. 向下调整建堆:先找到最后一个结点的父结点赋值给 i ,(n - 1 - 1) / 2,其中n-1是最后一个结点下标,再减1除二就是计算其父结点的公式。找到最后一个结点的父结点,将其作为根结点传入向下调整算法,那么向下调整算法就会将这棵子树调整为小堆,那么i--直到0都是度为2的结点,将这些结点作为根结点向下都调整为小堆,直到i = 0,注意 i 最终必须等于0,那么就是调整整个二叉树。简单来说就是先将子树都调整为小堆,最后将整棵二叉树调整为小堆
  3. 建完堆后,排序:排序使用向下调整算法,模拟的是出堆过程,我们定义一个 end 记录最后一个元素下标,进行循环排序,循环条件是 end>0,只要最后一个元素的下标不是0就进入循环,循环中,先交换堆顶与堆底元素,然后对堆使用向下调整算法,传入参数为数组,根结点下标,数组元素个数。数组元素个数为什么传end,因为end = n-1,模拟出堆时数组元素个数n会减1,而end刚好为n-1,经过一次向下调整算法,此时数组最后一个元素为堆的最小值,堆顶元素为第二小值,然后end--,我们让最后一个元素下标定位到数组倒数第二个元素,同时也让向下调整算法的元素个数-1,这样每次向下调整算法后,新最小值总会出现在数组end下标处,直到end=0。最小值一直往后跑,所以小堆是排降序的。相对的,大堆就是升序。

使用演示:

小堆:降序


int main()
{
	int arr[6] = { 18,15,20,23,11,10 };
	HeapSort(arr, 6);
	for (int i = 0; i < 6; i++)
	{
		printf("%d ", arr[i]);
	}
	printf("\n");

	return 0;
}

运行结果:


2.时间复杂度计算

一个向上调整算法建堆,一个向下调整算法建堆,哪种算法建堆效率更高,这需要通过计算时间复杂度来比较。

首先计算向上调整算法建堆的时间复杂度:

因为堆是完全二叉树,而满⼆叉树也是完全二叉树,此处为了简化使用满⼆叉树来证明(时间复杂度本来看的就是近似值,多几个结点不影响最终结果)

向下调整算法建堆时间复杂度:

结论

向下调整算法建堆效率更高,所以我们一般使用堆排序是用向下调整算法建堆的

在此基础上计算堆排序的时间复杂度

通过分析发现,堆排序第二个循环中的向下调整与建堆中的向上调整算法时间复杂度计算⼀致,此处 不再赘述。因此,堆排序的时间复杂度为

底数2可省略


七、top-k问题

TOP-K问题:即求数据结合中前K个最大的元素或者最小的元素,⼀般情况下数据量都比较大。 比如:专业前10名、世界500强、富豪榜、游戏中前100的活跃玩家等。

对于Top-K问题,能想到的最简单直接的方式就是排序,但是:如果数据量非常大,排序就不太可取了 (可能数据都不能⼀下子全部加载到内存中)。

因此解决思路如下:

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

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

(2)用剩余的N-K个元素依次与堆顶元素来比较,不满足则替换堆顶元素

  1. 将剩余N-K个元素依次与堆顶元素比完之后,堆中剩余的K个元素就是所求的前K个最小或者最大的元素
  2. 因为小堆堆顶是最小值,因此N-K个数据中只要有比堆顶元素大的元素,就直接替换掉堆顶元素,然后使用向下调整算法,更新堆顶元素,新堆顶又是最小值,这样循环下去,最后堆中小的值全部被替换出去了,剩下的就是K个最大数据,堆顶元素就是K个最大数据中的最小值
  3. 同理,大堆堆顶是最大元素,因此只要比堆顶小的元素就替换掉堆顶元素,使用向下调整算法,最终堆里就是最小的K个数据

举例:自动生成十万个随机整数,取出前K个最大值

#include <time.h>
#include "Heap.h"

void CreateNData()
{
	//建文件,设置随机数种子
	int n = 100000;
	srand((unsigned int)time(0));
	const char* file = "data.txt";
	FILE* fin = fopen(file, "w");
	if (fin == NULL)
	{
		perror("fopen fail!");
		exit(1);
	}

	//随机生成一百万以内整数,写入文件
	for (int i = 0; i < n; i++)
	{
		int x = (rand() + i) % 1000000;
		fprintf(fin, "%d\n", x);
	}

	//刷新并关闭文件
	fclose(fin);
}

void topk()
{
	//输如K
	printf("请输入k:\n");
	int k = 0;
	scanf("%d", &k);

	//打开文件
	const char* file = "data.txt";
	FILE* fout = fopen(file, "r");
	if (fout == NULL)
	{
		perror("fopen fail!");
		exit(1);
	}

	//建数组,用于存储前K个数据
	int* minHeap = (int*)malloc(sizeof(int) * k);
	if (minHeap == NULL)
	{
		perror("malloc fail!");
		exit(1);
	}

	//先加载K个数据
	for (int i = 0; i < k; i++)
	{
		fscanf(fout, "%d", &minHeap[i]);
	}

	//建堆
	for (int i = (k - 1 - 1) / 2; i >= 0; i--)
	{
		AdjustDown(minHeap, i, k);
	}

	//排序
	int x = 0;
	while (fscanf(fout, "%d", &x) != EOF)
	{
		//找最大的前k个数:建小堆,数据比堆顶大,替换
		//找最小的前k个数:建大堆,数据比堆顶小,替换
		if (x > minHeap[0])
		{
			minHeap[0] = x;
			AdjustDown(minHeap, 0, k);
		}
	}

	//打印
	for (int i = 0; i < k; i++)
	{
		printf("%d ", minHeap[i]);
	}
	printf("\n");
}

int main()
{
	CreateNData();
	topk();

	return 0;
}

为了方便验证正确性,我们第一次创建数据文件时,手动修改K个最大值,记住第二次运行时注释掉CreateNData,不然文件会被重新写入。

6个99999开头的数据就是我手动修改的,理论上这6个数据就是最大的6个数据,我们运行验证

运行结果:


八、链式二叉树的实现

注意:前方大量递归算法

1.二叉链结构

用链表来表示⼀棵二叉树,即用链来指示元素的逻辑关系。通常的方法是链表中每个结点由三个域组成,数据域和左右指针域,左右指针分别用来给出该结点左孩子和右孩子所在的链结点的存储地址, 其结构如下:

typedef int BTDataType;

//定义链式二叉树结点结构
typedef struct BinaryTreeNode
{
	BTDataType data;
	struct BinaryTreeNode* left;  //指向当前结点左孩子
	struct BinaryTreeNode* right;  //指向当前结点右孩子
}BTNode;


2.函数声明

Tree.h

#pragma once
#include <stdio.h>
#include <stdlib.h>
#include <assert.h>
#include <stdbool.h>

typedef int BTDataType;

//定义二叉树结点结构
typedef struct BinaryTreeNode
{
	BTDataType data;
	struct BinaryTreeNode* left;
	struct BinaryTreeNode* right;
}BTNode;

//先序遍历
void PreOrder(BTNode* root);

//中序遍历
void InOrder(BTNode* root);

//后序遍历
void PostOrder(BTNode* root);

//获取二叉树结点个数
int BinaryTreeSize(BTNode* root);

//二叉树叶子结点个数
int BinaryTreeLeafSize(BTNode* root);

//二叉树第k层结点个数
int BinaryTreeLeavlKSize(BTNode* root, int k);

//求二叉树深度/高度
int BinaryTreeDepth(BTNode* root);

//查找
BTNode* BinaryTreeFind(BTNode* root, BTDataType x);

//销毁
void BinaryTreeDestory(BTNode** root);

//层序遍历
void LeavlOrder(BTNode* root);

//判断是否为完全二叉树
bool BinaryTreeComplete(BTNode* root);

因为目前链式二叉树限制过少,不像堆是完全二叉树并且有小堆大堆的限制,所以初阶二叉链中不提供插入二叉树的插入删除操作,需手动创建结点连接。

链式二叉树常用操作:

  1. PreOrder函数:先序遍历
  2. InOrder函数:中序遍历
  3. PostOrder函数:后序遍历
  4. BinaryTreeSize函数:获取二叉链节点个数
  5. BinaryTreeLeafSize函数:二叉链叶子结点个数
  6. BinaryTreeLeavlKSize函数:二叉树第K层结点个数
  7. BinaryTreeDepth函数:求二叉树深度/高度
  8. BinaryTreeFind函数:查找指定结点
  9. BinaryTreeDestory函数:销毁二叉链
  10. LeavlOrder函数:层序遍历
  11. BinaryTreeComplete函数:判断是否为完全二叉树

*前中后序遍历

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

1)前序遍历(PreorderTraversal亦称先序遍历):访问根结点的操作发⽣在遍历其左右子树之前      

访问顺序为:根结点、左子树、右子树

    

2)中序遍历(InorderTraversal):访问根结点的操作发⽣在遍历其左右子树之中(间)           

访问顺序为:左子树、根结点、右子树

    

3)后序遍历(PostorderTraversal):访问根结点的操作发⽣在遍历其左右子树之后

访问顺序为:左子树、右子树、根结点

例如:

答案:

(1)前序遍历:1、2、4、6、5、3

(2)中序遍历:6、4、2、5、1、3

(3)后序遍历:6、4、5、2、3、1

解析(递归的暴力美学):

  • 前序遍历:访问顺序简称 根左右,即先遍历根结点1打印,再遍历1的左右子树2和3,先遍历左子树2,把2看成一棵树的根结点,继续先序遍历,则先打印‘根结点’2,再访问2的左子树4,把4又看作根结点,先打印4,再访问4的左子树6,把6又继续看作一棵树,先打印6,在访问6的左右子树,但是6已经是叶子结点,因此6的左右子树为空不打印直接返回,这样4的左子树6已经打印完成,再打印4的右子树,结果4的右子树为空不打印直接返回,返回到2结点,这样2的左子树已经打印完成,接着访问2的右子树,2的右子树为5,打印5,然后返回到根节点1,最后访问1的右子树3,打印3,。至此先序遍历结束。
  • 中序遍历:访问顺序简称 左根右,思想与前序遍历相同,先一直往左子树递归到叶子结点并打印,再回归到叶子结点的父结点,打印父节点再访问其右子树....如此回归再递推。最终完成中序遍历。
  • 后序遍历:访问顺序简称 左右根,即先打印完左子树,再打印完右子树,最后打印根节点,递归思想与前面一至,只是打印(访问)的顺序不同,注意后续遍历的特点就是整棵二叉树的根节点是最后打印的。


3.函数实现

1.前/中/后序遍历函数:PreOrder、InOrder、PostOrder

#include "Tree.h"
#include "Queue.h"

//先序遍历,根左右
void PreOrder(BTNode* root)
{
	if (root == NULL)
	{
		return;
	}
	printf("%d ", root->data);
	PreOrder(root->left);
	PreOrder(root->right);
}

//中序遍历,左根右
void InOrder(BTNode* root)
{
	if (root == NULL)
	{
		return;
	}
	InOrder(root->left);
	printf("%d ", root->data);
	InOrder(root->right);
}

//后序遍历,左右根
void PostOrder(BTNode* root)
{
	if (root == NULL)
	{
		return;
	}
	PostOrder(root->left);
	PostOrder(root->right);
	printf("%d ", root->data);
}

解析:

  1. 都采用递归实现,递归停止条件就是当前结点 root == NULL,这就代表当前结点为空,为空直接返回。
  2. 由上述图可以看到,递归就是一层一层的往下走,首先会一直往左子树走,走到底部为空,然后返回到叶子结点的函数栈帧中,继续执行叶子结点的剩余函数体,也就是继续走到其右孩子结点。当叶子结点的右孩子递归完后,叶子结点的函数栈帧结束,然后返回到叶子结点的父结点中,如此回归,直到回到根结点,也是最初的函数栈帧,在根结点的函数体中继续执行递归右子树,直到全部递归完毕。
  3. 三种遍历只有打印的位置不同,前序遍历是先访问根结点,所以先打印当前结点,因此打印位置在最前面。中序遍历要先打印左子树,因此一直递归当前结点的左子树,直到最后一棵左子树递归完,打印这最后一棵左子树的数据,因此打印位置在 InOrder(root->left) 后面。后续遍历也一样,先递归遍历左子树,再递归遍历右子树,那为什么打印位置在最后面,因为递归最后一个结点的左右孩子都为空,此时这个叶子结点就是其父结点的左子树,就打印它,打印完后返回到其父结点的函数栈帧中,此时刚执行完 PostOrder(root->left) ,继续遍历其右子树,最终也会递归到右子树的最后一个结点并打印它,再次回到父结点的函数栈帧中,它的代码也执行到了打印代码这一行,打印完后,也就遵循了左右根的打印顺序,其函数栈帧结束后继续往上返回,最终到根结点的函数栈帧中。
  4. 这就是递归的暴力美学,个人表述能力有限,可能无法准确表达,如果无法理解的话,可以在每次递归时,也就是每次自己调用自己时,画图将函数代码复制一份,一步一步走下去,然后回归,相信大家一定能够理解的

执行演示,验证答案:

#include "Tree.h"

//创建结点
BTNode* BuyNode(BTDataType x)
{
	BTNode* newnode = (BTNode*)malloc(sizeof(BTNode));
	if (newnode == NULL)
	{
		perror("maloc fail!");
		exit(1);
	}
	newnode->data = x;
	newnode->left = newnode->right = NULL;
}

int main()
{
	//手动创建结点
	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 = node3;
	node2->left = node4;
	node2->right = node5;
	node4->left = node6;

	printf("先序遍历:");
	PreOrder(node1);
	printf("\n");
	printf("中序遍历:");
	InOrder(node1);
	printf("\n");
	printf("后序遍历:");
	PostOrder(node1);

	return 0;
}

运行结果:与答案一致


2.BinaryTreeSize函数

功能:用于获取二叉树的结点个数

//获取二叉树结点个数
int BinaryTreeSize(BTNode* root)
{
	if (root == NULL)
	{
		return 0;
	}
	return 1 + BinaryTreeSize(root->left) + BinaryTreeSize(root->right);
}

解析:

  1. 相信大家理解了前中后序遍历后,对于求结点个数这种递归应该不会太难理解。
  2. 原理就是每进入到一个不为空的左孩子结点中,就+1继续递归左孩子,直到遍历到空结点就回归,回归上一层结点继续递归其右孩子,最终递归完所有子树结点回到根结点。因为每一个结点的 return 中都有1,因此就相当于把每一个有效结点计数了一次。


3.BinaryTreeLeafSize函数

功能:求二叉树叶子结点个数

//二叉树叶子结点个数
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);
}

解析:

  1. 因为该函数只计算叶子结点个数,所以递归到空节点就返回0,如果当前结点不为空,并且其左右孩子结点都为空,就说明当前结点为叶子结点,返回1。如果都不是,就继续递归左子树,递归完左子树就继续递归右子树。


4.BinaryTreeLeavlKSize函数

功能:求第k层结点个数

int BinaryTreeLeavlKSize(BTNode* root, int k)
{
	if (root == NULL)
	{
		return 0;
	}
	if (k == 1)
	{
		return 1;
	}

	return BinaryTreeLeavlKSize(root->left, k - 1) + BinaryTreeLeavlKSize(root->right, k - 1);
}

画图分析:


5.BinaryTreeDepth函数

功能:求二叉树深度/高度

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

解析:

  1. 二叉树深度就是二叉树的最大层次,因为最底层都是叶子结点,所以我们只需要递归遍历完根结点左右子树,也就是当一个结点的左右孩子都返回为0,那么这个结点就是叶子结点,从这个结点开始 return 回归,当然每个结点都有左右子树,我们用三目操作符判断,哪个大就将哪个+1返回,从叶子结点开始回归,每一次回归都会被+1,最终回到根结点函数体中,+1返回的就是整棵二叉树的深度


6.BinaryTreeFind函数

功能:查找指定结点,并返回该节点地址

//查找
BTNode* BinaryTreeFind(BTNode* root, BTDataType x)
{
	if (root == NULL)
	{
		return NULL;
	}
	if (root->data == x)
	{
		return root;
	}

	BTNode* leftFind = BinaryTreeFind(root->left, x);
	if (leftFind != NULL)
	{
		return leftFind;
	}

	BTNode* rightFind = BinaryTreeFind(root->right, x);
	if (rightFind != NULL)
	{
		return rightFind;
	}

	return NULL;
}

解析:

  1. 查找结点,并通过储存数据查找,那当然就是通过递归遍历,然后比对数据,首先遍历到空结点当然还是返回空,如果当前结点数据域等于查找参数,就直接返回该结点。假如找到了,那么递归就不用继续下去,直接回归,因此设立两个变量 leftFind 和 rightFind ,用于接收递归遍历的结果,如果不为空,那就说明找到了,也就直接一路返回。假如左右孩子结点都返回空没找到,就只能返回空了。


7.BinaryTreeDestory函数

功能:销毁整棵二叉树

//销毁
void BinaryTreeDestory(BTNode** root)
{
	if (*root == NULL)
	{
		return;
	}

	BinaryTreeDestory(&((*root)->left));
	BinaryTreeDestory(&((*root)->right));

	free(*root);
	*root = NULL;
}

解析:

  1. 因为释放掉结点后,也要将实参本身置空,避免野指针,所以传二级指针,二叉树的特性特别适合递归,因此销毁二叉树也是递归式销毁,它会递归到子树最后一个节点,然后开始一步步销毁。


8.层序遍历:LeavlOrder函数

功能:从左到右,从上往下一层一层打印每个数据

画图分析:

代码:

//层序遍历
//借助队列实现
void LeavlOrder(BTNode* root)
{
	//创建队列
	Queue q;
	QueueInit(&q);
	QueuePush(&q, root);

	while (!QueueEmpty(&q))
	{
		//取对头,打印,出队
		BTNode* front = QueueFront(&q);
		printf("%d ", front->data);
		QueuePop(&q);

		//出队结点左右孩子入队
		if (front->left)
		{
			QueuePush(&q, front->left);
		}
		if (front->right)
		{
			QueuePush(&q, front->right);
		}

	}

	QueueDesTroy(&q);
}

既然要用到队列,我们就可以使用之前写的队列的函数接口,记得将队列中的 typedef int QTypeData修改成 typedef struct BinaryTreeNode* QDatatype,这样才能储存结点指针。

使用演示:(使用上图中的例子)

#include "Tree.h"

int main()
{
	//手动创建结点
	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 = node3;
	node2->left = node4;
	node2->right = node5;
	node4->left = node6;

	printf("层序遍历:");
	LeavlOrder(node1);

	return 0;
}

运行结果:


9.BinaryTreeComplete函数

功能:判断是否为完全二叉树

画图分析:

解析:

  1. 因此,通过上述示例,我们可以推断完全二叉树用队列进行出队入队操作后,在第一次取对头元素为空时,此时队列中全是空结点,而非完全二叉树在第一次取到空元素时,队列中并非全是空结点

代码:

//判断是否为完全二叉树
//利用队列实现
bool BinaryTreeComplete(BTNode* root)
{
	//创建队列
	Queue q;
	QueueInit(&q);
	QueuePush(&q, root);

	while (!QueueEmpty(&q))
	{
		//取对头,如果为空则停止循环
		//反之,将对头的左右孩子入队
		BTNode* top = QueueFront(&q);
		QueuePop(&q);
		if (top == NULL)
		{
			break;
		}

		QueuePush(&q, top->left);
		QueuePush(&q, top->right);
	}

	//继续循环取对头元素,直到遇到不为空说明不是完全二叉树
	//反之则为二叉树
	while (!QueueEmpty(&q))
	{
		BTNode* top = QueueFront(&q);
		QueuePop(&q);

		if (top != NULL)
		{
			QueueDesTroy(&q);
			return false;
		}
	}

	QueueDesTroy(&q);
	return true;
}

解析:

  1. 第一次循环取到空结点就结束循环,第二次循环就是判断队列剩余元素是否全为空,如果有元素不为空,就返回 false 说明不是完全二叉树,循环结束说明队列元素都为空,此时返回 true 表示二叉树是完全二叉树


九、二叉树算法题

1.单值二叉树

题目出处:965. 单值二叉树 - 力扣(LeetCode)

解法:

bool isUnivalTree(struct TreeNode* root) {
    // 递归遍历--类似先序遍历
    if (root == NULL) {
        return true;
    }

    if (root->left && root->left->val != root->val) {
        return false;
    }
    if (root->right && root->right->val != root->val) {
        return false;
    }

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

解析:

  1. 递归判断,如果结点为空,返回true,说明要么二叉树为空树,要么二叉树走到底了,能走到底说明前面结点值都相同,接下来两个判断是确认孩子结点存在的同时,且数据域相同,因为要判断所有结点,因此这里判断是否不相同,不相同返回false,相同就继续递归判断,直到走到空结点,说明前面结点数据域全相等


2.相同的树

题目出处:100. 相同的树 - 力扣(LeetCode)

解法:

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

解析:

  1. 两棵树当前结点都为空返回 true,其中有一棵树为空,则返回false,前面两个判断都没问题就判断数据域是否不相同,为什么判断不相同,因为要遍历所有结点,不能因为一个结点相同就返回true,因此这里判断不相同,不相同返回false,相同则继续递归下一结点。


3.对称二叉树

题目出处:101. 对称二叉树 - 力扣(LeetCode)

解法:

// 判断相同树
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->right) && isSameTree(p->right, q->left);
}

// 判断子树对称
bool isSymmetric(struct TreeNode* root) {
    return isSameTree(root->left, root->right);
}

解析:

  1. 首先这题我们需要用到上一题中的函数,因为这题是判断对称二叉树,我们只需要改变上一题中孩子结点的指向,判断相同二叉树就同时判断两棵树的相同位置结点,判断对称二叉树就比较二叉树的左右子树,一个遍历左孩子,另一个就需要遍历右孩子,因此修改上一函数参数即可


4.另一棵树的子树

题目出处:572. 另一棵树的子树 - 力扣(LeetCode)

解法:

//判断相同树
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);
}

解析:

  1. 首先,当前结点等于空就返回 false,因为当前结点为空就不可能有子树了,也就不可能有与subRoot一样的子树,题目有范围,俩树都不为空
  2. 然后我们需要继续用到 isSameTree 函数,用来判断当前结点与 subRoot 结点是否相同,相同就直接返回 true,因为本身找到了就不需要继续递归了,不相同就继续递归左右孩子结点,切记两递归函数中间用 || 连接,因为只要有一个返回的是true就代表找到了。


5.二叉树的前序遍历

题目出处:144. 二叉树的前序遍历 - 力扣(LeetCode)

解法:

typedef struct TreeNode TreeNode;
//递归计算结点个数
int TreeSize(TreeNode* root)
{
	if (root == NULL)
	{
		return 0;
	}

	return 1 + TreeSize(root->left) + TreeSize(root->right);
}

//递归遍历
void _preorderTraversal(TreeNode* root, int* arr, int* pi)
{
	//先序遍历 --- 根左右
	if (root == NULL)
	{
		return;
	}

	arr[(*pi)++] = root->val;
	_preorderTraversal(root->left, arr, pi);
	_preorderTraversal(root->right, arr, pi);
}

int* preorderTraversal(struct TreeNode* root, int* returnSize)
{
	*returnSize = TreeSize(root);
	int* returnArr = (int*)malloc(sizeof(int) * (*returnSize));

	int i = 0;
	_preorderTraversal(root, returnArr, &i);

	return returnArr;
}

解析:

  1. 与前面先序遍历不同的是需要我们将数据存储在数组中,那么既然要数组存储数据,就需要知道数组大小,我们用已经实现过的计算二叉树结点个数的函数,直接使用即可
  2. 只需要将先序遍历中的打印改为存储即可,需要注意的是,递归中想要计数就需要用到指针,因此传入 i 的地址。


6.二叉树的中序、后序遍历

题目出处:144. 二叉树的前序遍历 - 力扣(LeetCode)

145. 二叉树的后序遍历 - 力扣(LeetCode)

解法一致,不再赘述

题解:

中序:

typedef struct TreeNode TreeNode;
//递归计算结点个数
int TreeSize(TreeNode* root)
{
	if (root == NULL)
	{
		return 0;
	}

	return 1 + TreeSize(root->left) + TreeSize(root->right);
}

void _inorderTraversal(TreeNode* root, int* arr, int* pi)
{
	//中序遍历 --- 左根右
	if (root == NULL)
	{
		return;
	}

	_inorderTraversal(root->left, arr, pi);
	arr[(*pi)++] = root->val;
	_inorderTraversal(root->right, arr, pi);
}

int* inorderTraversal(struct TreeNode* root, int* returnSize)
{
	*returnSize = TreeSize(root);
	int* returnArr = (int*)malloc(sizeof(int) * (*returnSize));

	int i = 0;
	_inorderTraversal(root, returnArr, &i);

	return returnArr;
}

后序: 

typedef struct TreeNode TreeNode;
//递归计算结点个数
int TreeSize(TreeNode* root)
{
	if (root == NULL)
	{
		return 0;
	}

	return 1 + TreeSize(root->left) + TreeSize(root->right);
}

void _postorderTraversal(TreeNode*root, int*arr, int*pi)
{
    if(root == NULL)
    {
        return;
    }

    _postorderTraversal(root->left, arr, pi);
    _postorderTraversal(root->right, arr, pi);
    arr[(*pi)++] = root->val;
}

int* postorderTraversal(struct TreeNode* root, int* returnSize)
{
    *returnSize = TreeSize(root);
    int*returnArr = (int*)malloc(sizeof(int)*(*returnSize));

    int i = 0;
    _postorderTraversal(root, returnArr, &i);

    return returnArr;
}


7.二叉树的构建及遍历

题目出处:二叉树遍历_牛客题霸_牛客网

解析:

  1. 与前面不同的是这题需要根据先序遍历构建二叉树,然后以中序遍历的方式打印出来
  2. 我们画图分析:
  3. 先序遍历为:根左右,第一个数据 a 就是根结点,接下来,# 代表空,每一个叶子结点左右孩子必为 #,由此我们还原出二叉树,并且符合题目给出的中序遍历。
  4. 首先,题目有说字符串长度不超过100,因此我们创建一个长度为100的字符数组用来存储输入的字符串。并且需要一个申请结点的函数 BuyNode,
  5. 根据先序遍历的特点,我们需要以根左右的顺序进行访问,因此仍需要以递归的方式进行创建二叉树,在createTree中,需要数组和下标两个参数,首先判断数组当前元素是否是 # 号,如果是,那么当前结点就为空,因为在申请结点时就已经赋值为空了,因此此时只需要让下标往后走一位,然后返回。不是则先生成当前结点,再继续递归生成自己的左右孩子结点。
  6. 当然递归仅用文字很难描述清楚,我们继续画图:
  7. 最主要的是这题给出了空节点的位置,这样才得以通过先序遍历构建出二叉树。

题解:

#include <stdio.h>

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

//申请结点
BTNode* BuyNode(char c)
{
	BTNode* newnode = (BTNode*)malloc(sizeof(BTNode));
	newnode->data = c;
	newnode->left = newnode->right = NULL;

	return newnode;
}

BTNode* createTree(char* arr, int* pi)
{
	if (arr[*pi] == '#')
	{
		++(*pi);
		return NULL;
	}
	BTNode* root = BuyNode(arr[(*pi)++]);
	root->left = createTree(arr, pi);
	root->right = createTree(arr, pi);

    return root;
}

//中序遍历
void InOrder(BTNode* root)
{
	if (root == NULL)
	{
		return;
	}
	InOrder(root->left);
	printf("%c ", root->data);
	InOrder(root->right);
}

int main()
{
	//首先创建数组,存储先序遍历数据
	char arr[100];
	scanf("%s", arr);

	//根据先序遍历顺序创建二叉树
	int i = 0;
	BTNode* root = createTree(arr, &i);

	//创建完二叉树,使用中序遍历
	InOrder(root);

	return 0;
}


十、二叉树选择题

二叉树性质:

任何⼀棵二叉树,如果度为 0 的叶结点个数为 n0,度为 2 的分支结点个数为n2,

则有n0 = n2 +1

公式来历:简单来说通过边数相等推导

a=c-1,移位一下就是:c = a+1


例1:在具有 2n 个结点的完全二叉树中,叶子结点个数为( )

  • A:n
  • B:n+1
  • C:n-1
  • D:n/2

题解:

  1. 结点总数为 2n,假设度为2的结点数为 a,度为1的结点数为 b,度为0的结点数为 c
  2. 因此 a+b+c = 2n,由二叉树性质得 a=c-1,带入得 2c+b-1 = 2n
  3. 不要忘了题目中完全二叉树的条件,完全二叉树度为1的节点个数只有两种情况,要么为1,要么为0,分类讨论就行。
  4. b = 0:即 2c - 1 = 2n,c = (2n+1)/2,明显不是整数
  5. b = 1:即 2c = 2n,c=n,这就是正确答案

例2:. 一棵完全二叉树的结点数位为 531 个,那么这棵树的高度为( )

  • A:11
  • B:10
  • C:8
  • D:12

解析:

  1. 二叉树高度公式为 ,最大结点数 n =,2的10次方等于1024,2的9次方为512,因此9层是不够的,答案是10


链式二叉树遍历选择题:

(1)某完全⼆叉树按层次输出(同⼀层从左到右)的序列为 ABCDEFGH 。该完全⼆叉树的前序序列为()

  • A:ABDHECFG
  • B:ABCDEFGH
  • C:HDBEAFCG
  • D:HDEBFGCA

解析:

  1. 选A

(2)二叉树的先序遍历和中序遍历如下:先序遍历: EFHIGJK; 中序遍历: HFIEJKG. 则二叉树根结点为()

  • A:E
  • B:F
  • C:G
  • D:H

解析:

  1. 先序遍历以根结点开始,即E
  2. 选A

(3)设一课二叉树的中序遍历序列: badce ,后序遍历序列: bdeca ,则二叉树前序遍历序列为为()

  • A:adbce
  • B:decab
  • C:debac
  • D:abcde

解析:

  1. 选D

4)某二叉树的后序遍历序列与中序遍历序列相同,均为 ABCDEF ,则按层次输出(同⼀层从左到右) 的序列()

  • A:FEDCBA
  • B:CBAFED
  • C:DEFCBA
  • D:ABCDEF

解析:

  1. 依旧先根据后序遍历找根结点。
  2. 选A


总结

        以上就是本文的全部内容,码字和画图不易,感谢支持!

猜你喜欢

转载自blog.csdn.net/x_p96484685433/article/details/142915204
今日推荐