从零构建二叉树:C语言实现核心方法与最佳实践

二叉树存储结构

1.顺序结构

用数组实现存储数据

2.链式结构

实现顺序结构二叉树

1.堆

分类:
小堆/最大堆/大根堆:根结点最大的堆
大堆/最小堆/小根堆:根结点最小的堆

下图所示,小根堆里的左右子树的根结点也是左右子树中最小的结点
在这里插入图片描述

2.堆的实现

(1)初始化和销毁

//Heap.c
//初始化
void HPInit(HP* php)
{
	assert(php);
	php->arr = NULL;
	php->size = php->capacity = 0;
}
//销毁
void HPDestroy(HP* php)
{
	assert(php);
	if (php->arr)
		free(php->arr);
	php->arr = NULL;
	php->size = php->capacity = 0;
}

(2)入堆——向上调整算法

向上调整算法
  1. 底层结构是动态数组,空间不足,不足以插入新的数据时需要扩容。
  2. 只有往数组中尾插数据时间复杂度才是最好的,那么就先将数据尾插进数组。在这个数据插入之前,这就已经是一个堆结构了,所以,只需和其父结点比较大小,若交换成功了,则还要接着和新的父结点比较,直到child走到根结点退出循环(根结点没有父结点),这个向上比较交换的过程我们叫做向上调整算法。
  3. size++放在最后是因为向上调整算法需要size为参数,以确定新插入数据的位置,方便找到其父结点。

在这里插入图片描述

//Heap.c
//交换数据
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, newCapacity * sizeof(HPDataType));
		if (tmp == NULL)
		{
			perror("realloc fail!");
			exit(1);
		}
		php->arr = tmp;
		php->capacity = newCapacity;
	}
	//直接尾插数据
	php->arr[php->size] = x;
	//向上调整
	AdjustUp(php->arr, php->size);
	//++size
	++php->size;
}

试着调试、打印出来:

在这里插入图片描述
画出堆结构能更直观一点:

在这里插入图片描述

计算向上调整算法建堆时间复杂度

我们以最坏的情况计算,就是除了根结点的所有结点都要移动到第一层处。
因为堆是完全二叉树,而满⼆叉树也是完全⼆叉树,此处为了简化使用满⼆叉树来证明(时间复杂度本来看的就是近似值,多几个结点不影响最终结果)

在这里插入图片描述

这里时间复杂度与所有结点的移动步数之和有关

在这里插入图片描述

(3)出堆——向下调整算法

出的是堆顶数据

堆的底层结构是数组,如果堆顶数据直接出堆,剩下数据往前挪动一位会导致堆结构被打乱。那么就要介绍一种新的算法思想——向下调整算法!

向下调整算法

向下调整算法有一个前提:左右子树必须是一个堆,才能调整。

  1. 堆顶和最后一个数据交换
  2. size - -,即堆顶数据出堆,堆中有效的数据个数是size - 1
  3. 将堆顶数据向下调整到满足堆特性为止。根结点数据被改变,根结点作为父结点(parent)与之左右孩子(child为左孩子,child + 1为右孩子)中的最小/大结点比较、交换

在这里插入图片描述

//Heap.c
//堆是否为空。为空,返回true
bool HPEmpty(HP* php)
{
	assert(php);
	return php->size == 0;
}
//向下调整
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[parent] < arr[child])
		{
			Swap(&arr[child], &arr[parent]);
			parent = child;
			child = 2 * parent + 1;
		}
		else {
			break;
		}
	}
}
//出堆
void HPPop(HP* php)
{
	//判断堆是否为空
	assert(!HPEmpty(php));
	Swap(&php->arr[0], &php->arr[php->size - 1]);
	--php->size;
	//向下调整
	AdjustDown(php->arr, 0, php->size);
}

在这里插入图片描述

在这里插入图片描述

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

我们还是以最坏的情况计算,就是除了叶子结点的所有结点都要移动到最后一层处。

在这里插入图片描述

对比向上调整算法建堆时间复杂度,向下调整算法建堆时间复杂度更好!

除了严谨的数学推理得出向下调整算法建堆时间复杂度更好,还有一种更直观能看出来的方法:

在这里插入图片描述
向下调整算法建堆,结点少移动层数多,结点多但是移动层数少。向上调整算法建堆,结点多移动层数多,时间复杂度又与移动步数有关,很明显,向上调整算法建堆比向下调整算法建堆总的移动步数要多,效率低。

(4)取堆顶数据

//Heap.c
//取堆顶数据
HPDataType HPTop(HP* php)
{
	assert(!HPEmpty(php));
	return php->arr[0];
}

3.堆的应用

(1)堆排序

说到排序,可以联想到之前学习C语言的时候学到的冒泡排序,以排序整型数据为例,冒泡排序总是可以把一组乱序的数据排成有序(升序/降序)的数据:

void BubbleSort(int* arr, int size)
{
	for (int i = 0; i < size - 1; i++)
	{
		int flag = 1;//假设已经有序
		for (int j = 0; j < size - i - 1; j++)
		{
			//进来交换了,说明无序
			flag = 0;
			//升序	>
			//降序	<
			if (arr[j] > arr[j + 1])
			{
				Swap(&arr[j], &arr[j + 1]);
			}
		}
		if (flag == 1)
		{
			break;
		}
	}
}

可以看出,冒泡排序的时间复杂度是O(n^2)(最好的情况下,数组中的数据已经是有序的,时间复杂度:O(1)),这个复杂度不是很好。

那么堆排序可不可以像冒泡排序一样将一组数据排成有序的?堆排序的时间复杂度会不会更好点?

版本一:借助数据结构堆来实现堆排序

是可以实现,不过不是堆排序思想,

//堆排序——借助数据结构堆来实现
void HeapSort(int* arr, int n)
{
	HP hp;
	HPInit(&hp);
	//将数组里的数据入堆
	int i = 0;
	for (i = 0; i < n; i++)
	{
		HPPush(&hp, arr[i]);
	}
	i = 0;
	while (!HPEmpty(&hp))
	{
		int top = HPTop(&hp);
		arr[i++] = top;
		HPPop(&hp);
	}
	HPDestroy(&hp);
}
int main()
{
	int arr[] = { 17, 20, 10, 13, 19, 15 };
	int size = sizeof(arr) / sizeof(arr[0]);
	printf("排序前:");
	for (int i = 0; i < size; i++)
	{
		printf("%d ", arr[i]);
	}
	printf("\n");

	HeapSort(arr, size);

	printf("排序后:");
	for (int i = 0; i < size; i++)
	{
		printf("%d ", arr[i]);
	}
	printf("\n");
	return 0;
}
版本二:给定的数组,调整此数组为堆结构,将数组中的数据变成有序

假设给定数组:

在这里插入图片描述

向下调整算法建堆:先调整右子树,再调整左子树,最后调整整个二叉树

找最后一个数据的父结点:n是数组中有效的数据个数,最后一个数据的下标n - 1,最后一个数据的父结点下标 i =(n - 1 - 1)/ 2,向下调整。
再找左子树的根结点 i =(n - 1 - 1)/ 2 - 1,向下调整。
二叉树根结点下标 i = 0,向下调整。

以建大堆为例

向上调整算法建堆:除了根结点没有父结点,其余结点从上至下、从左到右依次向上调整

以建小堆为例

//给定的数组,调整此数组为堆结构
void HeapSort(int* arr, int n)
{
	建堆——向下调整
	//for (int i = (n - 1 - 1) / 2; i >= 0; i--)
	//{
	//	AdjustDown(arr, i, n);
	//}
	
	//建堆——向上调整
	for (int i = 1; i <= n - 1; i++)
	{
		AdjustUp(arr, i);
	}
	
	//将数组中的数据变成有序——实现堆排序
	int end = n - 1;
	while (end > 0)
	{
		Swap(&arr[0], &arr[end]);
		AdjustDown(arr, 0, end);
		end--;
	}
}
int main()
{
	int arr[] = { 17, 20, 10, 13, 19, 15 };
	int size = sizeof(arr) / sizeof(arr[0]);
	printf("排序前:");
	for (int i = 0; i < size; i++)
	{
		printf("%d ", arr[i]);
	}
	printf("\n");

	
	HeapSort(arr, size);


	printf("排序后:");
	for (int i = 0; i < size; i++)
	{
		printf("%d ", arr[i]);
	}
	printf("\n");
	return 0;
}

升序—大堆

在这里插入图片描述

降序—小堆

在这里插入图片描述

堆排序时间复杂度计算

在这里插入图片描述

(2)TOP-K问题

TOP-K问题:即求数据集合中前K个最大的元素或者最小的元素,⼀般情况下数据量(n)都比较大。(n >> k)
比如:专业前10名、世界500强、富豪榜、游戏中前100的活跃玩家等。
对于Top-K问题,能想到的最简单直接的方式就是排序,但是:如果数据量非常大,排序就不太可取了(可能数据都不能一下子全部加载到内存中)。最佳的方式就是用堆来解决,基本思路如下:

假设n是10亿个整数,需要申请多大的内存?
1G = 1024MB = 1024 * 1024KB = 1024 * 1024 * 1024byte(10亿个字节)
10亿个整数,10亿 * 4 = 40亿字节 = 4G,需要申请4G大小的内存。

假设只有1G内存,怎么办?
10亿个数据分成4份,分别用来建堆查找前k个最大/最小数据,每个堆先后消耗1G内存,最后再查找这4份数据中前k个最大/最小数据。

假设只有1KB的内存,怎么办?

在这里插入图片描述

取数据集合中前k个数据进行建堆,遍历剩下的n - k个数据与堆顶比较。

找最大的k个数据,建小堆,比堆顶数据大的就入堆
找最小的k个数据,建大堆,比堆顶数据小的就入堆

首先肯定要创建数据集合,随机写入的数据都不超过1000000:

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 error");
		return;
	}
	for (int i = 0; i < n; i++)
	{
		int x = (rand() + i) % 1000000;
		fprintf(fin, "%d\n", x);
	}
	fclose(fin);
}

运行一下,按照图示顺序依次点击,就能看到写好的数据集合:

在这里插入图片描述

void Topk()
{
	int k = 0;
	printf("请输入K:");
	scanf("%d", &k);

	const char* file = "data.txt";
	FILE* fout = fopen(file, "r");
	if (fout == NULL)
	{
		perror("fopen error");
		exit(1);
	}

	//以找最大的前k个数据为例,建小堆
	int* minHeap = (int*)malloc(sizeof(int) * k);
	if (minHeap == NULL)
	{
		perror("malloc fail!");
		exit(2);//退出码不一样就知道在哪里退出的了
	}
	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);
	}
	//遍历剩下的n - k个数据,跟堆顶进行比较,大的入堆
	int x = 0;
	while (fscanf(fout, "%d", &x) != EOF)
	{
		if (x > minHeap[0])
		{
			minHeap[0] = x;
			AdjustDown(minHeap, 0, k);
		}
	}
	for (int i = 0; i < k; i++)
	{
		printf("%d ", minHeap[i]);
	}
	fclose(fout);
}
int main()
{
	//CreateNData();
	Topk();
	return 0;
}

时间复杂度:

在这里插入图片描述

在这里插入图片描述

在文件中手动构造几个大于1000000的数据并保存:

在这里插入图片描述

执行代码,符合预期:

在这里插入图片描述

实现链式结构二叉树

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

//二叉树结点的结构
typedef char BTDataType;
//二叉链
typedef struct BinaryTreeNode{
	BTDataType data;
	struct BinaryTreeNode* left;
	struct BinaryTreeNode* right;
}BTNode;

这里说的二叉树就是普通的二叉树,并不是完全二叉树,满足是二叉树的条件:

  1. 不存在度超过2
  2. 结点有左右之分

下面这些都是二叉树:

在这里插入图片描述
这里讲解向二叉树中插入数据就没有意义了,因为数据可以随便插入,只要保证二叉树度不超过2就行。

二叉树的创建方式比较复杂,为了更好的步入到二叉树内容中,我们先手动创建⼀棵链式二叉树,以下面这棵二叉树为例:

在这里插入图片描述

BTNode* buyNode(char x)
{
	BTNode* node = (BTNode*)malloc(sizeof(BTNode));
	if (node == NULL)
	{
		perror("malloc fail!");
		exit(1);
	}
	node->data = x;
	node->left = node->right = NULL;
	return node;
}
BTNode* createBinaryTree()
{
	BTNode* nodeA = buyNode('A');
	BTNode* nodeB = buyNode('B');
	BTNode* nodeC = buyNode('C');
	BTNode* nodeD = buyNode('D');
	BTNode* nodeE = buyNode('E');
	BTNode* nodeF = buyNode('F');

	nodeA->left = nodeB;
	nodeA->right = nodeC;
	nodeB->left = nodeD;
	nodeC->left = nodeE;
	nodeC->right = nodeF;
	return nodeA;
}
void test01()
{
	BTNode* root = createBinaryTree();
}
int main()
{
	test01();
	return 0;
}

回顾二叉树的概念,二叉树分为空树和非空二叉树,非空二叉树由根结点、根结点的左子树、根结点的右子树组成的

在这里插入图片描述
根结点的左子树和右子树分别又是由子树结点、子树结点的左子树、子树结点的右子树组成的,因此二叉树定义是递归式的,后序链式二叉树的操作中基本都是按照该概念实现的。

1.前中后序遍历、层次遍历

前中后序遍历都是深度优先遍历

⼆叉树的遍历有:前序/中序/后序的递归结构遍历

前序遍历(Preorder Traversal 亦称先序遍历):先遍历根节点,再遍历左子树,最后遍历右子树。——根左右
中序遍历(Inorder Traversal):先遍历左子树,再遍历根节点,最后遍历右子树。——左根右
后序遍历(Postorder Traversal):先遍历左子树,再遍历右子树,最后遍历根节点。——左右根
层序遍历:按照层次依次遍历(从上到下,从左到右依次遍历)

在这里插入图片描述

//Tree.c
//前序遍历——根左右
void Preorder(BTNode* root)
{
	if (root == NULL)
	{
		printf("NULL ");
		return;
	}
	printf("%c ", root->data);
	Preorder(root->left);
	Preorder(root->right);
}
//中序遍历——左根右
void Inorder(BTNode* root)
{
	if (root == NULL)
	{
		printf("NULL ");
		return;
	}
	Inorder(root->left);
	printf("%c ", root->data);
	Inorder(root->right);
}
//后序遍历
void Postorder(BTNode* root)
{
	if (root == NULL)
	{
		printf("NULL ");
		return;
	}
	Postorder(root->left);
	Postorder(root->right);
	printf("%c ", root->data);
}

前序遍历图解

函数return或执行到最后一行,函数的栈帧就会被销毁,销毁后找到上一个函数,继续执行上一个函数即当前函数中的代码。

基于手动创建的二叉树代码,调用前序遍历函数,遍历结果:

在这里插入图片描述

函数递归栈帧图:

在这里插入图片描述

把函数递归栈帧图画熟练了就可以将函数递归栈帧图中的一个一个函数栈帧看成一个一个的结点的递归图:

在这里插入图片描述

中序遍历图解

基于手动创建的二叉树代码,调用中序遍历函数,遍历结果:

在这里插入图片描述
函数递归栈帧图:
在这里插入图片描述

将函数递归栈帧图中的一个一个函数栈帧看成一个一个的结点的递归图:
在这里插入图片描述

后序遍历图解

基于手动创建的二叉树代码,调用后序遍历函数,遍历结果:

在这里插入图片描述

函数递归栈帧图:

在这里插入图片描述
将函数递归栈帧图中的一个一个函数栈帧看成一个一个的结点的递归图:

在这里插入图片描述

层序遍历

层序遍历——广度优先遍历

思路:借助数据结构——队列,将根结点入队列,循环判断队列是否为空,不为空取队头,将队头结点的不为空的左右孩子入队列。

注意:

  1. 队列里进入的是结点。
  2. 左右孩子不为空入队列,左右孩子为空不入队列。

在这里插入图片描述

添加队列实现方法:

  1. 右击头文件:

在这里插入图片描述

  1. ctrl+c:
    在这里插入图片描述

  2. ctrl+v进今天的文件中,选中点击添加:

在这里插入图片描述

  1. 拖拽到源文件中:

在这里插入图片描述

在Tree.c中实现层序遍历,函数中定义队列变量时,Tree.c要包含头文件Queue.h:

在这里插入图片描述

队列中的结点里存储的数据是二叉树结点类型,实现图中写法要包含头文件Tree.h:

这里说一下为什么二叉树结点数据类型是指针类型的?
——存放结构体的地址更节省空间。如果存放整个结构体的话,是比较浪费空间的。

在这里插入图片描述

但是问题来了,Tree.c里包含两个头文件,而Queue.h又包含头文件Tree.h,会有冲突:
在这里插入图片描述
怎么样才能解决在Queue.h中不包含头文件Tree.h?

在这里插入图片描述

加了关键字struct表示前置声明,typedef 给数据类型取别名,int是内置的数据类型,所以编译器会识别出来,会给int取别名。struct同理,即便之前没有定义过结构体类型,但是编译器能识别出struct,就知道是在给结构体类型取别名,所以这样就无需加头文件Tree.h了!

上面两种写法都可以,用第一个是因为是原始的结构体指针类型写法。

//Tree.c
//层序遍历
void LevelOrder(BTNode* root)
{
	Queue q;
	QueueInit(&q);
	QueuePush(&q, root);
	while (!QueueEmpty(&q))
	{
		//取队头,出队,打印结点值
		BTNode* top = QueueFront(&q);
		QueuePop(&q);
		printf("%c ", top->data);
		//队头结点的非空左右孩子结点入队
		if (top->left)
		{
			QueuePush(&q, top->left);
		}
		if (top->right)
		{
			QueuePush(&q, top->right);
		}
	}
	QueueDestroy(&q);
}

2.二叉树结点个数

二叉树结点的个数 = 1 + 左子树结点的个数 + 右子树结点的个数

左/右子树的结点个数算法与二叉树结点的个数算法一致,下图辅助理解,即形成递归:
在这里插入图片描述

//Tree.c
//二叉树结点的个数
int BinaryTreeSize(BTNode* root)
{
	if (root == NULL)
	{
		return 0;
	}
	//走到这里说明结点不为空
	return 1 + BinaryTreeSize(root->left) + BinaryTreeSize(root->right);
}

在这里插入图片描述
附上函数栈帧递归图:

在这里插入图片描述

分析“二叉树结点个数”算法时,可能会出现的问题:

假设我们都不知道正确的算法,就当一道全新的算法题分析。

  1. 问题一:

按照根左右的遍历方式递归二叉树,计算二叉树中的结点个数,算法一:

在这里插入图片描述

在画图的过程中发现,在每一个函数栈帧中,size都会被初始化为0,最后结果size毫无疑问是1,没办法达到每遍历到一个非空结点就累加的效果:

在这里插入图片描述

  1. 问题二:

定义局部变量不行,那么定义全局变量呢?画图分析看着是可以的,算法二:
在这里插入图片描述
在这里插入图片描述

因为定义的是全局变量,所以连续调用的话,一定会出现累加的问题:

在这里插入图片描述

  1. 问题三:

若在测试中每一次调用前将size置为0,并将size作为参数传递,那为什么两次调用size还是为0呢?

在这里插入图片描述

这个问题是最常见的,size传递的是数值,形参只是实参值的拷贝,所以形参的改变不会影响实参。

改进:

在这里插入图片描述

是可以实现出来,但是不符合要求的函数的返回值类型是int,还是不要改变函数的定义来计算。

3.二叉树叶子结点的个数

根结点肯定不是叶子结点,所以二叉树叶子结点的个数 = 左子树叶子结点的个数 + 右子树叶子结点的个数

怎么样才能确定结点是叶子结点呢?——该结点的左右孩子都为空

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

在这里插入图片描述

4.二叉树第k层结点的个数

二叉树第k层结点的个数 = 左子树第k层结点的个数 + 右子树第k层结点的个数

怎么样确定是第k层呢?假设k = 3,每递归完一层k就减1,直到k = 1时,说明递归到第k层了

//Tree.c
//二叉树第k层结点的个数
int BinaryTreeLevelKSize(BTNode* root, int k)
{
	if (root == NULL)
	{
		return 0;
	}
	//走到这里说明结点不为空
	if (k == 1)
	{
		return 1;
	}
	//走到这里说明结点不为空且不是第k层
	//继续递归,k到下一层
	return BinaryTreeLevelKSize(root->left, k - 1) + BinaryTreeLevelKSize(root->right, k - 1);
}

在这里插入图片描述

5.二叉树的高度/深度

二叉树的高度/深度 = 根结点所在的层数1 + 左右子树中最大的层数

//Tree.c
//二叉树的高度/深度
int BinaryTreeDepth(BTNode* root)
{
	if (root == NULL)
	{
		return 0;
	}
	//走到这里说明结点不为空
	//继续递归,分别计算左右子树中的层数
	int leftDep = BinaryTreeDepth(root->left);
	int rightDep = BinaryTreeDepth(root->right);
	return 1 + (leftDep > rightDep ? leftDep : rightDep);
}

在这里插入图片描述

6.查找值为x的结点

查找就要涉及到遍历——先序遍历
若根结点不为空,根结点也不是要查找的结点,则先去左子树找,找到了返回;没找到,再去右子树找,找到了返回。都没找到,返回NULL。

//Tree.c
//查找值为x的结点
BTNode* BinaryTreeFind(BTNode* root, BTDataType x)
{
	if (root == NULL)
	{
		return NULL;
	}
	//走到这里说明结点不为空
	//很可能这个结点就是值为x的结点
	if (root->data == x)
	{
		return root;
	}
	//走到这里说明结点不为空且不是值为x的结点
	//继续递归
	//先遍历左子树,再遍历右子树。若左子树遍历到了就不用遍历右子树了
	BTNode* leftFind = BinaryTreeFind(root->left, x);
	if (leftFind)
	{
		return leftFind;
	}
	BTNode* rightFind = BinaryTreeFind(root->right, x);
	if (rightFind)
	{
		return rightFind;
	}
	return NULL;
}

在这里插入图片描述
在这里插入图片描述

7.销毁

指向根结点的指针销毁后指向NULL,指针变量里的内容改变了,所以要传址调用。

可以为了保持接口的一致性,传递一级指针,只不过最后要手动的将根节点指针置为空。

销毁也涉及遍历,先销毁左子树,再销毁右子树,最后销毁根结点,即选择后序遍历。若选择先序遍历,就会把根节点销毁,这样就找不到它的左右结点;若选择中序遍历,会找不到右子树。

//Tree.c
//销毁
void BinaryTreeDestroy(BTNode** root)
{
	if ((*root) == NULL)
	{
		return;
	}
	//走到这说明结点不为空
	//继续递归
	BinaryTreeDestroy(&((*root)->left));
	BinaryTreeDestroy(&((*root)->right));
	free(*root);
	*root = NULL;
}

在这里插入图片描述

8.判断是否是完全二叉树

完全二叉树:最后一层结点个数不一定达到最大,其他层的结点个数达到最大,结点从上到下从左到右依次排列。

思路:与层序遍历思路相近,借助数据结构——队列,根结点入队,循环判断队列是否为空,不为空,取队头,出队,将队头结点的左右孩子入队。对于非完全二叉树,取到空结点时跳出循环;对于完全二叉树,当队列为空时跳出循环。队列不为空,继续取队头、出队,队头存在非空结点——非完全二叉树。
在这里插入图片描述

//Tree.c
//判断是否是完全二叉树
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, root->left);
		QueuePush(&q, root->right);
	}
	//队列不为空,继续取队头、出队
	//队头存在非空结点——非完全二叉树
	while (!QueueEmpty(&q))
	{
		BTNode* top = QueueFront(&q);
		QueuePop(&q);
		if (top != NULL)
		{
			QueueDestroy(&q);
			return false;
		}
	}
	QueueDestroy(&q);
	return true;
}

二叉树选择题

二叉树性质:对任何一棵二叉树,度为0的结点个数为n0,度为2的结点个数为n2,则n0 = n2 + 1

在这里插入图片描述
证明上述性质:

一个度为2的结点,在结点下方与之相连的有2条边;一个度为1的结点有1条边;一个度为0的结点有0条边。假设⼀个二叉树有 a 个度为2的结点, b 个度为1的结点, c 个叶结点,则这个二叉树的边数是2a+b。另一方面,由于共有 a+b+c 个节点,所以边数等于 a+b+c-1
结合上面两个公式:2a+b = a+b+c-1 ,即:c = a + 1

根据二叉树的性质完成下列选择题:

  1. 某⼆叉树共有 399 个结点,其中有 199 个度为 2 的结点,则该⼆叉树中的叶⼦结点数为( )
    A 不存在这样的⼆叉树
    B 200
    C 198
    D 199
  2. 在具有 2n 个结点的完全⼆叉树中,叶⼦结点个数为( )
    A n
    B n+1
    C n-1
    D n/2
  3. ⼀棵完全⼆叉树的结点数位为531个,那么这棵树的⾼度为( )
    A 11
    B 10
    C 8
    D 12
  4. ⼀个具有767个结点的完全⼆叉树,其叶⼦结点个数为()
    A 383
    B 384
    C 385
    D 386

解题过程:

  1. 根据二叉树的性质:n0 = n2 + 1,得出叶子结点个数为199 + 1 = 200,选B

在这里插入图片描述

在完全二叉树中,n1只有为1或为0两种可能(度为1的结点个数为0时,还有一种情况是满二叉树,满二叉树也是完全二叉树):

在这里插入图片描述

在这里插入图片描述
结点个数不可能有半个,所以叶子结点的个数只能为n个,选A。

满二叉树的结点个数与高度的关系:

在这里插入图片描述
本题中总结点的个数比9层满二叉树多了20个结点,同时比10层满二叉树的结点个数少,即这是一个10层的完全二叉树,选择B。
在这里插入图片描述

这题与第二题同理:

在这里插入图片描述
同理,结点个数不能为半个,所以叶子结点个数为384,选B

链式⼆叉树遍历选择题:

  1. 某完全⼆叉树按层次输出(同⼀层从左到右)的序列为 ABCDEFGH 。该完全⼆叉树的前序序列为()
    A ABDHECFG
    B ABCDEFGH
    C HDBEAFCG
    D HDEBFGCA
  2. ⼆叉树的先序遍历和中序遍历如下:先序遍历:EFHIGJK;中序遍历:HFIEJKG.则⼆叉树根结点为()
    A E
    B F
    C G
    D H
  3. 设⼀课⼆叉树的中序遍历序列:badce,后序遍历序列:bdeca,则⼆叉树前序遍历序列为____。
    A adbce
    B decab
    C debac
    D abcde
  4. 某⼆叉树的后序遍历序列与中序遍历序列相同,均为 ABCDEF ,则按层次输出(同⼀层从左到右)的序列为
    A FEDCBA
    B CBAFED
    C DEFCBA
    D ABCDEF

解题过程:

先把这一棵完全二叉树画出来,再写出它的前序序列——根左右,选A

在这里插入图片描述

因为知道先序遍历——根左右,所以能直接得出根结点是E,选A

根据中序遍历序列和后序遍历序列画出一棵二叉树:

在这里插入图片描述
得出前序遍历序列:abcde,选D

根据中序遍历序列和后序遍历序列画出一棵二叉树:
在这里插入图片描述
则按层次输出的序列为:FEDCBA,选A

三棵子树:
在这里插入图片描述
例如,这棵树有四个结点,有3条边:
在这里插入图片描述

15是左子树中的根节点,是这三个结点中最小的结点
在这里插入图片描述
跟排序没有关系,只能保证根是最小的:
在这里插入图片描述

在这里插入图片描述
求除取整:在这里插入图片描述
在这里插入图片描述
顺序表插入数据实现方法尾插时间复杂度最好,尾插后,找到父节点,与之比较,没有必要与兄弟节点比较,若交换了,还要接着与父节点比较:
在这里插入图片描述

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值