二叉树(C 语言)

目录

  • 一、树
    • 1. 树的概念
    • 2. 树的表示方法
    • 3. 树在实际当中的应用
  • 二、二叉树
    • 1. 二叉树的定义
    • 2. 现实中的二叉树
    • 3. 特殊的二叉树
    • 4. 二叉树的性质
    • 5. 二叉树的存储结构
  • 三、堆 —— 完全二叉树的顺序存储
    • 1. 堆的概念
    • 2. 堆的性质
    • 3. 堆的设计思路
    • 4. 堆的实现代码
  • 四、堆排序
    • 1. 堆排序的设计思路
    • 2. 堆排序的实现代码
  • 五、二叉树的链式存储
    • 1. 二叉树的结构声明
    • 2. 二叉树的四种遍历方式
    • 3. 四种遍历方式的实现
    • 4. 二叉树的销毁

一、树

在现实生活中,我们对树并不陌生,因为树随处可见。而数据结构中也拥有属于它的树。

1. 树的概念

可以把数据结构中的树想象成现实生活中的树倒过来的模样:
在这里插入图片描述
那么就可以把根当作一个节点,然后长出它的枝干,枝干再长出它的枝干,如此往复,直到长出叶子为止。那么把枝干和叶子也当作一个节点,一颗数据结构中的树就成形了:
在这里插入图片描述
在上图中,节点 A 是这颗树的根,下一层就是 A 的枝干。如此往复,节点 P 和 Q 就是叶子。

下面就通过这张图,来了解数据结构中数的相关概念:

  1. 节点的度: 该节点含有的子树的个数,也就是孩子的个数。如:A 节点的度是 6,包含 B、C、D、E、F 和 G 六颗子树。
  2. 叶节点或者终端节点: 度为 0 的节点,也就是没有子树(孩子)的节点,如:节点 P 和 Q 等。
  3. 非终端节点或分支节点: 度不为 0 的节点,如:D、E、F 和 G。
  4. 双亲节点或父节点: 该节点向上延伸的第一个节点就是该节点的双亲节点。除了根节点以外,其他节点都有双亲节点,且只有一个,因为在树结构中,一个节点只能和它的双亲节点或者孩子节点相连。如:A 是 B、C、D、E、F 和 G 节点的父节点,且它们只有 A 这一个父节点。
  5. 孩子节点或子节点: 当前结点向下延伸的第一层节点,如:B、C、D、E、F 和 G 节点都是 A 的孩子节点。
  6. 兄弟节点:具有相同父亲节点的节点互为兄弟节点,如:B、C、D、E、F 和 G 节点互为兄弟节点。
  7. 树的度: 一颗数中,最大的节点的度称为树的度。上图中树的度为:
  8. 节点的层次: 上图中根节点 A 为第 1 层,B、C、D、E、F 和 G 节点是第 2 层,以此类推。
  9. 树的高度或深度: 树中节点的最大层次,上图中树的深度为:4
  10. 堂兄弟节点:双亲互为兄弟的节点称为堂兄弟节点,如:H、I、J、K、L、M 和 N 这些节点互为堂兄弟节点。
  11. 祖先:从根节点到该节点路径上的所有节点。如:A 是所有节点的祖先。
  12. 森林:由 m (m > 0)棵互不相交的树的集合称为森林。

只要记住上面加粗标记的就行了,身下的在二叉树阶段用处不大。

2. 树的表示方法

下面介绍一种简单常用的孩子兄弟表示法

// 孩子兄弟表示法

// 类型声明
typedef int DataType;

// 树节点声明
typedef struct TreeNode
{
	DataType data;  // 当前节点的数据
	struct TreeNode* next_brother;  // 下一个兄弟节点
	struct TreeNode* first_child;  // 第一个孩子节点
}TreeNode;

可以把下一个兄弟节点和第一个孩子节点都想象成一个链表的开头,遇到 NULL 就结束。
在这里插入图片描述
这个结构的思路大概就是上图这样。(下次还是偷课件的图算了,自己画图太累了)

下面给出遍历用孩子兄弟表示法表示的二叉树的代码:

// 遍历孩子兄弟表示法的树
void PrintTree(TreeNode* root)
{
	// 空树返回
	if (NULL == root)
		return;
	// 前序遍历
	// 打印当前节点
	printf("%d ", root->data);
	// 打印第一个孩子节点
	PrintTree("d ", root->first_child);
	// 打印下一个兄弟节点
	PrintTree("d ", root->next_brother);
}

如果用此方法打印上图数据,那么结果如下:
A B D E G F C

3. 树在实际当中的应用

其实电脑上文件系统就是使用树来完成的:
在这里插入图片描述
还是偷课件的图爽!

二、二叉树

二叉树二叉树,顾名思义,每个枝干都应该有两个分支嘛。

1. 二叉树的定义

二叉树是一颗特殊的树,这棵树的度为 2。也就是说,该树的每个节点(包括根节点)最多只能有两颗子树。
在这里插入图片描述
从上述图片中,我们可以得出四个结论:

  1. 二叉树的度为 2,每个节点最多只能有两个孩子
  2. 二叉树的每个节点都有左子树和右子树,如:A 有左子树 B,右子树 C,那么相应的 B 和 C 也有自己的左右子树。D 的左右子树都为空树。
  3. 二叉树的子树有左右之分,且次序不能颠倒,所以二叉树是有序树。
  4. 二叉树只有一下四种状态:空树、只有左树、只有右树和左右树都有。

2. 现实中的二叉树

下面是现实中两颗经典的二叉树:
在这里插入图片描述
别问图片哪里来的,问就是从课件偷的。

3. 特殊的二叉树

在二叉树中又存在两种特殊的二叉树:满二叉树和完全二叉树。

  1. 满二叉树: 每个节点都拥有两个孩子,也可以说该树的每层都是满的。假设该树的高度为 h,那么它的节点个数为 2h-1。(等比数列,自己算一下哈)

  2. 完全二叉树: 除了最后一层外,其他层都是满的,而且最后一层必须是连续的。假设该树的高度为 h,那么它的节点的个数范围是 [2h-1, 2h-1]。(最少是前面 h-1 层加 1,最多是 h 层全满)

如下图所示:
在这里插入图片描述

4. 二叉树的性质

  1. 若规定根节点的层数为 1,那么该一颗非空二叉树第 n 层上最多有 2n-1 个节点。
  2. 若规定根节点的层数为 1,那么一颗高为 h 的树的节点范围在 [h, 2h-1]。
  3. 对于任意一颗二叉树而言,如果它的度为 0 的叶子节点的个数为 n0,那么度为 2 的分支节点的个数为 n2,且 n0 = n2 + 1
  4. 若规定根节点的层数为 1,那么具有 n 个节点的满二叉树的深度 h = log2(n+1)
  5. 对于具有 n 个节点的完全二叉树,如果从根节点开始从上到下从左到右开始从 0 编号,那么对于序列号为 i 的节点便有如下结论:
    (1)若 i > 0,它的双亲节点的序列号为 (i-1)/2,i = 0 为根节点,根节点没有双亲节点
    (2)若 2*i+1 < n,那么序列号为 i 的节点的左孩子的序列号为 2*i +1 ,同理它的右孩子就在左孩子的右边第一位。如果 2*i+1 >= n,那么当前结点为叶子节点,没有孩子。

5. 二叉树的存储结构

二叉树有两种存储方式:顺序存储和链式存储。

1. 顺序存储: 顺序存储就是开辟一个动态数组,然后把二叉树从根节点开始,从上到下从左往右放入数组中。而顺序存储一般只用来存储完全二叉树,因为存储非完全二叉树会有空间的浪费。
在这里插入图片描述
1.1 顺序存储的优点: 可以通过公式计算当前下标为 i 的节点的双亲节点和孩子节点的下标并且可以快速访问。
双亲节点:(i-1)/2
孩子节点:2*i+1

2.2 顺序存储的缺点: 通常用来存储完全二叉树,种类单一。且使用数组为底层实现,容量不够的时候需要增容,增容后可能会有一定的空间浪费。

2. 链式存储: 可以参考链表的实现方式,因为每个节点最多只有两个孩子节点。而且当前树由根节点和它的左子树和右子树构成,而根节点的左孩子可以当作左子树的根节点,根节点的右孩子可以当作右子树的根节点,如此往复,直到空树。

那么该数的节点就应该包含三个成员:当前结点的数据 data,左孩子的位置 left,右孩子的位置(也可以是左右树的位置)。如下代码:

// 二叉树节点声明
typedef struct BinaryTreeNode
{
	DataType data;  // 数据
	struct BinaryTreeNode* left;  // 左孩子的位置
	struct BinaryTreeNode* right;  // 右孩子的位置
}BTNode;

该链式存储方式被称为二叉链,如果再添加一个双亲节点的指针就是三叉链。

// 二叉树三叉链节点声明
typedef struct BinaryTreeNode
{
	DataType data;  // 数据
	struct BinaryTreeNode* parent;  // 双亲的位置
	struct BinaryTreeNode* left;  // 左孩子的位置
	struct BinaryTreeNode* right  // 右孩子的位置
};

2.1 链式存储的优点: 可以存储所有种类的二叉树,左右子树分开,层次分明。不需要扩容,且可以前序、中序、后序和层序遍历。

2.2 链式存储的缺点: 不能从当前节点找到其双亲节点,访问节点的速度没有顺序存储快。

三、堆 —— 完全二叉树的顺序存储

1. 堆的概念

这里的堆是一种二叉树,是一种数据结构,不是内存里的堆。如果一个元素的集合按照二叉树顺序存储的方式存储在一个一维数组中,并且满足所有双亲节点大于(小于)孩子节点,那么该堆被称为大堆(小堆)。将根节点最大的堆叫做最大堆或者大根堆,根节点最小的堆叫做最小堆或者小根堆。

2. 堆的性质

(1)堆中节点的值总是大于等于或者小于等于其双亲节点。
(2)堆总是一颗完全二叉树
在这里插入图片描述

3. 堆的设计思路

1. 堆结构设计思路:

首先,堆的底层是使用数组来实现的,那么堆的结构中应该包含指向数据的指针 pdata,其次还需要扩容,那么还需要当前数据个数 size 和当前容量 capacity,共计三个成员。

// 堆

// 类型声明
typedef int DataType;

// 常量声明
#define CAPACITY_INIT 8

// 堆结构声明
typedef struct Heap
{
	DataType* pdata;  // 数据
	size_t size;  // 当前数据个数
	size_t capacity;  // 当前容量
}Heap;

2. 堆方法设计思路:
下面是堆要实现的功能:

// 方法

// 构建堆
void HeapCreate(Heap* hp);

// 销毁堆
void HeapDestory(Heap* hp);

// 堆的插入
void HeapPush(Heap* hp, DataType data);

// 堆的删除
void HeapPop(Heap* hp);

// 返回堆顶的元素
DataType HeapTop(const Heap* hp);

// 返回堆中的元素个数
size_t HeapSize(const Heap* hp);

// 空堆判断
bool HeapEmpty(const Heap* hp);

上面的功能大部分都非常简单,只有插入和删除这两个操作稍微复杂一些,因为要保证在插入和删除之后,还要保持原来堆的属性,也就是大堆或者小堆。

堆的插入:
堆的插入是在堆的末尾进行插入的,假设现在有一个大堆,需要往其中插入一个元素:
在这里插入图片描述
可以看到,在插入元素 80 之前,该堆是一个大堆,如何保证在插入元素 80 之后仍是一个大堆?

可以通过从该节点向上调整,使得整个堆继续保持大堆。也就是该节点和它的双亲节点进行比较,如果比双亲节点大,那么就交换,交换之后再继续和它的双亲节点进行比较,直到比双亲节点小或者达到根节点就停止,调整完成之后仍是一个大堆。

在这里插入图片描述

堆的删除:
堆的删除是在堆顶进行删除,那么如何在删除之后保持原来的大堆呢?

首先,把堆顶的元素和堆底的元素进行交换,然后把当前元素个数 size 减 1:
在这里插入图片描述
此时,除了根节点以外,它的左子树和右子树均是一个大堆,那么让堆顶的数据向下调整,向下调整如果是大堆需要和孩子节点中较大的数进行比较,如果是小堆需要和孩子节点中较小的节点进行比较。
在这里插入图片描述

4. 堆的实现代码

(1)头文件:Heap.h

#pragma once

// 头文件
#include <stdio.h>
#include <stdbool.h>

// 堆

// 类型声明
typedef int DataType;

// 常量声明
#define CAPACITY_INIT 8

// 堆结构声明
typedef struct Heap
{
	DataType* pdata;  // 数据
	size_t size;  // 当前数据个数
	size_t capacity;  // 当前容量
}Heap;

// 方法

// 构建堆
void HeapInit(Heap* hp);

// 销毁堆
void HeapDestory(Heap* hp);

// 堆的插入
void HeapPush(Heap* hp, DataType data);

// 堆的删除
void HeapPop(Heap* hp);

// 返回堆顶的元素
DataType HeapTop(const Heap* hp);

// 返回堆中的元素个数
size_t HeapSize(const Heap* hp);

// 空堆判断
bool HeapEmpty(const Heap* hp);

(2)方法实现文件:Heap.c

// 头文件
#include "Heap.h"
#include <assert.h>
#include <stdlib.h>

// 方法

// 构建堆
void HeapInit(Heap* hp)
{
	assert(hp);
	// 申请数据空间
	hp->pdata = (DataType*)malloc(sizeof(DataType)*CAPACITY_INIT);
	if (NULL == hp->pdata)
	{
		perror("HeapCreate::malloc: ");
		return;
	}
	// 成员初始化
	hp->capacity = CAPACITY_INIT;
	hp->size = 0;
}

// 销毁堆
void HeapDestory(Heap* hp)
{
	assert(hp);
	// 释放数据空间
	free(hp->pdata);
	hp->pdata = NULL;
	// 成员置零
	hp->size = hp->capacity = 0;
}

// 交换
void Swap(DataType* d1, DataType* d2)
{
	DataType tmp = *d1;
	*d1 = *d2;
	*d2 = tmp;
}

// 向上调整(大堆)
void AdjustUp(DataType* arr, size_t child)
{
	assert(arr);
	// 找双亲节点下标
	size_t parent = (child - 1) / 2;
	// 开始调整
	while (child > 0)  // child 为 0 时是根节点,没有双亲节点
	{
		if (arr[child] > arr[parent])
		{
			// 交换
			Swap(&arr[child], &arr[parent]);
			// 下一组
			child = parent;
			parent = (parent - 1) / 2;
		}
		else
		{
			break;
		}
	}
}

// 堆的插入
void HeapPush(Heap* hp, DataType data)
{
	assert(hp);
	// 增容判断
	if (hp->size == hp->capacity)
	{
		DataType* tmp = (DataType*)realloc(hp->pdata, sizeof(DataType) * hp->capacity * 2);
		if (NULL == tmp)
		{
			perror("HeapPush::realloc: ");
			return;
		}
		// 成功,更新成员
		hp->pdata = tmp;
		hp->capacity *= 2;
	}
	// 插入
	hp->pdata[hp->size++] = data;
	// 向上调整
	AdjustUp(hp->pdata, hp->size - 1);
}

// 向下调整
void AdjustDown(DataType* arr, size_t n, size_t parent)
{
	assert(arr);
	// 找左孩子节点下标
	size_t child = parent * 2 + 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 = child * 2 + 1;
		}
		else
		{
			break;
		}
	}
}

// 堆的删除
void HeapPop(Heap* hp)
{
	assert(hp);
	// 空堆判断
	if (0 == hp->size)
	{
		printf("Emtpty Heap! Cant't Pop!\n");
		return;
	}
	// 交换堆顶和堆底
	Swap(&hp->pdata[0], &hp->pdata[hp->size - 1]);
	// 元素个数 -1
	--hp->size;
	// 堆顶向下调整
	AdjustDown(hp->pdata, hp->size, 0);
}

// 返回堆顶的元素
DataType HeapTop(const Heap* hp)
{
	assert(hp);
	// 空堆判断
	if (0 == hp->size)
	{
		printf("Empty Heap!\n");
		exit(-1);
	}
	// 返回
	return hp->pdata[0];
}

// 返回堆中的元素个数
size_t HeapSize(const Heap* hp)
{
	assert(hp);
	// 返回
	return hp->size;
}

// 空堆判断
bool HeapEmpty(const Heap* hp)
{
	assert(hp);
	// 返回
	return (0 == hp->size);
}

(3)测试文件:test.c

// 头文件
#include "Heap.h"
#include <stdlib.h>
#include <time.h>

int main()
{
	// 设置随机数种子
	srand((unsigned)time(0));

	// 创建堆
	Heap hp;
	// 初始化堆
	HeapInit(&hp);
	// 堆的插入
	// 随机生成值为 11 - 99 的 10 个数
	int i;
	for (i = 0; i < 10; ++i)
	{
		int tmp = rand() % 89 + 11;
		printf("%d ", tmp);
		HeapPush(&hp, tmp);
	}
	printf("\n");
	// 堆的删除
	while (!HeapEmpty(&hp))
	{
		printf("%d ", HeapTop(&hp));
		HeapPop(&hp);
	}
	// 销毁堆
	HeapDestory(&hp);

	return 0;
}

(4)测试结果:
通过观察可以发现,如果是大堆,每次删除的都是当前堆中最大的数,如果是小堆每次删除的都是当前堆中最小的数。所以,把删除的数据打印出来,如果有序,则证明代码基本上没有问题。
在这里插入图片描述

四、堆排序

从上面的堆实现的测试结果中发现,堆好像可以给数据进行排序。因为如果是大堆的话,每次删除都会把最大的数据放在最后面;如果是小堆的话,每次删除都会把最小的数据放在最后面。

所以,排升序需要使用大堆,排降序需要使用小堆。

1. 堆排序的设计思路

首先,排序是传给你一个数组,然后使用对应的方法进行排序是数组中的数据变得有序。那么数组中的原数据是随机的,如果要进行堆的删除,首先要确保原数组是一个堆,那么第一步就是建堆。

(1)建堆
建堆有两种方法,一种是向上调整建堆,另一种是向下调整建堆。

向上调整建堆: 把数组的第一个元素看成一个堆,然后把第二个元素向上调整,接着把前面两个元素看成一个堆,把第三个元素向上调整,直到调整完数组的最后一个元素。调整完成后,该数组就是一个大(小)堆。

向下调整: 从数组的第一个非叶子节点开始向下调整,因为第一个非叶子节点的左右子树要么只有一个节点,要么就是空树,所以它的左右子树都是大(小)堆,而根节点向下调整之后该根节点表示的这颗树就是大(小)堆了。然后继续调整前一个节点,直到调整完所有节点。调整完成之后,该数组就是一个大(小)堆。

(2)开始堆的删除,直到删除完数组的第二个元素位置
假设数组元素个数为 n,那么大的 n-1 个数已经有序排在后面了,当前数组首元素就是最小的数了。

由于向下调整建堆的速度比向上调整建堆块,所以一般使用向下调整建堆。

2. 堆排序的实现代码

(1)头文件:HeapSort.h

#pragma once

// 函数声明

// 堆排序
void HeapSort(int* arr, int sz);

// 打印数组
void PrintArr(int* arr, int sz);

// 交换
void Swap(int* a, int* b);

(2)方法实现文件:HeapSort.c

// 头文件
#include "HeapSort.h"
#include <stdio.h>

// 函数定义

// 向下调整
void AdjustDown(int* arr, size_t sz, size_t parent)
{
	// 找到左孩子的下标
	size_t child = parent * 2 + 1;
	// 开始调整
	while (child < sz)
	{
		// 找到大孩子
		if ((child + 1) < sz && arr[child] < arr[child + 1])
			++child;
		// 比较
		if (arr[child] > arr[parent])
		{
			// 交换
			Swap(&arr[child], &arr[parent]);
			// 下一组
			parent = child;
			child = child * 2 + 1;
		}
		else
		{
			break;
		}
	}
}

// 堆排序
void HeapSort(int* arr, int sz)
{
	// 向下调整建堆
	int i = 0;
	for (i = (sz - 2) / 2; i >= 0; --i)
	{
		// 向下调整
		AdjustDown(arr, sz, i);
	}
	// 堆的删除,直到只剩最后一个元素
	int n = sz - 1;
	while (n--)
	{
		// 交换堆顶和堆底
		Swap(&arr[0], &arr[n + 1]);
		// 堆顶向下调整
		AdjustDown(arr, n + 1, 0);
	}
}

// 打印数组
void PrintArr(int* arr, int sz)
{
	int i = 0;
	for (i = 0; i < sz; ++i)
		printf("%d ", arr[i]);
	printf("\n");
}

// 交换
void Swap(int* a, int* b)
{
	int tmp = *a;
	*a = *b;
	*b = tmp;
}

(3)测试文件:test.c

// 头文件
#include "HeapSort.h"
#include <stdlib.h>
#include <time.h>

// 堆排序测试
int main()
{
	// 设置随机数种子
	srand((unsigned)time(0));
	// 随机生成 20 个 1 - 99 之间的数字
	int arr[20];
	size_t sz = sizeof(arr) / sizeof(arr[0]);
	int i = 0;
	for (i = 0; i < 20; ++i)
		arr[i] = rand() % 99 + 1;
	// 堆排序
	HeapSort(arr, sz);
	// 打印验证
	PrintArr(arr, sz);

	return 0;
}

(4)测试结果:
在这里插入图片描述

五、二叉树的链式存储

1. 二叉树的结构声明

// 二叉树

// 类型声明
typedef int DataType;

// 二叉树结构声明
typedef struct TreeNode
{
	DataType data;  // 数据
	struct TreeNode* left;  // 左子树
	struct TreeNode* right;  // 右子树
}TreeNode;

(1)若根节点为空,则为空树
(2)根节点不为空,那么该二叉树就分为根节点,左子树和右子树。左子树和右子树可以继续按照该方法往下分。

总结得出:二叉树的定义方式和递归很相似。递归的本质是将一个复杂的大问题逐步分解为规模更小、与原问题相似的子问题来求解,直到子问题简单到可以直接得出答案。而二叉树把原树分为根节点左子树和右子树,其子树继续往下分,直到空树为止。

所以,对二叉树的操作应该都和递归有关。

2. 二叉树的四种遍历方式

二叉树有前序、中序、后序和层序四种遍历方式。其中,前序、中序和后序遍历的差别是访问根节点的顺序不同,而层序遍历是从根节点开始从上到下,从左往右依次遍历每个节点。

前序遍历: 先访问根节点,然后访问左子树,最后访问右子树
中序遍历: 先访问左子树,然后访问根节点,最后访问右子树
后序遍历: 先访问左子树,然后访问右子树,最后访问根节点

下面对前序遍历给出递归顺序图,中序和后序读者根据前序依葫芦画瓢。

3. 四种遍历方式的实现

1. 前序遍历

// 前序遍历
void PreOrder(const TreeNode* root)
{
	// 空树
	if (NULL == root)
		return;
	// 打印根节点
	printf("%d", root->data);
	// 左子树
	PreOrder(root->left);
	// 右子树
	PreOrder(root->right);
}

递归过程图:
(1)自己画的
在这里插入图片描述
(2)课件偷的
在这里插入图片描述

2. 中序遍历:

// 中序遍历
void InOrder(const TreeNode* root)
{
	// 空树
	if (NULL == root)
		return;
	// 左子树
	InOrder(root->left);
	// 打印根节点
	printf("%d ", root->data);
	// 右子树
	InOrder(root->right);
}

3. 后序遍历

// 后序遍历
void PostOrder(const TreeNode* root)
{
	// 空树
	if (NULL == root)
		return;
	// 左子树
	PostOrder(root->left);
	// 右子树
	PostOrder(root->right);
	// 打印根节点
	printf("%d ", root->data);
}

4. 层序遍历
层序遍历需要借助队列实现,先把根节点放进去,然后每次出一个节点就把它的孩子入队,直到队列为空,这样就能实现层序遍历。

出入过程图:
在这里插入图片描述
代码如下:

// 层序遍历
void LevelOrder(const TreeNode* root)
{
	assert(root);

	// 创建队列
	Queue q;
	// 初始化队列
	QueueInit(&q);
	// 根节点入队
	QueuePush(&q, root);
	// 开始层序遍历
	while (!QueueEmpty(&q))
	{
		// 出队
		TreeNode* cur = QueueFront(&q);
		QueuePop(&q);
		// 打印
		printf("%d ", cur->data);
		// 入其孩子节点
		if (cur->left)
			QueuePush(&q, cur->left);
		if (cur->right)
			QueuePush(&q, cur->right);
	}
}

4. 二叉树的销毁

二叉树的销毁只能选择后序遍历的方式,因为根节点要留在最后删除。

// 销毁二叉树
void TreeDestory(TreeNode* root)
{
	// 空树
	if (NULL == root)
		return;
	// 释放左子树
	TreeDestory(root->left);
	// 释放右子树
	TreeDestory(root->right);
	// 释放根节点
	free(root);
}	

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:/a/913977.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

游戏引擎学习第五天

这节貌似没讲什么 视频参考:https://www.bilibili.com/video/BV1Gmm2Y5EwE/ uint8 *A somewhere in memory; uint8 *B somewhere in memory;//BEFORE WE GOT TO HERE int Y *B; // whatever was actually there before the 5 *A 5; int X *B; // 5 //Obviously! Y and …

大路灯护眼灯十大品牌哪个牌子好?儿童大路灯护眼灯品牌排行榜

大路灯护眼灯十大品牌哪个牌子好&#xff1f;长时间在不良光线下用眼很容易引起视觉疲劳&#xff0c;最终影响视力健康&#xff0c;这个时候大路灯护眼灯以良好的性能成为了很不错的照明产品。不过如今行业热度很高&#xff0c;网红跨界品牌大路灯护眼灯出于成本压缩&#xff0…

1.2 图像处理基本操作

在本实战中&#xff0c;我们将学习如何使用OpenCV进行基本的图像处理操作。首先&#xff0c;我们将通过cv2.imread()函数读取图像&#xff0c;并使用cv2.imshow()在窗口中显示它。接着&#xff0c;我们将探索如何通过cv2.imwrite()保存图像&#xff0c;并设置不同的参数以控制图…

K8S如何基于Istio实现全链路HTTPS

K8S如何基于Istio实现全链路HTTPS Istio 简介Istio 是什么?为什么选择 Istio?Istio 的核心概念Service Mesh(服务网格)Data Plane(数据平面)Sidecar Mode(边车模式)Ambient Mode(环境模式)Control Plane(控制平面)Istio 的架构与组件Envoy ProxyIstiod其他组件Istio 的流量管…

手动搭建 Ghost 博客

操作场景 Ghost 是使用 Node.js 语言编写的开源博客平台&#xff0c;您可使用 Ghost 快速搭建博客&#xff0c;简化在线出版过程。本文档介绍如何在腾讯云云服务器&#xff08;CVM&#xff09;上手动搭建 Ghost 个人网站。 进行 Ghost 网站搭建&#xff0c;您需要熟悉 Linux …

MySQL之索引(3)(索引基本语法、SQL执行计划、常见索引失效原因与解决方法)

目录 一、索引基本语法。 &#xff08;1&#xff09;创建索引。 &#xff08;2&#xff09;查看索引。 &#xff08;3&#xff09;删除索引。 &#xff08;4&#xff09;给多列添加组合索引。 1、何时添加索引&#xff1f;&#xff1f; 2、组合索引。 二、SQL执行计划。 &#…

前端中的 File 和 Blob两个对象到底有什么不同

JavaScript 在处理文件、二进制数据和数据转换时&#xff0c;提供了一系列的 API 和对象&#xff0c;比如 File、Blob、FileReader、ArrayBuffer、Base64、Object URL 和 DataURL。每个概念在不同场景中都有重要作用。下面的内容我们将会详细学习每个概念及其在实际应用中的用法…

一步一步从asp.net core mvc中访问asp.net core WebApi

"从asp.net core mvc中访问asp.net core WebApi"看到这个标题是不是觉得很绕口啊&#xff0c;但的确就是要讲一讲这样的访问。前面我们介绍了微信小程序访问asp.net core webapi(感兴趣的童鞋可以看看前面的博文有关WEBAPI的搭建)&#xff0c;这里我们重点不关心如何…

【Linux系列】VNC安装ssh后,ssh无法登录

&#x1f49d;&#x1f49d;&#x1f49d;欢迎来到我的博客&#xff0c;很高兴能够在这里和您见面&#xff01;希望您在这里可以感受到一份轻松愉快的氛围&#xff0c;不仅可以获得有趣的内容和知识&#xff0c;也可以畅所欲言、分享您的想法和见解。 推荐:kwan 的首页,持续学…

温度虽寒,其道犹变:OpenAI接口之温度参数设置为0,为何每次回复仍有不确定性?

问题描述 调用openai API&#xff0c;使用templature 0&#xff0c;每次返回的内容仍有一些不同 >>> client OpenAI( ... api_keyapi_key, ... base_urlapi_base) #第一次尝试 >>> response client.chat.completions.create(mo…

【软件测试】需求的概念和常见模型(瀑布、螺旋、增量、迭代)

1. 什么是需求 在企业中&#xff0c;经常会听到&#xff1a;用户需求和软件需求 用户需求&#xff1a;没用经过合理的评估&#xff0c;通常就是一句话&#xff08;开发一个五彩斑斓的黑&#xff09;软件需求&#xff1a;开发人员和测试人员执行工作的依据 1.2 软件需求 在工…

食品配送管理系统(源码+文档+部署+讲解)

食品配送管理系统是成品商业化项目&#xff0c;系统可基于源码二开。 系统概述 餐饮食品配送&#xff0c;包含配送人APP、下单APP、管理端等&#xff0c;实现订餐、配餐&#xff0c;用于食品店、中央厨房等订餐、团餐业务 本项目名称为食品配送系统&#xff0c;是针对食品配…

./bin/mindieservice_daemon启动成功

接MindIE大模型测试及报错Fatal Python error: PyThreadState_Get: the function must be called with the GIL held,-CSDN博客经过调整如下红色部分参数&#xff0c;昇腾310P3跑起来了7b模型&#xff1a; rootdev-8242526b-01f2-4a54-b89d-f6d9c57c692d-qjhpf:/home/apulis-de…

我谈维纳(Wiener)复原滤波器

Rafael Gonzalez的《数字图像处理》中&#xff0c;图像复原这章内容几乎全错。上篇谈了图像去噪&#xff0c;这篇谈图像复原。 图像复原也称为盲解卷积&#xff0c;不处理点扩散函数&#xff08;光学传递函数&#xff09;的都不是图像复原。几何校正不属于图像复原&#xff0c…

精选 Top10 开源调度工具,解锁高效工作负裁自动化

在大数据和现代 IT 环境中&#xff0c;任务调度与工作负载自动化&#xff08;WLA&#xff09;工具是优化资源利用、提升生产效率的核心驱动力。随着企业对数据分析、实时处理和多地域任务调度需求的增加&#xff0c;这些工具成为关键技术。 本文将介绍当前技术发展背景下的Top …

高效视觉方案:AR1335与i.MX8MP的完美结合

方案采用NXP i.MX8MP处理器和onsemi AR1335图像传感器&#xff0c;i.MX8MP集成四核Cortex-A53、NPU及双ISP技术。AR1335是一颗分辨率为13M的CMOS传感器。它使用了先进的BSI技术&#xff0c;提供了超高的分辨率和出色的低光性能&#xff0c;非常适合于需要高质量图像的应用。此外…

Ubuntu+ROS 机械臂拾取和放置

官方链接&#xff1a;https://github.com/skumra/baxter-pnp 1.下载并安装 SDK 依赖项 sudo apt-get install python-wstool python-rosdep 2.创建新的 catkin 工作区 mkdir -p ~/ros_ws/src cd ~/ros_ws/src 3.使用 wstool 下载 rosinstall 文件并将其复制到 Catkin 工作区…

论文阅读《Structure-from-Motion Revisited》

摘要 增量式地运动结构恢复是从无序图像集合中进行三维重建的一个普遍策略。虽然增量式地重建系统在各个方面上都取得了巨大的进步&#xff0c;但鲁棒性、准确性、完整度和尺度仍然是构建真正通用管道的关键问题。我们提出了一种新的运动结构恢复技术&#xff0c;它改进了目前…

基于Spring Boot的船运物流管理系统的设计与实现,LW+源码+讲解

摘要 近年来&#xff0c;信息化管理行业的不断兴起&#xff0c;使得人们的日常生活越来越离不开计算机和互联网技术。首先&#xff0c;根据收集到的用户需求分析&#xff0c;对设计系统有一个初步的认识与了解&#xff0c;确定船运物流管理系统的总体功能模块。然后&#xff0…

威联通Docker Compose搭建NAS媒体库资源工具NAS Tools

文章目录 一、环境配置1-1 需要的配件1-2 环境安装及配置注意:获取PUID/PGID1-3 目录位置准备总结,这里我们要做5件事备注:Docker无法下载解决办法二、登录配件,进行配件连接和配置2-1 jackett设置2-2 qBittorrent设置!!!设置文件下载地址2-3 jellyfin设置2-4 NASTools设…