深入探索C语言中的二叉树:数据结构之旅

news2025/1/20 1:49:01

引言

在计算机科学领域,数据结构是基础中的基础。在众多数据结构中,二叉树因其在各种操作中的高效性而脱颖而出。二叉树是一种特殊的树形结构,每个节点最多有两个子节点:左子节点和右子节点。这种结构使得搜索、插入、删除等操作可以在对数时间复杂度内完成,这对于算法性能的提升至关重要。

核心内容

  • 二叉树的基本概念   

  • 我们首先需要理解什么是二叉树。在本文的代码中,二叉树由结构体BinaryTreeNode表示,每个节点包含数据以及指向左右子节点的指针。

  • 创建和销毁二叉树

    代码演示了如何使用BuyTreeNode函数为一个新节点分配内存,并通过CreateNode函数来构建一个简单的二叉树。同时,DestoryTree函数展示了如何安全地销毁二叉树,释放其占用的资源。

  • 二叉树的遍历

    遍历是二叉树中最重要的操作之一。我们介绍了三种基本的遍历方式:前序(PrevOrder)、中序(InOrder)和后序(PostOrder)遍历。这些遍历方法在二叉树的搜索和排序操作中发挥着关键作用。

  • 二叉树的其他属性

    除了遍历,我们还探讨了如何使用代码来确定二叉树的大小(TreeSize)、叶子节点的数量(TreeLeafSize)、树的高度(TreeHeight)以及特定层级的节点数(TreeLeveLK)。

  • 层序遍历的实现

    除了深度优先遍历,层序遍历(LevelOrder)也是一种重要的遍历方式。它按照节点所在的层级依次访问,这在某些特定的应用场景下非常有用,例如在构建搜索算法或执行宽度优先搜索时。

代码目录

#define _CRT_SECURE_NO_WARNINGS
#include <stdio.h>
#include <stdlib.h>
#include <assert.h>
#include <math.h>

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

TreeNode* BuyTreeNode(int x);
TreeNode* CreateNode();
void PrevOrder(TreeNode* root);//前序遍历  --深度优先遍历 dfs
void InOrder(TreeNode* root);
void PostOrder(TreeNode* root);
int TreeSize(TreeNode* root);
int TreeLeafSize(TreeNode* root);//叶子节点个数
int TreeHeight(TreeNode* root);//高度
int TreeLeveLK(TreeNode* root, int k);//第k层节点个数
TreeNode* TreeCreate(char* a, int* pi);//构建二叉树
void DestoryTree(TreeNode** root);//销毁二叉树
void LevelOrder(TreeNode* root);//层序遍历  --广度优先遍历bfs

1.二叉树的基本概念和结构 

在深入了解二叉树之前,我们必须首先理解它的基本构成。二叉树是一种非常重要的数据结构,广泛应用于编程和算法设计中。它是一个有序树,每个节点最多有两个子节点,通常被称为左子节点和右子节点。

结构体表示二叉树

在我的代码中,二叉树通过结构体BinaryTreeNode表示。这个结构体定义了树的基本单元——节点。每个节点包含三个部分:数据部分和两个指针。

typedef struct BinaryTreeNode {
    BTDataType data; // 节点存储的数据
    struct BinaryTreeNode* left; // 指向左子节点的指针
    struct BinaryTreeNode* right; // 指向右子节点的指针
} TreeNode;

这种结构体的设计允许二叉树以递归的方式定义:每个节点本身可以被视为一个树的根,具有自己的子树。二叉树的这种特性是其许多算法操作的基础,包括遍历、搜索和修改。

数据与节点关系

在这个结构体中,data字段存储了节点的值。这个值可以是任意类型,在我的示例中,它被定义为BTDataType(在这里是int类型)。每个节点的leftright指针分别指向它的左子节点和右子节点。如果某个节点不存在左子节点或右子节点,相应的指针将为NULL

这种结构使得操作和遍历二叉树变得可能,允许我们实现诸如插入、删除、查找等复杂操作,同时也为高效的算法实现提供了基础。

在后续部分,我们将探索如何使用这种结构体来创建和管理二叉树

 2.创建和销毁二叉树

在操作二叉树时,正确地管理内存是至关重要的。这涉及到两个基本操作:创建二叉树和销毁二叉树。在您的代码中,这两个过程通过BuyTreeNodeCreateNodeDestoryTree函数实现。

创建二叉树

  1. 分配节点内存: BuyTreeNode函数用于创建一个新的树节点。它接受一个数据值,分配足够的内存来存储一个新的TreeNode结构体,并将传入的数据值赋给新节点 

TreeNode* BuyTreeNode(int x) {
    TreeNode* node = (TreeNode*)malloc(sizeof(TreeNode));
    assert(node); // 确保内存分配成功
    node->data = x;
    node->left = NULL;
    node->right = NULL;
    return node;
}

构建二叉树(1):

TreeNode* TreeCreate(char* a, int* pi)
{
	if (a[*pi] == '#')
		return NULL;
	TreeNode* root = (TreeNode*)malloc(sizeof(TreeNode));
	if (root == NULL)
	{
		perror("malloc fail");
		exit(-1);
	}
	root->data = a[(*pi)++];
	root->left = TreeCreate(a, pi);
	root->right = TreeCreate(a, pi);
	return root;
}

过程步骤

  1. 检查终止条件: 函数首先检查当前位置的字符是否为'#'。在这个上下文中,'#'代表一个空位置,意味着在这里不需要创建节点。如果是'#',函数返回NULL,这表明当前没有节点被创建,相当于告诉递归调用的上一层,这里是一个空分支。

  2. 创建新节点: 如果当前位置的字符不是'#',函数会继续创建一个新的树节点。它分配内存空间给新节点,并检查内存分配是否成功。如果分配失败,函数会报错并退出程序。

  3. 设置节点数据: 新节点的数据设置为当前字符数组位置的值。然后,指针pi递增,以便下一次调用时读取下一个字符。

  4. 递归构建子树: 接下来,函数递归地调用自身两次:一次用于构建左子树,一次用于构建右子树。这两个递归调用分别处理字符数组中接下来的部分,因此逐渐构建出整个树的结构。

  5. 返回树的根节点: 一旦左右子树都被创建,函数返回当前创建的节点,这个节点现在是一个完整子树的根节点。

说明

通过这种方式,TreeCreate函数能够从一个序列化的表示(在这里是一个字符数组)中逐步重建出原始的二叉树结构。这种序列化表示通常包含特殊的字符(如'#')来标示空节点,从而允许树的形状在数组中得以完整表达。

这个函数的递归性质使得它能够处理任意复杂的树结构,只要输入的字符数组正确地表示了树的结构。这种方法在二叉树的序列化和反序列化中非常常见,是处理树结构数据的一种强大技巧。

 构建二叉树(2) CreateNode函数展示了如何将这些独立的节点组合成一个完整的二叉树结构。这个函数硬编码了节点的创建和连接方式,构造了一个特定的二叉树。

TreeNode* CreateNode() {
    TreeNode* node1 = BuyTreeNode(1);
    TreeNode* node2 = BuyTreeNode(2);
    ...
    node1->left = node2;
    node1->right = node4;
    ...
    return node1;
}

销毁二叉树

内存管理的另一方面是当二叉树不再需要时,正确地释放其占用的资源。这是通过DestoryTree函数实现的。

  1. 递归销毁: DestoryTree采用递归方式访问每个节点,并释放其占用的内存。递归是处理树结构的一种自然和强大的方法。

void DestoryTree(TreeNode** root) {
    if (*root == NULL)
        return;
    DestoryTree(&(*root)->left);
    DestoryTree(&(*root)->right);
    free(*root);
    *root = NULL;
}

 这种方法确保了所有节点都被适当地访问和释放,从而防止了内存泄漏——一种在长时间运行的程序中特别重要的考虑因素。

通过这些函数的实现,我们不仅构建了一个基本的二叉树,还学会了如何负责任地管理与之相关的内存。下一步,我们将探讨如何遍历二叉树,并了解它的其他重要属性

3.二叉树的其他属性

除了遍历,我们还探讨了如何使用代码来确定二叉树的大小(TreeSize)、叶子节点的数量(TreeLeafSize)、树的高度(TreeHeight)以及特定层级的节点数(TreeLeveLK)在理解了二叉树的基本结构和如何创建及销毁它之后,我们接下来会探索二叉树的几个其他重要属性:树的大小、叶子节点的数量、树的高度,以及特定层级的节点数。这些属性在二叉树的应用和分析中扮演着关键角色。

1. 二叉树的大小(TreeSize)

二叉树的大小是指树中节点的总数。这可以通过递归地计算每个节点的左右子树来确定。

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

在这个函数中,如果当前节点为NULL,表示子树不存在,因此返回0。否则,计算大小时,将当前节点(1)加上左子树和右子树的大小。 

2. 叶子节点的数量(TreeLeafSize)

叶子节点是指没有子节点的节点。计算二叉树中叶子节点的数量有助于理解树的分布和深度。

int TreeLeafSize(TreeNode* root) {
    if (root == NULL) {
        return 0;
    }
    if (root->left == NULL && root->right == NULL) {
        return 1;
    }
    return TreeLeafSize(root->left) + TreeLeafSize(root->right);
}

 当遇到叶子节点时(即左右子节点均为NULL),返回1。否则,递归地计算左右子树中的叶子节点数量。

3. 树的高度(TreeHeight)

树的高度是从根到最远叶子节点的最长路径上的节点数。这是衡量树平衡和深度的重要指标。

int TreeHeight(TreeNode* root) {
    if (root == NULL) {
        return 0;
    }
    return fmax(TreeHeight(root->left), TreeHeight(root->right)) + 1;
}

 在这个函数中,如果节点为NULL,表示到达了树的底部,返回0。否则,高度是左右子树高度的最大值加1(当前节点)。

4. 特定层级的节点数(TreeLeveLK)

计算特定层级的节点数有助于理解树在不同深度的分布。

int TreeLeveLK(TreeNode* root, int k) {
    if (root == NULL || k < 1) {
        return 0;
    }
    if (k == 1) {
        return 1;
    }
    return TreeLeveLK(root->left, k - 1) + TreeLeveLK(root->right, k - 1);
}

当到达目标层级(k == 1)时,返回1。如果不是目标层级,递归地计算左右子树在k-1层级的节点数。

通过这些函数,我们不仅能够构建和管理二叉树,还能深入了解树的结构和特性。这些属性对于优化算法、分析数据结构性能等方面都至关重要。接下来,我们将研究二叉树的遍历方法,这是理解和操作二叉树的关键一环。

1.二叉树的遍历

前序、中序以及后序遍历
学习二叉树结构,最简单的方式就是遍历。所谓 二叉树遍历 (Traversal) 是按照某种特定的规则,依次对二叉 树中的节点进行相应的操作,并且每个节点只操作一次 。访问结点所做的操作依赖于具体的应用问题。 遍历   是二叉树上最重要的运算之一,也是二叉树上进行其它运算的基础

按照规则,二叉树的遍历有: 前序 / 中序 / 后序的递归结构遍历
1. 前序遍历 (Preorder Traversal 亦称先序遍历 )—— 访问根结点的操作发生在遍历其左右子树之前。
2. 中序遍历 (Inorder Traversal)—— 访问根结点的操作发生在遍历其左右子树之中(间)。
3. 后序遍历 (Postorder Traversal)—— 访问根结点的操作发生在遍历其左右子树之后。

由于被访问的结点必是某子树的根, 所以 N(Node )、 L(Left subtree )和 R(Right subtree )又可解释为
根、根的左子树和根的右子树 NLR LNR LRN 分别又称为先根遍历、中根遍历和后根遍历。

 

void PrevOrder(TreeNode* root)
{
	if (root == NULL)
	{
		printf("N ");
		return;
	}
	printf("%d ", root->data);
	PrevOrder(root->left);
	PrevOrder(root->right);
}
void InOrder(TreeNode* root)
{
	if (root == NULL)
	{
		printf("N ");
		return;
	}
	InOrder(root->left);
	printf("%d ", root->data);
	InOrder(root->right);
}
void PostOrder(TreeNode* root)
{
	if (root == NULL)
	{
		printf("N ");
		return;
	}
	PostOrder(root->left);
	PostOrder(root->right);
	printf("%d ", root->data);
}

 2. 层序遍历

层序遍历 :除了先序遍历、中序遍历、后序遍历外,还可以对二叉树进行层序遍历。设二叉树的根节点所在 层数为1 ,层序遍历就是从所在二叉树的根节点出发,首先访问第一层的树根节点,然后从左到右访问第 2 层 上的节点,接着是第三层的节点,以此类推,自上而下,自左至右逐层访问树的结点的过程就是层序遍历。

 层序遍历  --广度优先遍历bfs 比如扫雷和基本搜索算法中就是以广度优先算法为基底

void LevelOrder(TreeNode* root)
{
	Queue q;  
	QueueInit(&q);
	if (root)
		QueuePush(&q, root);
	int LevelSize = 1;
	
	while (!QueueEmpty(&q))
	{
		while (LevelSize--)//这里稍作变形,很多面试常考   控制层序遍历每一层每一层的输出
		{

			TreeNode* front = QueueFront(&q);
			QueuePop(&q);
			printf("%d ", front->data);
			if (front->left)
				QueuePush(&q, front->left);
			if (front->right)
				QueuePush(&q, front->right);
		}
		printf("\n");
		LevelSize = QueueSize(&q);
	}
	printf("\n");
	QueueDestory(&q);
}

层序遍历是一种特殊的遍历方式,它按照树的层级,从上到下、从左到右的顺序访问每个节点。在我的函数中,这是通过使用一个队列实现的,队列是一种先进先出(FIFO)的数据结构。

过程步骤

  1. 初始化队列: 首先,创建一个空队列,用于存储将要访问的树节点。

  2. 根节点入队: 如果二叉树不为空,把根节点放入队列。这是遍历的起始点。

  3. 遍历队列中的节点: 接下来的步骤是循环进行的,直到队列为空。在每一次循环中,执行以下操作:

    • 处理当前层级的节点: 对于队列中的每个节点,执行以下子步骤:

      • 从队列中取出一个节点。
      • 访问该节点(比如,打印节点数据)。
      • 如果这个节点有左子节点,将左子节点加入队列。
      • 如果这个节点有右子节点,将右子节点加入队列。
    • 这个过程将会持续,直到队列为空。每处理完一层的所有节点,就开始处理下一层。

  4. 层与层之间的分隔: 在我的函数中,每处理完一层节点后,打印一个换行符,这样可以在输出中清楚地区分不同的层级。

  5. 销毁队列: 最后,当所有的节点都被访问过后,队列将会变空,遍历结束。此时,销毁或清空队列来释放资源。

通过这个过程,我的函数能够按层次顺序访问二叉树中的每个节点。这种遍历方式在很多场景中非常有用,例如在树的宽度优先搜索(Breadth-First Search, BFS)中。

结语

通过本文,我们不仅了解了二叉树的基本理论知识,还学习了如何在C语言中实现和操作这种数据结构。无论是对于初学者还是有经验的程序员来说,掌握这些知识都是非常有价值的。

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

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

《机器学习实战》MNIST 数据集的导入方法

1、在网上下载数据集 mnister 数据集有两个类型&#xff0c; &#xff08;1&#xff09;一个是手写的阿拉伯数字图片&#xff1a;MNIST 下载地址&#xff1a; 链接&#xff1a;https://pan.baidu.com/s/1dd-I-laysPbT8wxbyvxTvg 提取码&#xff1a;1234 &#xff08;2…

深度解析 Kafka 消息保证机制

Kafka作为分布式流处理平台的重要组成部分&#xff0c;其消息保证机制是保障数据可靠性、一致性和顺序性的核心。在本文中&#xff0c;将深入探讨Kafka的消息保证机制&#xff0c;并通过丰富的示例代码展示其在实际应用中的强大功能。 生产者端消息保证 1 At Most Once &quo…

Android 13 Settings蓝牙列表卡顿问题排查及优化过程

一.背景 此问题是蓝牙列表界面息屏后再点击亮屏蓝牙界面卡住,划不动也不能返回,在人多的时候(附近开启的蓝牙设备过多的时候)会卡住大概四五秒才能滑动. 优化前效果见资源: 二.查找耗时点 根据Android Studio的Profiler工具进行排查,查找主线程时间线比较长的方法,如下:…

记录 | centos源码编译bazel

tensorflow的源码编译依赖于 bazel 这里进行 bazel 的源码编译 1、安装依赖 sudo yum install -y java-11-openjdk sudo yum install -y java-11-openjdk-devel sudo yum install -y protobuf-compiler zip unzip2、知悉要安装的 bazel 的版本 务必安装受支持的 Bazel 版本…

展望2024年供应链安全

2023年是开展供应链安全&#xff0c;尤其是开源治理如火如荼的一年&#xff0c;开源治理是供应链安全最重要的一个方面&#xff0c;所以我们从开源治理谈起。我们先回顾一下2023的开源治理情况。我们从信通院《2023年中国企业开源治理全景观察》发布的信息。信通院调研了来自七…

linux安装mysql5.7(一遍过)

之前安装的时候遇到了很多问题&#xff0c;浪费了一些时间。整理出这份教程&#xff0c;照着做基本一遍过。 这是安装包: 链接&#xff1a;https://pan.baidu.com/s/1gBuQBjA4R5qRYZKPKN3uXw?pwd1nuz 1.下载安装包&#xff0c;上传到linux。我这里就放到downloads目录下面…

软著项目推荐 疫情数据分析与3D可视化 - python 大数据

文章目录 0 前言1 课题背景2 实现效果3 设计原理4 部分代码5 最后 0 前言 &#x1f525; 优质竞赛项目系列&#xff0c;今天要分享的是 &#x1f6a9; 大数据全国疫情数据分析与3D可视化 该项目较为新颖&#xff0c;适合作为竞赛课题方向&#xff0c;学长非常推荐&#xff0…

Google Bard vs. ChatGPT 4.0:文献检索、文献推荐功能对比

在这篇博客中&#xff0c;我们将探讨和比较四个不同的人工智能模型——ChatGPT 3.5、ChatGPT 4.0、ChatGPT 4.0插件和Google Bard。我们将通过三个问题的测试结果来评估它们在处理特定任务时的效能和响应速度。 导航 问题 1: 统计自Vehicle Routing Problem (VRP)第一篇文章发…

【Flink系列二】如何计算Job并行度及slots数量

接上文的问题 并行的任务&#xff0c;需要占用多少slot &#xff1f;一个流处理程序&#xff0c;需要包含多少个任务 首先明确一下概念 slot&#xff1a;TM上分配资源的最小单元&#xff0c;它代表的是资源&#xff08;比如1G内存&#xff0c;而非线程的概念&#xff0c;好多…

设备制造行业CRM:提升客户满意度,驱动业务增长

设备制造行业客户需求多样化、服务链路长&#xff0c;企业在关注APS、EMS等工业软件之余还要以客户为中心&#xff0c;做好客户服务。设备制造行业CRM管理系统是企业管理客户关系的利器&#xff0c;设备制造行业CRM的作用有哪些&#xff1f;一文带您看懂。 设备制造行业需要解…

【深度学习】强化学习(一)强化学习定义

文章目录 一、强化学习问题1、交互的对象1. 智能体&#xff08;Agent&#xff09;2. 环境&#xff08;Environment&#xff09; 2、强化学习的基本要素1. 状态 &#x1d460;2. 动作 &#x1d44e;3. 策略 &#x1d70b;(&#x1d44e;|&#x1d460;)4. 状态转移概率 &#x1…

elk(filebeat)日志收集工具

elk&#xff08;filebeat&#xff09;日志收集工具 elk&#xff1a;filebeat日志收集工具 和logstash相同 filebeat是一个轻量级的日志收集工具&#xff0c;所使用的系统资源比logstash部署和启动时使用的资源要小得多 filebeat可以运行在非Java环境。他可以代理logstash在…

ArcGIS Pro中怎么设置标注换行

在ArcGIS Pro中进行文字标注的时候&#xff0c;如果标注的字段内容太长&#xff0c;直接标注的话会不美观&#xff0c;而且还会影响旁边的标注显示&#xff0c;这里为大家介绍一下在ArcGIS Pro中设置文字换行的方法&#xff0c;希望能对你有所帮助。 数据来源 本教程所使用的…

【UE5】瞬移+马赛克过渡效果

效果 步骤 1. 新建一个工程&#xff0c;创建一个Basic关卡 2. 添加第三人称游戏资源到内容浏览器 3. 新建一个材质&#xff0c;这里命名为“M_Pixel” 打开“M_Pixel”&#xff0c;设置材质域为“后期处理” 在材质图表中添加如下节点 此时效果如下&#xff0c;已经有马赛克的…

JVM 命令行监控及诊断工具

面试题 你使用过Java虚拟机性能监控和故障处理工具吗&#xff1f;&#xff08;美图&#xff09; 怎么打出线程栈信息。&#xff08;字节跳动&#xff09; JVM诊断调优工具用过哪些&#xff1f; (京东) 怎么获取 Java 程序使用的内存&#xff1f;堆使用…

Django模板,Django中间件,ORM操作(pymysql + SQL语句),连接池,session和cookie, 缓存

day04 django进阶-知识点 今日概要&#xff1a; 模板中间件ORM操作&#xff08;pymysql SQL语句&#xff09;session和cookie缓存&#xff08;很多种方式&#xff09; 内容回顾 请求周期 路由系统 最基本路由关系动态路由&#xff08;含正则&#xff09;路由分发不同的app中…

ssh安装和Gitee(码云)源码拉取

文章目录 安装ssh服务注册码云公钥设置码云账户SSH公钥安装git客户端和git-lfs源码获取 安装ssh服务 更新软件源&#xff1a; sudo apt-get update安装ssh服务 sudo apt-get install openssh-server检查ssh是否安装成功 which ssh输出&#xff1a; /usr/bin/ssh启动ssh 服…

『亚马逊云科技产品测评』活动征文|基于亚马逊云EC2搭建PG开源数据库

授权声明&#xff1a;本篇文章授权活动官方亚马逊云科技文章转发、改写权&#xff0c;包括不限于在 Developer Centre, 知乎&#xff0c;自媒体平台&#xff0c;第三方开发者媒体等亚马逊云科技官方渠道 亚马逊EC2云服务器&#xff08;Elastic Compute Cloud&#xff09;是亚马…

conda配置不同版本的python及依赖库--conda conda conda

一、conda介绍 Conda 是一个开源的软件包管理系统和环境管理系统&#xff0c;用于安装多个不同版本的软件包及其依赖关系&#xff0c;并在它们之间轻松切换。 Conda 是为 Python 程序创建的&#xff0c;适用于 Linux&#xff0c;OS X 和Windows Conda可以构建不同的环境&#…

时间序列预测实战(二十四)PyTorch实现RNN进行多元和单元预测(附代码+数据集+完整解析)

一、本文介绍 本篇文章给大家带来的是利用我个人编写的架构进行RNN时间序列卷积进行时间序列建模&#xff08;专门为了时间序列领域新人编写的架构&#xff0c;简单且不同于市面上大家用GPT写的代码&#xff09;&#xff0c;包括结果可视化、支持单元预测、多元预测、模型拟合…