【数据结构】二叉树结构

二叉树

  • 前言
  • 引入二叉树——二叉树的独特之处
  • 一、二叉树的结构 的 核心思想
  • 二、二叉树的代码实现
  • >binary tree.h
  • > binary tree.c
    • (一)手动构建二叉树 <测试用>
    • (二)二叉树销毁
    • (三)节点个数
    • (四)二叉树第k层节点个数
    • (五) 二叉树查找值为x的节点 — 前序遍历
    • (六) 二叉树前序遍历
    • (七) 二叉树中序遍历
    • (八) 二叉树后序遍历
    • (九) 层序遍历—— 队列实现
  • 深度优先遍历 和 广度优先遍历
    • 深度优先遍历(DFS)
    • 广度优先遍历(BFS)
  • (十)判断二叉树是否是完全二叉树
  • (十一)树的高度
  • (十二)前序遍历 构建二叉树
  • > test.c



前言

引入二叉树——二叉树的独特之处

在学习二叉树之前,我们已经学习了从最开始的顺序表,到后面的栈和队列,再到,然后就到了我们现在的二叉树

我们前面学习了堆及其结构所带来的性质

堆:根据其性质(我在【数据结构】二叉树的顺序结构及实现(理论学习篇)这篇博客里详细的讲述了堆的性质,大家可通过此篇来掌握),可得到 min 或 max 值 从而解决TopK问题(此问题也在前面的链接的那篇文章中有讲述)

既然堆结构有这样的作用和意义,那么请大家思考一个问题:二叉树结构真正的意义是什么?

  • 在对于数据的存储方面(数据结构的增删查改):
    在二叉树结构中插入数据,倒不如直接用顺序表实现的二叉树结构的本质-顺序表去直接存储数据来的方便

  • 但在二分查找数据并删除数据 时:

    • 顺序表要先排序成有序(遍历一遍数组,时间复杂度是O(N^2)),且找到想要的数据并且想要实现删插数据的功能时,顺序表的数据需要挪动,时间复杂度是O(N)
    • 而链表实现的二叉树 则不用挪动数据,通过堆的性质,构建出有一定性质结构的二叉树,根据这些性质结构,通过中序遍历二叉树,可快速找出想要的数据,时间复杂度是O(N*logN)

由此可见,普通的链式二叉树没有意义
而我们现在学习普通链式二叉树,重点不在于对其数据的增删查改,而是在于通过学习二叉树的结构以及其性质为后面更复杂的 搜索二叉树(AVL,红黑树等) 打下基础。



一、二叉树的结构 的 核心思想

二叉树的结构的核心思想就是 ——" 递归 "
递归的本质 就在于 将一个复杂庞大的问题 逐步分解成 模式一样、最小规模 的子问题 (分治:层层分包)

递归代码逻辑运行的理解 : 函数栈帧的创建和销毁 (建议大家先去了解一下 函数栈帧的创建和销毁的运行机制)

谨记:函数递归返回的不是最外层,而是上一层

树的结构本来就是递归结构,所以更适合用递归来实现



二、二叉树的代码实现

>binary tree.h

#pragma once#define _CRT_SECURE_NO_WARNINGS 1
#include<stdio.h>
#include<assert.h>
#include<stdbool.h>typedef int BTDataType;typedef struct BinaryTreeNode {BTDataType val;struct BinaryTreeNode* left;struct BinaryTreeNode* right;
}BTNode;// 二叉树销毁
void BinaryTreeDestory(BTNode* root);// 二叉树节点个数
int BinaryTreeSize(BTNode* root);// 二叉树叶子节点个数
int BinaryTreeLeafSize(BTNode* root);// 二叉树第k层节点个数
int BinaryTreeLevelKSize(BTNode* root, int k);// 二叉树查找值为x的节点
BTNode* BinaryTreeFind(BTNode* root, BTDataType x);// 二叉树前序遍历 
void BinaryTreePrevOrder(BTNode* root);// 二叉树中序遍历
void BinaryTreeInOrder(BTNode* root);// 二叉树后序遍历
void BinaryTreePostOrder(BTNode* root);// 层序遍历
void BinaryTreeLevelOrder(BTNode* root);


> binary tree.c

(一)手动构建二叉树 <测试用>

手动构建二叉树 —— 保存成文本文件,记录这段代码,用于下一次检测二叉树代码的正确性的样本,打造自己的 代码库
自己画二叉树的图顺着图来链接各节点就好。

//创建树节点
BTNode* BuyNode(BTDataType x) {BTNode* node = (BTNode*)malloc(sizeof(BTNode));if (node == NULL) {perror("malloc fail");exit(-1);}node->val = x;node->left = NULL;node->right = NULL;return node;
}int main() {//手动构建二叉树   —— 保存成文本文件,记录这段代码,用于下一次检测二叉树代码的正确性的样本,打造自己的代码库BTNode* node1 = BuyNode(1);BTNode* node2 = BuyNode(2);BTNode* node3 = BuyNode(3);BTNode* node4 = BuyNode(4);BTNode* node5 = BuyNode(5);BTNode* node6 = BuyNode(6);BTNode* node7 = BuyNode(7);BTNode* node8 = BuyNode(8);BTNode* node9 = BuyNode(9);node1->left = node2;node1->right = node4;node2->left = node3;node4->left = node5;node4->right = node6;node2->right = node7;node3->left = node8;node6->right = node9;}


(二)二叉树销毁

前序也可以,只是要用两个临时的指针变量去存放root->left 和 root->right 这两个节点的地址,避免释放内存后找不到下一节点的指针了

// 二叉树销毁 —— 后序递归('递'到最深处,逐层往回'归'释放内存)    //前序也可以,只是要用两个临时的指针变量去存放root->left 和 root->right 这两个节点的地址,避免释放内存后找不到下一节点的指针了
void BinaryTreeDestroy(BTNode* root) { //一级指针,形参 不影响实参if (root == NULL)  //递归返回的条件,碰到NULL就开始返回了return;BinaryTreeDestory(root->left);BinaryTreeDestory(root->right);  //'递'到最深处free(root);//root=NULL;   //思考:这里将root节点置空,有用吗}
  • 思考:这里将root节点置空,有用吗?
    答:BinaryTreeDestroy用于接收的只是一级指针,接收到的只是root节点里面的内容,所以只是root的一份临时拷贝(形参) ,置空要到外面才能滞空。


(三)节点个数

  • 先行版
// 节点个数
int TreeSize(BTNode* root)
{int size = 0;     //思考:这样 int size会出现什么问题if (root == NULL)return 0;else++size;TreeSize(root->left);TreeSize(root->right);return size;
}

int size = 0; // 思考:这样 int size会出现什么问题
每次函数调用递归,int size都为0,无法做到记数的作用。

  • static局部变量版
 //节点个数
int TreeSize(BTNode* root)
{static int size = 0;if (root == NULL)return 0;else++size;TreeSize(root->left);TreeSize(root->right);return size;
}

static int size=0; // 局部的静态变量的初始化 只会执行一次(只会在第一次调用时执行,后面就不会再初始化为0了)

【关于static的知识点补充,后在后续补充,到时候会把链接放这里,敬请期待】

也正是因为static有这样的特性
引出问题:下一次再想调用这个函数,会出现什么情况?
下一次再调用时,static修饰的变量并未被销毁,还存放上一次调用函数的数据,并且在作用域外也没有办法对static局部变量进行修改

解决方面如下方版本:改为 定义全局变量在下一次调用函数之前初始化一下

  • 一路记数下去版
int size=0;//定义全局变量//节点个数
int BinaryTreeSize(BTNode* root) {if (root == NULL)return 0;elsesize++;BinaryTreeSize(root->left);BinaryTreeSize(root->right);return size;
}
  • 递归版
// 二叉树节点个数
int size=0;//定义全局变量int BinaryTreeSize(BTNode* root) {if (root == NULL)return 0;return BinaryTreeSize(root->left) + BinaryTreeSize(root->right)+1;}

更优化的写法

int BinaryTreeSize(BTNode* root) {//   若根节点为空,空则返回0,否则返回 BinaryTreeSize(root->left) + BinaryTreeSize(root->right)+1return root==NULL?0:BinaryTreeSize(root->left) + BinaryTreeSize(root->right)+1;}


(四)二叉树第k层节点个数

核心思想: root的第k层,相对与root->left,root->right的第k-1层。

// 二叉树第k层节点个数
int BinaryTreeLevelKSize(BTNode* root, int k) {assert(k > 0);if (root == 0)return 0;if (k == 1)    //当k=1时,到第k层了return 1;//root的第k层,相对与root->left,root->right的第k-1层return BinaryTreeLevelKSize(root->left, k - 1) + BinaryTreeLevelKSize(root->right, k - 1);
}


(五) 二叉树查找值为x的节点 — 前序遍历

思路就是先遍历左子树,左子树找不到再去找右子树。

// 二叉树查找值为x的节点 前序遍历
BTNode* BinaryTreeFind(BTNode* root, BTDataType x) {  if (root == NULL)  //空树 递归返回的条件return NULL;if (root->val == x)return root;BTNode* ret = NULL;     //用一个指针变量来保存返回的指针ret= BinaryTreeFind(root->left, x); if (ret)                //如果ret不为空,则返回retreturn ret;ret= BinaryTreeFind(root->right, x);    //ret为空则继续遍历右子树if (ret)return ret;return NULL;
}

也是根据这样的思路,有的同学可能会出现这样的错误

// 二叉树查找值为x的节点 前序遍历
BTNode* BinaryTreeFind(BTNode* root, BTDataType x) {  if (root == NULL)  //空树 递归返回的条件return NULL;if (root->val == x)return root;return BinaryTreeFind(root->left, x) || BinaryTreeFind(root->right, x)  //如果左子树找到了,就不用再遍历右子树了
}

思考一下,这样会导致什么问题。

解析:逻辑运算符:|| 逻辑或 。确实是能做到 判断左边若为true,则结束判断。
但判断完了,其返回的是数(C语言中,判断真假,非0为真,0则为假),而不是 函数返回类型 BTNode* BinaryTreeFind (BTNode* root, BTDataType x) 中的BTNode* 。返回的就不是指针了。

【关于 逻辑或 || 运算符 的知识点补充,后在后续补充,到时候会把链接放这里,敬请期待】



(六) 二叉树前序遍历

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


(七) 二叉树中序遍历

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


(八) 二叉树后序遍历

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


(九) 层序遍历—— 队列实现

队列:先进先出

  • 核心思路:上一层带下一层

(需要包含Queue.h Queue.c 文件)

// 层序遍历—— 队列实现
void BinaryTreeLevelOrder(BTNode* root) {Que q;QueueInit(&q);if(root)QueuePush(&q, root); //存放头节点的地址,QNode里的data存放地址值while (!QueueEmpty(&q)) {BTNode* front = QueueFront(&q); //取出用BTNode*来保存   //取头节点,读取printf头节点printf("%d",front->val);if (front->left)                        //再顺带把其下一层带上QueuePush(&q, front->left);if (front->right)QueuePush(&q,front->right);QueuePop(&q);                           //再排出其左节点}printf("\n");QueueDestroy(&q);
}


  • 学习完 二叉树的 前、中、后序遍历 以及 层序遍历 ,我们来介绍两个概念

深度优先遍历 和 广度优先遍历

深度优先遍历(DFS)

  1. 广义:前、中、后序遍历都是(都往深走,只是访问节点的时机不同)
  2. 严格:前序(先递归到最深处,才往回走)

广度优先遍历(BFS)

层序遍历 [ 队列配合 ] (非递归)



(十)判断二叉树是否是完全二叉树

完全二叉树:前n-1层都是满的,最后一层不满,但是连续
非完全二叉树:非空节点不连续,中间有空节点,=> 要录空,不然看不出来

核心判断条件层序遍历,录到空后,若后面还有节点(剩下的若不为空),则说明是非二叉树

// 层序遍历进阶应用——判断二叉树是否是完全二叉树
int BinaryTreeComplete(BTNode* root) {Que q;QueueInit(&q);if (root != NULL)QueuePush(&q,root);while(!QueueEmpty(&q)) {BTNode* front = QueueFront(&q);    //因为传入的是BTNode的地址,从队列中取出也需要BTNode*if (front == NULL)     //遇空开始判断,后面是否还有节点break;QueuePush(&q, front->left);QueuePush(&q, front->right);QueuePop(&q);}// 已经遇到空节点,如果队列中后面的节点还有非空,就不是完全二叉树while (!QueueEmpty(&q)) {BTNode* front = QueueFront(&q);   //挨个判断剩下的节点是否都为空,都为空则为完全二叉树QueuePop(&q);if (front != NULL) {QueueDestroy(&q);return false;}}QueueDestroy(&q);return true;
}


(十一)树的高度

核心思路:对比左树和右树的高度,大的那一棵的高度+1 。

  • 先行版
//树的高度
int TreeHeight(BTNode* root) {if (root == NULL)return 0;return TreeHeight(root->left) > TreeHeight(root->right) ? TreeHeight(root->left) + 1 : TreeHeight(root->right) + 1;
}

这样会存在什么问题,是否高效?

虽然使用的是三目运算符,使代码更简洁,但代码运行是否高效并不是看你代码简不简洁,而是要看你计算机到底要跑多久多少次。你人是爽了,但你电脑也快跑死了。

☆★存在的问题:
return TreeHeight(root->left) > TreeHeight(root->right) ? TreeHeight(root->left) + 1 : TreeHeight(root->right) + 1;

前半部分 TreeHeight(root->left) > TreeHeight(root->right) ,递归计算出 leftTreeHeight 和 rightTreeHeight 。
由于递归计算完后,并没有变量将其递归结果进行保存 ,判断出来结果后,再进行 TreeHeight(root->left) + 1 的运算,又展开一次深度递归进行运算 。

这样进行了大量的重复计算,本来递归要进行的计算就多了,计算还要重复的。



  • 改良版
    根据这个问题,进行改良:将递归结果用变量进行保存
//树的高度 —— 优化版
int TreeHeight(BTNode* root) {if (root == NULL)return 0;int leftHeight = TreeHeight(root->left);int rightHeight = TreeHeight(root->right);return leftHeight > rightHeight ? leftHeight + 1 : rightHeight + 1;
}


  • fmax 版本
int fmax(int x,int y) {if (x > y)return x;elsereturn y;
}//树的高度 —— 终版
int TreeHeight(BTNode* root) {if (root == NULL)return 0;return fmax(TreeHeight(root->left), TreeHeight(root->right)) + 1;    //TreeHeight(root->left)在传过去之前就已经计算出结果,并将结果用int x(形参)接收储存。
}


运行结果

手动构建的二叉树
在这里插入图片描述
在这里插入图片描述


(十二)前序遍历 构建二叉树

// 通过前序遍历的数组"ABD##E#H##CF##G##"构建二叉树
BTNode* BinaryTreeCreate(char* str, int* pi) {if (str[*pi]=='#') {(*pi)++;return NULL;}BTNode* root = (BTNode*)BuyNode(str[(*pi) ++]);root->left = BinaryTreeCreate(str, pi);root->right = BinaryTreeCreate(str, pi);}int main() {char a[] = "ABD##E#H##CF##G##";int i = 0;int size = sizeof(a) / sizeof(a[0]);BinaryTreeCreate(&a,&i);}


> test.c

int main()
{// 手动构建BTNode* node1 = BuyNode(1);BTNode* node2 = BuyNode(2);BTNode* node3 = BuyNode(3);BTNode* node4 = BuyNode(4);BTNode* node5 = BuyNode(5);BTNode* node6 = BuyNode(6);node1->left = node2;node1->right = node4;node2->left = node3;node4->left = node5;node4->right = node6;PrevOrder(node1);printf("\n");InOrder(node1);printf("\n");PostOrder(node1);printf("\n");printf("%d\n", TreeSize(node1));//size = 0;printf("%d\n", TreeSize(node1));TreeDestroy(node1);node1 = NULL;return 0;
}

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

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

相关文章

OSPF高级特性1(重发布,虚链路)

目录 OSPF高级特性(1) 一、OSPF不规则区域类型 二、解决方案 1、使用虚连接 演示一&#xff1a;非骨干区域无法和骨干区域保持连通 演示二&#xff1a;骨干区域被分割 2、使用多进程双向重发布 OSPF高级特性(1) 一、OSPF不规则区域类型 产生原因&#xff1a;区…

界面组件DevExtreme v23.1 —— UI模板库更新新功能

在DevExtreme在v22.2版本中附带了针对Angular、React和Vue的新UI模板库&#xff0c;这个新的UI模板库包含多个响应式UI模板&#xff0c;您可以将其用作业务应用程序的起点&#xff0c;模板包括类似CRM的布局、仪表盘、身份验证表单等。在这篇文章中&#xff0c;我们将看看在v23…

如何让企业配件管理高效又智能!仓库配件出入库管理系统哪家的好用?

在当今快速发展的商业环境中&#xff0c;企业运营的效率和管理的重要性日益凸显。对于许多企业来说&#xff0c;仓库配件管理是一个关键的环节&#xff0c;它不仅涉及到物品的存储和分发&#xff0c;还与企业的成本控制和运营流程紧密相关。然而&#xff0c;管理仓库配件是一项…

[概述] 获取点云数据的仪器

这里所说的获取点云的仪器指的是可以获取场景中物体距离信息的相关设备&#xff0c;下面分别从测距原理以及适用场景来进行介绍。 一、三角测距法 三角测距原理 就是利用三角形的几何关系来测量物体的距离。想象一下&#xff0c;你站在一个地方&#xff0c;你的朋友站在另一…

Redis 分片集群

目录 ​编辑一、搭建分片集群 1、集群结构 ​编辑 2、准备实例和配置 3、启动 4、创建集群 二、散列插槽 三、集群伸缩 四、故障转移 1、自动故障转移 2、手动故障转移 五、RedisTemplate 访问分片集群 一、搭建分片集群 1、集群结构 主从和哨兵可以解决高可用、高…

如何将苹果手机照片导出?教你3个导出照片的必备技巧!

照片是我们记录生活&#xff0c;以及留下美好瞬间的最佳方式之一。通过手机照片&#xff0c;我们可以随时随地回忆过去的点点滴滴&#xff0c;还能将其分享给朋友和家人。因此&#xff0c;照片对于大家来说具有不可替代的价值与意义。 为了防止手机照片丢失&#xff0c;部分小…

多路IO—POll函数,epoll服务器开发流程

引言 "在计算机网络编程中&#xff0c;多路IO技术是非常常见的一种技术。其中&#xff0c;Poll函数和Epoll函数是最为常用的两种多路IO技术。这两种技术可以帮助服务器端处理多个客户端的并发请求&#xff0c;提高了服务器的性能。本文将介绍Poll和Epoll函数的使用方法&am…

算法通关村第四关-黄金挑战基础计算器问题

大家好我是苏麟 , 今天带来栈的比较难的问题 . 计算器问题 基础计算器 LeetCode 224 描述 : 给你一个字符串表达式 s &#xff0c;请你实现一个基本计算器来计算并返回它的值。 s 由数字、、-、(、)、和 组成s 表示一个有效的表达式 不能用作一元运算(例如&#xff0c; …

高效处理异常值的算法:One-class SVM模型的自动化方案

一、引言 数据清洗和异常值处理在数据分析和机器学习任务中扮演着关键的角色。清洗数据可以提高数据质量&#xff0c;消除噪声和错误&#xff0c;从而确保后续分析和建模的准确性和可靠性。而异常值则可能对数据分析结果产生严重影响&#xff0c;导致误导性的结论和决策。因此&…

C++类和对象下(初始化列表,静态成员,explicit关键字,友元)

C类和对象下[初始化列表,静态成员,explicit关键字,友元] 一.初始化列表1.为什么会有初始化列表2.初始化列表的语法形式3.没有默认构造函数的自定义成员变量4.初始化列表是成员变量定义的地方5.初始化列表可以跟函数体内定义搭配使用6.初始化列表执行的顺序7.总结建议 二.静态成…

当风格遇上浴缸:浴室装饰的秘诀

浴室不再仅仅是个洗漱的地方&#xff0c;如今它们是室内设计的一部分&#xff0c;有时甚至是焦点。浴室的装饰风格可以塑造整个房间的氛围&#xff0c;而浴缸通常是浴室内最引人注目的元素之一。在这里&#xff0c;我们将简单探讨不同室内设计风格与浴缸如何融合&#xff0c;让…

PlantSimulation安装帮助文档端口被占用的解决办法

PlantSimulation安装帮助文档端口被占用的解决办法 从PlantSimulaiton&#xff08;TPS&#xff09;2201开始帮助文档开始使用在线&#xff0c;如果使用本地则需要安装本地文档服务器。但是在安装过程中你可能会遇到&#xff0c;5000断开被占用的情况。解决办法如下&#xff1a…

C# 发送邮件

1.安装 NuGet 包 2.代码如下 SendMailUtil using MimeKit; using Srm.CMER.Application.Contracts.CmerInfo; namespace Srm.Mail { public class SendMailUtil { public async static Task<string> SendEmail(SendEmialDto sendEmialDto,List<strin…

java APP自动化测试AppIum

一、前言 二、Appium环境搭建 2.1 JDK安装 2.2 Android SDK安装配置 2.3 模拟器安装及配置 2.4 Appium Desktop安装及使用 2.5 Appium配置连接模拟器 三、实战基本脚本编写 3.1 创建Maven项目并配置 3.2 简单Demo 四、写在最后 一、前言 随着移动互联网的发展&#xff0c;AP…

mac录屏快捷键指南,轻松录制屏幕内容!

“大家知道mac电脑有录屏快捷键吗&#xff0c;现在录屏不太方便&#xff0c;每次都花很多时间&#xff0c;要是有录屏快捷键&#xff0c;应该会快速很多&#xff0c;可是哪里都找不到&#xff0c;有人知道吗&#xff1f;帮帮我&#xff01;” 苹果的mac电脑以其精美的设计和卓…

Java使用pdfbox进行pdf和图片之间的转换

简介 pdfbox是Apache开源的一个项目,支持pdf文档操作功能。 官网地址: Apache PDFBox | A Java PDF Library 支持的功能如下图.引入依赖 <dependency><groupId>org.apache.pdfbox</groupId><artifactId>pdfbox-app</artifactId><version>…

AIGC|把Azure Open AI和Jira集成起来,实现智能化项目管理

目录 一、Jira与Azure OpenAI介绍 二、Jira和Azure OpenAI的REST API对接 三、利用Chats插件实现对话的过程 四、总结 一、Jira与Azure OpenAI介绍 Jira是一款由澳大利亚公司Atlassian开发的项目管理工具&#xff0c;主要用于追踪问题、管理需求、构建报告和管理故障等事项…

ruby语言怎么写个通用爬虫程序?

Ruby语言爬虫是指使用Ruby编写的网络爬虫程序&#xff0c;用于自动化地从互联网上获取数据。其中&#xff0c;CRawler是一个基于文本的小型地牢爬虫&#xff0c;它被设计为可扩展&#xff0c;所有游戏数据均通过JSON文件提供&#xff0c;程序仅处理游戏引擎。除此之外&#xff…

EasyExcel复杂表头数据导入

目录 表头示例导入代码数据导出 表头示例 导入代码 Overridepublic void importExcel(InputStream inputStream) {ItemExcelListener itemExcelListener new ItemExcelListener();EasyExcel.read(inputStream, ImportItem.class, itemExcelListener).headRowNumber(2).sheet()…

OSPF 高级特性3

一、OSPF安全特性 1、OSPF报文验证&#xff1a; 区域验证模式&#xff1a;在区域下配置一致的密码才能加入同一个区域。 [r3-ospf-1-area-0.0.0.0]authentication-mode md5 1 cipher 123456 接口验证模式&#xff1a;链路两端的接口必须配置一致的密码才能建立邻居关系 [r5-Gig…