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

引言

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

核心内容

  • 二叉树的基本概念   

  • 我们首先需要理解什么是二叉树。在本文的代码中,二叉树由结构体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语言中实现和操作这种数据结构。无论是对于初学者还是有经验的程序员来说,掌握这些知识都是非常有价值的。

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

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

相关文章

web:[GXYCTF2019]BabyUpload(文件上传、一句话木马、文件过滤)

题目 页面显示为文件上传 随便上传一个文件看看 上传一个文本文件显示 上传了一个图片显示 上传包含一句话木马的图片 上传了一个包含php一句话木马的文件&#xff0c;显示如上 换一个写法 上传成功 尝试上传.htaccess&#xff0c;上传失败&#xff0c;用抓包修改文件后缀 …

python 编写的windows实用演示程序 使用到C语言风格,同时对Windows消息机制进行演示

因为内容较多 涉及知识点也多一些 但是具体使用时分开在几个文件 同时展示C语言的结构类型如何在python中定义与使用。为了便于区别 我定义的数据文件最后都带有一个数字1&#xff0c;涉及第三方库较少 &#xff0c;以便灵活使用Windows自带的很多api函数功能&#xff0c;可以根…

智能优化算法应用:基于爬行动物算法无线传感器网络(WSN)覆盖优化 - 附代码

智能优化算法应用&#xff1a;基于爬行动物算法无线传感器网络(WSN)覆盖优化 - 附代码 文章目录 智能优化算法应用&#xff1a;基于爬行动物算法无线传感器网络(WSN)覆盖优化 - 附代码1.无线传感网络节点模型2.覆盖数学模型及分析3.爬行动物算法4.实验参数设定5.算法结果6.参考…

Linux_CentOS_7.9配置oracle sqlplus、rman实现上下按键切换历史命令等便捷效率功能之简易记录

配置oracle sqlplus以及rman可以上下按键切换历史命令等便捷效率功能 设置前提是已经yum安装了rlwrap软件具体软件下载及配置参考文章http://t.csdnimg.cn/iXuVK su - oracleVim .bash_profile ## 文件中增加如下的别名设置 ---------------- alias sqlplusrlwrap sqlplus…

如何使用Matlab完成窗口与子窗口

目录 一、前言 二、主窗口与主窗口按钮 三、子窗口 四、调用函数并显示在子窗口中的文本框中 五、关闭子窗口 一、前言 有时候需要借用Matlab完成一个图窗功能&#xff0c;但是我们的程序不仅拥有功能&#xff0c;还拥有一些子功能&#xff0c;那么我们该如何借助Matlab完…

【无线网络技术】——无线个域网(学习笔记)

&#x1f4d6; 前言&#xff1a;手机、PC机、电视等消费类产品非常普及&#xff0c;人们希望有一种短距离、低成本、小功耗的无线通信方式&#xff0c;实现不同功能单一设备的互联&#xff0c;提供小范围内设备的自组网机制&#xff0c;并通过一定的安全接口完成自组小网与广域…

12.Java程序设计-基于Springboot框架的Android学习生活交流APP设计与实现

摘要 移动应用在日常生活中扮演着越来越重要的角色&#xff0c;为用户提供了方便的学习和生活交流渠道。本研究旨在设计并实现一款基于Spring Boot框架的Android学习生活交流App&#xff0c;以促进用户之间的信息分享、学术交流和社交互动。 在需求分析阶段&#xff0c;我们明…

3 文本分类入门finetune:bert-base-chinese

项目实战&#xff1a; 数据准备工作 bert-base-chinese 是一种预训练的语言模型&#xff0c;基于 BERT&#xff08;Bidirectional Encoder Representations from Transformers&#xff09;架构&#xff0c;专门用于中文自然语言处理任务。BERT 是由 Google 在 2018 年提出的一…

Spring Boot的日志

打印日志 打印日志的步骤: • 在程序中得到日志对象. • 使用日志对象输出要打印的内容 在程序中得到日志对象 在程序中获取日志对象需要使用日志工厂LoggerFactory,代码如下: package com.example.demo;import org.slf4j.Logger; import org.slf4j.LoggerFactory;public c…

【深度学习】迁移学习中的领域转移及迁移学习的分类

领域转移 根据分布移位发生的具体部分&#xff0c;域移位可分为三种类型&#xff0c;包括协变量移位、先验移位和概念移位 协变量移位: 在协变量移位的情况下&#xff0c;源域和目标域的边际分布是不同的&#xff0c;即ps(x)∕ pt(x)&#xff0c;而给定x的y的后验分布在域之间…

算法训练营Day3(链表)

语言 采用的Java语言&#xff0c;一些分析也是用于Java&#xff0c;请注意。 理论基础 对于链表我之前学的蛮多的&#xff0c;说基础的话&#xff0c;基本上就是说链表在内存上的不连续性 以及要和数组对比&#xff0c;数组知道下表之后&#xff0c;可以直接O&#xff08;1…

JM中ref_pic_list_modification bug记录

问题描述 今天在用JM对YUV420p编码时,发现编出的码流用ffplay播放花屏,报如下错误: JM的版本时19.1,没有使能B帧,PicOrderCntType设置为2,其它都是encoder.cfg中的默认配置。我用一些码流分析工具播放H264码流正常,用一些播放器播放也都存在花屏,不过大多数播放器都是…

【扩散模型】ControlNet从原理到实战

ControlNet从原理到实战 ControlNet原理ControlNet应用于大型预训练扩散模型ControlNet训练过程ControlNet示例1 ControlNet与Canny Edge2. ControlNet与Depth3. ControlNet与M-LSD Lines4. ControlNet与HED Boundary ControlNet实战Canny Edge实战Open Pose 小结参考资料 Cont…

如何使用ArcGIS Pro制作类似CAD的尺寸注记

经常使用CAD制图的朋友应该比较熟悉CAD内的尺寸标注&#xff0c;这样的标注看起来直观且简洁&#xff0c;那么在ArcGIS Pro内能不能制作这样尺寸注记呢&#xff0c;答案是肯定的&#xff0c;这里为大家介绍一下制作的方法&#xff0c;希望能对你有所帮助。 数据来源 本教程所…

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

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

动态设置当前按钮是否可以点击

当审核状态为通过时不可以点击审核按钮 <vxe-columnfixed"right"align"center"width"100"title"操作"><template slot-scope"scope"><el-button v-if"hasPermission(basic:archivalInfo:edit)":di…

资源三号5米全国数字高程模型DEM

简介 近些年来&#xff0c;国产高分辨率遥感卫星的发展突飞猛进&#xff0c;天绘系列卫星、资源三号卫星、高分一号、二号卫星以不断提高的影像空间分辨率、逐步增强的影像获取能力、较好的影像现势性等特点逐步打破了国外商业卫星的主导地位&#xff0c;开始广泛服务于各…

【webpack】应用篇

基础应用 代码分离常用的代码分离方法方法一&#xff1a;配置入口节点方法二&#xff1a;防止重复方法三&#xff1a;动态导入 缓存原因解决思路 缓存第三方库原因解决思路 将所有js文件单独存放文件夹拆分开发环境和生产环境配置公共路径环境变量和区分环境代码压缩 拆分配置文…

【React】路由的基础使用

react-router-dom6的基础使用 1、安装依赖 npm i react-router-dom默认安装最新版本的 2、在src/router/index.js import { createBrowserRouter } from "react-router-dom"/* createBrowserRouter&#xff1a;[/home]--h5路由createHashRouter&#xff1a;[/#/ho…

LAMP部署

目录 一、安装apache 二、配置mysql 三、安装php 四、搭建论坛 4、安装另一个网站 一、安装apache 1.关闭防火墙&#xff0c;将安装Apache所需软件包传到/opt目录下 systemctl stop firewalld systemctl disable firewalld setenforce 0 httpd-2.4.29.tar.gz apr-1.6.2.t…