数据结构——二叉树的链式结构

 个人主页日刷百题

系列专栏〖C语言小游戏〗〖Linux〗〖数据结构〗 〖C语言〗

🌎欢迎各位点赞👍+收藏⭐️+留言📝 

一、二叉树的创建

这里我们使用先序遍历的思想来创建二叉树,这里的内容对于刚接触二叉树的朋友可能有些难理解,不妨先看完下面的二叉树各种遍历再来看创建就会简单很多,为了保持文章的整体性,先讲二叉树的创建。

当然为了后续内容能够衔接,我们先手动创建一个固定的树,就是上面这棵树,后续内容全部围绕这棵树

typedef int DataType;
typedef struct TreeNode
{DataType data;struct  TreeNode* left;struct  TreeNode* right;
}TreeNode;
TreeNode* BuyNode(int x)
{TreeNode* node = (TreeNode*)malloc(sizeof(TreeNode));if (node == NULL){perror("malloc fail:");return NULL;}node->data = x;node->left = node->right = NULL;
}TreeNode* CreatTree()
{TreeNode* node1 = BuyNode(1);TreeNode* node2 = BuyNode(2);TreeNode* node3 = BuyNode(3);TreeNode* node4 = BuyNode(4);TreeNode* node5 = BuyNode(5);TreeNode* node6= BuyNode(6);node1->left = node2;node1->right = node4;node2->left = node3;node4->left = node5;node4->right = node6;return node1;
}

现在开始讲如何用前序遍历方式来进行创建二叉树,这里给俩种创建方法。

1.1  返回根节点指针创建

注:我们用前序遍历方式输入数字,-1代表空,上面的二叉树可写为:1 2 3 -1 -1 -1 4 5 -1 -1 6 -1 -1

TreeNode* CreatTree()
{int i;scanf("%d", &i);if (i == -1){return NULL;}TreeNode* root = (TreeNode*)malloc(sizeof(TreeNode));if (root == NULL){perror("malloc fail:");exit(-1);}root->data = i;root->left =  CreatTree();root->right = CreatTree();return root;
}

注:return root 是不能省略的,递归返回时,遇到空返回;或者构建完子数,返回根节点,作为上一级左子树,构建完子树,返回根节点,作为上一级右子树,依次递归回去,直到返回整个数的根节点

1.2 二级指针传参创建

同样的,依然构建上面的而二叉树,用前序遍历方式输入数字,-1代表空,上面的二叉树可写为:1 2 3 -1 -1 -1 4 5 -1 -1 6 -1 -1

void CreatTree(TreeNode** root)
{int i;scanf("%d", &i);if (i == -1){*root = NULL;}else{*root = (TreeNode*)malloc(sizeof(TreeNode));if (*root == NULL){perror("malloc fail:");exit(-1);}(*root)->data = i;CreatTree(&((*root)->left));CreatTree(&((*root)->right));}}

 注:二级指针传参可以改变一级指针的指向,同样的,这里创建好根节点后,创造左子树,在创造右子树,依次递归下去。

二、二叉树的遍历

我们先从最简单的操作----遍历学起。所谓二叉树遍历(Traversal)就是按照某种特定的规则,依次对二叉树中的结点进行相应的操作,并且每个节点有且只操作一次。访问结点所做的操作依赖于具体的应用问题。 遍历是二叉树上最重要的运算之一,也是二叉树上进行其它运算的基础。二叉树的遍历分为四种:前序遍历、中序遍历、后序遍历和层序遍历。

2.1 前序遍历

前序遍历(Preorder Traversal)又称先根遍历,即先遍历根结点,再遍历左子树,最后遍历右子树。而对于子树的遍历,也服从上述规则。利用递归,我们可以很快地写出代码:

// 二叉树前序遍历
void PreOrder(TreeNode* root)
{if (root == NULL){printf("NULL ");return ;}printf("%d ", root->data);PreOrder(root->left);PreOrder(root->right);}

便于我们更好的理解,我们画出递归展开图

2.2 中序遍历

中序遍历(Inorder Traversal)又称中根遍历,即先遍历左子树,再遍历根结点,最后遍历右子树。同样,子树的遍历规则也是如此。递归代码如下:

// 二叉树中序遍历
void InOrder(TreeNode* root)
{if (root == NULL){printf("NULL ");return ;}InOrder(root->left);printf("%d ", root->data);InOrder(root->right);
}

2.3 后序遍历

后序遍历(Inorder Traversal)又称后根遍历,即先遍历左子树,再遍历右子树,最后遍历根结点递归代码如下:

// 二叉树后序遍历
void PostOrder(TreeNode* root)
{if (root == NULL){printf("NULL ");return ;}PostOrder(root->left);PostOrder(root->right);printf("%d ", root->data);
}

2.4  层序遍历

除了先序遍历、中序遍历、后序遍历外,还可以对二叉树进行层序遍历。设二叉树的根节点所在
层数为1,层序遍历就是从所在二叉树的根节点出发,首先访问第一层的树根节点,然后从左到右访问第2层上的节点,接着是第三层的节点,以此类推, 自上而下,自左至右逐层访问树的结点 的过程就是层序遍历。
层序遍历借助队列实现,思路解析图如下:
//层序遍历
void LevelOrder(TreeNode* root)
{Queue pq;QueueInit(&pq);if (root == NULL){QueueDestroy(&pq);return;}QueuePush(&pq,root);while (!QueueEmpty(&pq)){TreeNode* front= QueueFront(&pq);printf("%d ", front->data);QueuePop(&pq);if (front->left!= NULL){QueuePush(&pq, front->left);}if (front->right != NULL){QueuePush(&pq, front->right);}}QueueDestroy(&pq);
}

思考:当然层序遍历这里有一个变形,我们能不能将二叉树每一层打印单独放一行,该怎么做呢?

思路:

(1)设二叉树的根节点所在层数为1,第一层根节点进队列,队列元素个数为1,size==1
(2)每出队列一次,size--,根节点出完队列,俩个子节点进队列,此时队列元素个数为第二层节点个数,size==2
(3)当我们出队列size次,把第二层元素出完,队列剩下的元素是第三层节点,size==QueueSize
以此类推,以size为循环条件,则可实现每层单独打印一行

void LevelOrder(TreeNode* root)
{Queue pq;QueueInit(&pq);if (root == NULL){QueueDestroy(&pq);return;}QueuePush(&pq,root);int size = 1;while (!QueueEmpty(&pq)){while (size--){TreeNode* front = QueueFront(&pq);printf("%d ", front->data);QueuePop(&pq);if (front->left != NULL){QueuePush(&pq, front->left);}if (front->right != NULL){QueuePush(&pq, front->right);}}size = QueueSize(&pq);printf("\n");}QueueDestroy(&pq);
}

三、二叉树的结点

3.1 二叉树的总结点数

一颗二叉树的结点数我们可以看作是根结点+左子树结点数+右子树结点数,那左右子树的结点数又是多少呢?按照相同的方法继续拆分,层层递归直到左右子树为空树,返回空树的结点数0即可。递归代码如下:

// 二叉树节点个数
int TreeSize(TreeNode* root)
{return root == NULL? 0 : TreeSize(root->left) + TreeSize(root->right) + 1;
}

3.2 二叉树的叶子结点数

左右子树都为空的结点即是叶子结点。这里分为两种情况:左右子树都为空和左右子树不都为空。

(1)当左右子树都为空时,则这颗树的叶子结点数为1(根节点)。

(2)当左右子树不都为空,即根结点不是叶子结点时,这棵树的叶子结点数就为左子树叶子结点数+右子树叶子结点数(空树没有叶子结点)。
 

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

3.3 二叉树第k层结点数

一颗树第k层的结点数我们可以拆分为其左子树第k-1层结点+右子树第k-1层结点(注:当前节点为第一层)

(1)若为空树,无论哪层节点数都是0

(2)若不是空树且k==1,则只有一个节点(根节点)

// 二叉树第k层节点个数
int TreeLevelKSize(TreeNode* root, int k)
{assert(k > 0);if (root!=NULL&&k == 1){return 1;}if (root == NULL){return 0;}if (k > 1){return TreeLevelKSize(root->left, k - 1) + TreeLevelKSize(root->right, k - 1);}
}
// 判断二叉树是否是完全二叉树
bool TreeComplete(TreeNode* root)
{Queue pq;QueueInit(&pq);if (root == NULL){QueueDestroy(&pq);return;}QueuePush(&pq, root);while (!QueueEmpty(&pq)){TreeNode* front = QueueFront(&pq);QueuePop(&pq);if (front == NULL){break;}QueuePush(&pq, front->left);QueuePush(&pq, front->right);}while (!QueueEmpty(&pq)){TreeNode* front = QueueFront(&pq);QueuePop(&pq);if (front != NULL){return false;}}QueueDestroy(&pq);return true;
}

四、二叉树的查找

二叉树的查找本质上就是一种遍历,只不过是将之前的打印操作换为查找操作而已。我们可以使用前序遍历来进行查找:

(1)先比较根结点是否为我们要查找的结点,如果是,返回根节点地址

(2)如果不是,遍历左子树,如果左子树是,直接返回节点地址;不是则遍历右子树,如果右子树是,直接返回节点地址,不是返回空,说明左右子树都没找到。

// 二叉树查找值为x的节点
TreeNode* TreeFind(TreeNode* root, DataType x)
{if (root == NULL){return NULL;}if (root->data == x){return root;}TreeNode* node1 = TreeFind(root->left, x);if (node1){return node1;}TreeNode* node2 = TreeFind(root->right, x);if (node2){return node2;}return NULL;
}

五、二叉树的高度/深度

树中结点的最大层次称为二叉树的高度。因此,一颗二叉树的高度我们可以看作是

1(根结点)+左右子树高度的较大值。层层递归下去直到遇到空树返回0即可,

这里值得注意的是:不要写成

return TreeHeight(root->left)>TreeHeight(root->right) ? TreeHeight(root->left)+1:TreeHeight(root->right)+1;
}
这里比较好左右子树较大的一颗后,又会从新递归较大那颗子树高度,会造成重复计算,时间复杂度增高。

我们要保存好左右子树层数,避免重复计算,代码如下:

//二叉树的高度
int TreeHeight(TreeNode* root)
{if (root == NULL){return 0;}int left = TreeHeight(root->left);int right = TreeHeight(root->right);return  left>right ? left+1:right+1;
}

六、完全二叉树的判断

这里的思路利用了层序遍历,不同的是,将空节点指针也入队列,当我们遇到第一个空节点指针则退出循环,然后对队列进行检测,若第一个空节点指针以后全都是空,则为完全二叉树,反之,不为完全二叉树。

注:当在队列遇到第一个空节点指针时,二叉树中空节点指针之后所有非空节点指针全部进队列

思路解析图如下:

代码如下:

// 判断二叉树是否是完全二叉树
bool TreeComplete(TreeNode* root)
{Queue pq;QueueInit(&pq);if (root == NULL){QueueDestroy(&pq);return;}QueuePush(&pq, root);while (!QueueEmpty(&pq)){TreeNode* front = QueueFront(&pq);QueuePop(&pq);if (front == NULL){break;}QueuePush(&pq, front->left);QueuePush(&pq, front->right);}while (!QueueEmpty(&pq)){TreeNode* front = QueueFront(&pq);QueuePop(&pq);if (front != NULL){return false;}}QueueDestroy(&pq);return true;
}

 七、二叉树的销毁

7.1 一级指针传参销毁

同样的,和创建节点一样,我们给出俩个销毁方式:

(1)一种是传一级指针方式,这种方式不是改变根节点的指向,需要在销毁函数结束后,将root置为NULL

void TreeDestroy(TreeNode* root)//出来将root=NULL
{if (root == NULL){return;}TreeDestroy(root->left);TreeDestroy(root->right);free(root);}

7.2 二级指针传参销毁 

(2)另一种是传二级指针,直接在函数内部将每一个销毁的节点指针置为NULL.

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

 总结:本篇文章将二叉树的基础知识差不多囊括了,后续的话还需要大量练习做巩固加强,递归比较抽象难以理解,需要动手画递归展开图进行帮助理解。

希望大家阅读完可以有所收获,同时也感谢各位铁汁们的支持。文章有任何问题可以在评论区留言,百题一定会认真阅读!

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

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

相关文章

Word插件-好用的插件-一键设置字体--大珩助手

常用字体 整理了论文、公文常用字体 整理了常用的论文字体,可一键设置当前节或选择的文字的字体 字体设置 包含字体选择、字体颜色 特殊格式 包含首字下沉、段落分栏、统一宽度、双行合一、上标切换、下标切换、转为全角、转为半角、挖词填空、当前日期、大写金…

LabVIEW开发远程结构健康监测系统

LabVIEW开发远程结构健康监测系统 工程师依赖于振动监测来评估建筑物、桥梁和其他大型结构的完整性。传统的振动监测工具在数据收集上存在限制,无法长时间收集高保真波形。随着内存存储、处理器速度和宽带无线通信技术的进步,出现了对能够长时间收集并实…

Navicat 技术指引 | 适用于 GaussDB 分布式的查询功能

Navicat Premium(16.3.3 Windows 版或以上)正式支持 GaussDB 分布式数据库。GaussDB 分布式模式更适合对系统可用性和数据处理能力要求较高的场景。Navicat 工具不仅提供可视化数据查看和编辑功能,还提供强大的高阶功能(如模型、结…

深入了解对象与内置构造函数

1. 深入对象 1.1 创建对象的三种方式 1.2 构造函数 语法约定: 总结 构造函数可以快速创建多个对象大写字母开头的函数使用new关键字将对象实例化构造函数不需要返回值自动返回新的对象 new实例化的执行过程 创建空对象this指向对象执行代码,追加新…

使用wire重构商品微服务

一.wire简介 Wire 是一个轻巧的Golang依赖注入工具。它由Go Cloud团队开发,通过自动生成代码的方式在编译期完成依赖注入。 依赖注入是保持软件 “低耦合、易维护” 的重要设计准则之一。 此准则被广泛应用在各种开发平台之中,有很多与之相关的优秀工…

使用pyftpdlib组件实现FTP文件共享

目录 一、引言 二、技术背景 三、实现逻辑 1、创建FTP服务器: 2、实现文件共享: 3、设置用户权限: 4、处理异常: 5、优化与扩展: 四、代码实现 五、测试与评估 测试用例: 评估方法:…

React/Vue/Svelte 前端项目中开始使用TailwindCSS

背景 TailwindCSS 近年来在前端圈非常流行,它摆脱了原有的CSS限制,以灵活实用为卖点,用户通过各种class组合即可构建出漂亮的用户界面。对于初学者而言,可能需要一些上手成本,一旦掌握实用技巧后,Tailwind…

Unity中Batching优化的GPU实例化整理总结

文章目录 前言一、GPU Instancing的支持1、硬件支持2、Shader支持3、脚本支持 二、我们来顺着理一下GPU实例化的使用步骤1、GPU实例化前的C#代码准备2、在 appdata 和 v2f 中定义GPU实例化ID3、在顶点着色 和 片元着色器 设置GPU Instance ID,使实例化对象顶点位置正…

Docker的资源控制

Docker的资源控制: 对容器使用宿主机的资源进行限制。 CPU 内存 磁盘I/O(读写性能) docker使用linux自带的功能cgroup control groups是linux内核系统提供的一种可以限制,记录,隔离进程组所使用的物理资源的一种机制。 docker借助这个机制…

go grpc高级用法

文章目录 错误处理常规用法进阶用法原理 多路复用元数据负载均衡压缩数据 错误处理 gRPC 一般不在 message 中定义错误。毕竟每个 gRPC 服务本身就带一个 error 的返回值,这是用来传输错误的专用通道。gRPC 中所有的错误返回都应该是 nil 或者 由 status.Status 产…

如何克服微服务测试的挑战,并最大化收益

多年来,微服务一直是行业趋势,但组织却未能从该方法中获益,并因发布失败而苦苦挣扎。这些失败通常归结为测试服务之间的接口以获得预期的质量、安全性和性能的困难。 最终,未能以足够稳健的方式测试这些 API。一线希望是遗留 SOA…

cookie总结

cookie和session: 一、Cookie和Session二、使用Cookie保存用户上次的访问时间。三、Cookie常用方法总结乱码问题解决: 一、Cookie和Session 会话:用户从打开浏览器到关闭的整个过程就叫1次会话。 比如有的网站登录过一次,下次再进…

使用 Kubernetes 为 CI/CD 流水线打造高效可靠的临时环境

介绍 在不断发展的科技世界中,快速构建高质量的软件至关重要。在真实环境中测试应用程序是及早发现和修复错误的关键。但是,在真实环境中设置 CI/CD 流水线进行测试可能既棘手又昂贵。 Kubernetes 是一个流行的容器编排平台,提供临时环境解决…

【qt】Qt+OpenCv读取带有中文路径的图片

【opencv4.5.1版本】下载exe解压即可。。。https://opencv.org/releases/page/2/ 【qt5.15.2】 pro文件 QT core guigreaterThan(QT_MAJOR_VERSION, 4): QT widgetsCONFIG c17# You can make your code fail to compile if it uses deprecated APIs. # In order to …

硕士毕业论文格式修改要点_word

目录 0、最开始要做的事情1、更改样式(先善器)2、多级标题(解决自动更新问题必要的基础设置)2、插入图片(1)设置一个图片样式——“无间隔”(2)插入题注(3)修…

【flink番外篇】1、flink的23种常用算子介绍及详细示例(完整版)

Flink 系列文章 一、Flink 专栏 Flink 专栏系统介绍某一知识点,并辅以具体的示例进行说明。 1、Flink 部署系列 本部分介绍Flink的部署、配置相关基础内容。 2、Flink基础系列 本部分介绍Flink 的基础部分,比如术语、架构、编程模型、编程指南、基本的…

小白学java栈的经典算法问题——第四关白银挑战

内容1.括号匹配问题2.最小栈3.最大栈 1.括号匹配问题 栈的典型题目还是非常明显的,括号匹配、表达式计算等等几乎都少不了栈,本小节我们就看两个最经典的问题 首先是LeetCode20,链接 本道题还是比较简单的,其中比较麻烦的是如何判断两个符…

力扣面试题 08.12. 八皇后(java回溯解法)

Problem: 面试题 08.12. 八皇后 文章目录 题目描述思路解题方法复杂度Code 题目描述 思路 八皇后问题的性质可以利用回溯来解决,将大问题具体分解成如下待解决问题: 1.以棋盘的每一行为回溯的决策阶段,判断当前棋盘位置能否放置棋子 2.如何判…

hbuilder + uniapp +vue3 开发微信云小程序

1、创建项目: 2、创建项目完成的默认目录结构: 3、在根目录新建一个文件夹cloudFns(文件名字随便),存放云函数源码: 4、修改manifest.json文件:添加 小程序 appid和cloudfunctionRoot&#xff0…

python的websocket方法教程

WebSocket是一种网络通信协议,它在单个TCP连接上提供全双工的通信信道。在本篇文章中,我们将探讨如何在Python中使用WebSocket实现实时通信。 websockets是Python中最常用的网络库之一,也是websocket协议的Python实现。它不仅作为基础组件在…