数据结构:二叉树的链式结构及相关算法详解

目录

一.链式结构的实现

1.二叉树结点基本结构,初始化与销毁:

二.链式结构二叉树的几种遍历算法

1.几种算法的简单区分:

2.前序遍历:

3.中序遍历:

4.后序遍历:

5.层序遍历(广度优先遍历BFS):

三.链式二叉树的几种使用

1.计算树的结点个数:

2.计算树的叶子结点个数:

3.计算树的第k层结点个数:

4.计算树的深度:

5.查找结点为X的结点:

6.判断二叉树是否为完全二叉树:


一.链式结构的实现

1.二叉树结点基本结构,初始化与销毁:

(1)⽤链表来表示⼀棵⼆叉树,即⽤链来指示元素的逻辑关系。通常的⽅法是链表中每个结点由三个域组成,数据域左右指针域,左右指针分别⽤来给出该结点左孩子和右孩子所在的链结点的存储地址, 其结构如下:

(2)二叉树结点的初始化与树的创建:

#include"Tree.h"
//结点的初始化
BTNode* buyNode(char x)
{BTNode* node = (BTNode*)malloc(sizeof(BTNode));node->data = x;node->left = node->right = NULL;return node;
}
//树结构的创建
BTNode* creatBinaryTree()
{BTNode* nodeA = buyNode('A');BTNode* nodeB = buyNode('B');BTNode* nodeC = buyNode('C');BTNode* nodeD = buyNode('D');BTNode* nodeE = buyNode('E');BTNode* nodeF = buyNode('F');nodeA->left = nodeB;nodeA->right = nodeC;nodeB->left = nodeD;nodeC->right = nodeE;nodeC->right = nodeF;
}

(3)链式二叉树的销毁:

(使用后序遍历,我在下文会写出具体操作)

void BinaryTreeDestory(BTNode** root)
{//这里因为要改变根节点,应该传入的是根节点的地址,所以得拿二级指针接收//递归出口if ((*root) == NULL){return;}//自叶向根方向的释放//如果先释放的话,就找不到叶子节点了BinaryTreeDestory(&((*root)->left));BinaryTreeDestory(&((*root)->right));free(*root);*root = NULL;
}


二.链式结构二叉树的几种遍历算法

1.几种算法的简单区分:

举一个普遍的例子具体说明一下,如图:

2.前序遍历:

简单说明一下前序遍历的基本逻辑:

假设一个树:   

    A
   / \
  B   C
 / \
D   E

那么其遍历过程为:
访问根节点  A 。
递归遍历左子树(以  B  为根):
访问  B 
递归遍历左子树(以  D  为根):
访问  D 
递归遍历右子树(以  E  为根):
访问  E 
递归遍历右子树(以  C  为根):
访问  C 

//前序遍历
void preOrder(BTNode* root)
{//头 左 右//递归出口if (root == NULL){printf("NULL");return;}printf("%c",root->data);preOrder(root->left);preOrder(root->right);
}

3.中序遍历:

void inOrder(BTNode* root)
{//左 头 右//递归出口if (root == NULL){printf("NULL");return;}inOrder(root->left); //注意别调用错了,调用中序的printf("%c", root->data);inOrder(root->right);
}

4.后序遍历:

void postOrder(BTNode* root)
{//递归出口if (root == NULL){printf("NULL");return;}postOrder(root->left);postOrder(root->right);printf("%c", root->data);
}

总结:如上述所示,前中后序遍历的代码共同点也是相当明显了,主要就是递归顺序左右子树的顺序不同而影响的代码输出顺序不同,其根本上来说就是函数栈帧的不断进行嵌套式的创建与销毁,如果挨个遍历可能会显得比较复杂,但通过代码所示的这种递归算法,前中后序的遍历实现也显得十分简洁明了

但还有一点尤其需要注意:就是不同于层序遍历,我上面所说的三种遍历都属于深度优先遍历(DFS),而层序遍历却属于广度优先遍历,关于这两大类遍历的优劣我会在实现层序遍历后作详细介绍

5.层序遍历(广度优先遍历BFS):

思路:

借助数据结构:队列,先通过根节点入队列,再循环判断队列是否为空,不为空则取队头然后出队头,并将队头结点的左右孩子入队列

(由于后续层序遍历的实现需要用到好些队列的知识,所有我先将队列的一些简单用法附在下面,需要的可以稍微看看)

//定义结点结构
typedef int QDataTpe;
typedef struct QueueNode
{struct QueneNode* next;QDataTpe data;
}QueueNode;//定义队列的结构
typedef struct Queue
{QueueNode* phead;//队头QueueNode* ptail;//队尾int size;
}Queue;BTNode* buyNode(char x);//队列初始化
void QueueInit(Queue* pq)
{assert(pq);pq->phead = pq->ptail = NULL;
}//入队(从队尾入)
void QueuePush(Queue* pq, QDataTpe x)
{assert(pq);//申请一个结点QueueNode* newnode = (QueueNode*)malloc(sizeof(QueueNode));if (newnode == NULL){perror("malloc failed");exit(1);}newnode->data = x;newnode->next = NULL;//队列为空,newnode是对头也是队尾if (pq->phead == NULL){pq->phead = pq->ptail = newnode;}else //队列非空,尾插{pq->ptail->next = newnode;pq->ptail = pq->ptail->next;}pq->size++;
}
//判断队列是否为空
bool QueueEmpty(Queue* pq)
{assert(pq);return pq->phead == 0;
}//出队(从队头出)
void QueuePop(Queue* pq)
{assert(!QueueEmpty(pq));//只有一个结点的情况下,要把队尾和队头的两个指针都考虑到if (pq->phead == pq->ptail){free(pq->ptail);pq->phead = pq->ptail = NULL;}QueueNode* next = pq->phead->next;free(pq->phead);pq->phead = next;  pq->size--;
}//取队头数据
QDataTpe QueueFront(Queue* pq)
{assert(!QueueEmpty(pq));return pq->phead->data;
}
//取队尾数据
QDataTpe QueueBack(Queue* pq)
{assert(!QueueEmpty(pq));return pq->ptail->data;
}//队列的销毁
void QueueDestory(Queue* pq)
{assert(pq);QueueNode* pcur = pq->phead;while (pcur){QueueNode* next = pcur->next;free(pcur);pcur = next;}pq->phead = pq->ptail = NULL;
}//取队列元素个数
int QueueSize(Queue* pq)
{return pq->size;
}
//层序遍历
void LeveIOrder(BTNode* root)
{Queue q;//创建一个队列QueueInit(&q);QueuePush(&q, root);while (!QueueEmpty(&q)){//取队头出队头,打印结点值BTNode* top = QueueFront(&q);QueuePop(&q);printf("%c", top->data);//将队头结点的非空左右孩子结点入队列if (top->left){QueuePush(&q, top->left);}if (top->right){QueuePush(&q, top->right);}QueuDestroy(&q);}}

小结: 
DFS 优点(深度优先遍历)
节点少时效率高,节省内存
适合需要“全探索”或“找到任意解即可”的场景(如迷宫路径)

 
DFS 缺点
可能陷入无限循环(需记录已访问节点)
不保证最短路径(除非剪枝优化)


BFS 优点 (广度优先遍历)
保证找到最短路径(无权图中)
层级分明,易于理解

 
BFS 缺点
内存占用大(需存储整个层级节点)
不适合大规模数据或深层结构(如树深度极大)


三.链式二叉树的几种使用

1.计算树的结点个数:

法一:把size作为一个函数的形参,然后把这个树遍历一遍,每遍历一个节点就size(节点个数)加一,但需要注意的是,需要传入size的地址才能改变size的值

void BinaryTreeSize(BTNode* root,int* size)
{if (root == NULL){return;}(*size)++;BinaryTreeSize(root->left,size);BinaryTreeSize(root->right, size);
}

法二:递归,  节点个数=左子树节点个数+右子树节点个数,所以我们以此为基础递归就可以了

int BinaryTreeSize(BTNode* root)
{//节点个数=左子树节点个数+右子树节点个数//递归出口if (root == NULL){return 0;}return 1 + BinaryTreeSize(root->left) + BinaryTreeSize(root->right);
}

2.计算树的叶子结点个数:

叶子结点:即没有左右孩子结点的结点4

int BinaryTreeLeafSize(BTNode* root)
{//递归出口if (root == NULL){return 0;}if (root->left == NULL && root->right == NULL){return 1;}return BinaryTreeLeafSize(root->left) + BinaryTreeLeafSize(root->right);
}

3.计算树的第k层结点个数:

如上图,当k=1时就是最底层结点,因此从最底层往上嵌套遍历就可以实现

int BinaryTreeLeafSize(BTNode* root)
{//递归出口if (root == NULL){return 0;}if (root->left == NULL && root->right == NULL){return 1;}return BinaryTreeLeafSize(root->left) + BinaryTreeLeafSize(root->right);
}

4.计算树的深度:

int BinaryTreeDeep(BTNode* root)
{//计算树的深度if (root == NULL){return 0;}int lefDep = BinaryTreeDeep(root->left);int rigDep = BinaryTreeDeep(root->right);return 1 + (lefDep > rigDep ? lefDep : rigDep);
}

5.查找结点为X的结点:

BTNode* BinaryTreeFind(BTNode* root, BTDataType x)
{//递归出口if (root == NULL){return 0;}if (root->data == x){return root;}//代码走到这里证明根节点并不是我们要找的结点//接下来是左右子树各自分开递归搜查BTNode* left = BinaryTreeFind(root->left, x);if (left)//由于函数最后返回NULL,所以如果这个if条件可以进入就足以说明找到了需要的结点{return root;}BTNode* right = BinaryTreeFind(root->right, x);if (right){return root;}return NULL;
}

6.判断二叉树是否为完全二叉树:

// 判断⼆叉树是否是完全⼆叉树
bool BinaryTreeComplete(BTNode* root)
{Queue q;QueueInit(&q);QueuePush(&q, root);while (!QueueEmpty(&q)){//取队头,出队头BTNode* top = QueueFront(&q);QueuePop(&q);if (top == NULL){break;}//队头结点的左右孩子入队列QueuePush(&q, top->left);QueuePush(&q, top->right);}//队列不为空,继续取队头出队头//1)队头存在非空结点----非完全二叉树//2)队头不存在非空结点----完全二叉树while (!QueueEmpty(&q)){BTNode* top = QueueFront(&q);QueuePop(&q);if (top != NULL){QueueDestroy(&q);return false;}}QueueDestroy(&q);return true;
}

第一个while是为了验证根结点,如果根结点为空,直接返回false,如果根结点不为空,就将树里的结点循环入队列然后继续将子结点入队列并且判断其是否为空,同样的第一次循环的的结束条件是当出现空为止,因此,当来到第二次循环时,树的结点里说明已经第一次循环出了空结点,而当树是完全二叉树时,第二次循环之后队列里应该全是空结点,因此,只要当第二次循环里出现非空结点时,就可以判断其时非完全二叉树(这题思路比较复杂,可以多想一会)

欧克,本次关于链式二叉树的知识点就到此为止了,相关的题目我也会在未来附上

ok,全文终

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

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

相关文章

WebRTC与PJSIP:呼叫中心系统技术选型指南

助力企业构建高效、灵活的通信解决方案 在数字化时代,呼叫中心系统的技术选型直接影响客户服务效率和业务扩展能力。WebRTC与PJSIP作为两大主流通信技术,各有其核心优势与适用场景。本文从功能、成本、开发门槛等维度为您深度解析,助您精准匹…

cuda-12.4.0 devel docker 中源码安装 OpenAI triton

1,准备 docker 容器 下载docker image: $ sudo docker pull nvidia/cuda:12.6.2-devel-ubuntu20.04 创建容器: sudo docker run --gpus all -it --name cuda_LHL_01 -v /home/hongleili/ex_triton/tmp1:/root/ex_triton/tmp1 nvidia/cuda:12.6…

Zookeeper(67) Zookeeper在HBase中的应用是什么?

Zookeeper 在 HBase 中起到了至关重要的作用,主要用于协调和管理 HBase 集群中的多个组件。具体来说,Zookeeper 在 HBase 中的应用包括以下几个方面: Master 选举:HBase 集群中可以有多个 Master 节点,但只有一个处于…

React antd的datePicker自定义,封装成组件

一、antd的datePicker自定义 需求:用户需要为日期选择器的每个日期单元格添加一个Tooltip,当鼠标悬停时显示日期、可兑换流量余额和本公会可兑流量。这些数据需要从接口获取。我需要结合之前的代码,确保Tooltip正确显示,并且数据…

由 JRE 软链接依赖引发的故障排查

最近工作查到了一个有意思的问题,在这里记录一下。 1. 问题背景 Java应用 SAP 系统自带了一个 jre,但在Ubutnu2004上部分功能不正常。 初步排查发现,该 JRE 版本来源不明(用户也说不清是哪里来的)。 于是尝试以下方…

2-3文件的属性信息

文章目录 1 file命令2 stat命令 1 file命令 用来识别文件类型 # 参数的位置是任意的 file 文件名 [参数]-b 只显示文件类型和文件编码,不显示文件名-i 显示文件的MIME类型-F 设置输出字符串的分隔符-L 查看软链接文件自身文件属性liyblyb:/tmp$ file xxxtmp.log …

JavaScript 系列之:垃圾回收机制

前言 垃圾回收是一种自动内存管理机制,用于检测和清除不再使用的对象,以释放内存空间。当一个对象不再被引用时,垃圾回收器会将其标记为垃圾,然后在适当的时候清除这些垃圾对象,并将内存回收给系统以供其他对象使用。…

(七)趣学设计模式 之 适配器模式!

目录 一、 啥是适配器模式?二、 为什么要用适配器模式?三、 适配器模式的实现方式1. 类适配器模式(继承插座 👨‍👩‍👧‍👦)2. 对象适配器模式(插座转换器 &#x1f50c…

【Java基础】Java中new一个对象时,JVM到底做了什么?

Java中new一个对象时,JVM到底做了什么? 在Java编程中,new关键字是我们创建对象的最常用方式。但你是否想过,当你写下new MyClass()时,Java虚拟机(JVM)到底在背后做了哪些工作?今天&…

内网穿透:打破网络限制的利器

目录 深入理解内网穿透 内网与外网的奥秘 内网穿透的原理剖析 总结与展望 在如今这个数字化时代,网络已经成为我们生活和工作中不可或缺的一部分。但你是否遇到过这样的困扰:在家办公时,想要访问公司内部的文件服务器,却因为网…

【汽车ECU电控数据管理篇】HEX文件格式解析篇章

一、HEX格式文件是啥 HEX 文件是 Intel 公司提出的一种按地址排列的数据信息格式,通常用于存储嵌入式系统的二进制代码。它以 ASCII 码的形式记录数据,每一行以冒号开头,包含数据长度、地址、记录类型、数据和校验码等信息。HEX 文件常用于程…

深入理解 CSS pointer-events: none:穿透点击的魔法

一、什么是 pointer-events: none? pointer-events: none 是一个强大的 CSS 属性,它控制元素是否响应鼠标/触摸事件(如点击、悬停、拖拽)。当设置为 none 时,元素会变得“透明”,事件会直接穿透到下方的元…

【AHK】资源管理器自动化办公实例/自动连点设置

此处为一个自动连续点击打开检查的自动化操作案例,没有quicker的鼠键录制,不常用了,做个备份 #MaxThreadsPerHotkey 2 ; 这个是核心!!!!确保可以同时运行多个热键或标签global isRunning : tru…

html css js网页制作成品——HTML+CSS甜品店网页设计(5页)附源码

目录 一、👨‍🎓网站题目 二、✍️网站描述 三、📚网站介绍 四、🌐网站效果 五、🪓 代码实现 🧱HTML 六、🥇 如何让学习不再盲目 七、🎁更多干货 一、👨‍&#x1f…

Springboot使用Milvus的基本操作

Milvus 先得保证数据的正确安装并且正确运行 <dependency><groupId>com.google.code.gson</groupId><artifactId>gson</artifactId> </dependency> <dependency><groupId>io.milvus</groupId><artifactId>milvu…

初阶数据结构(C语言实现)——3顺序表和链表(2)

2.3 数组相关面试题 原地移除数组中所有的元素val&#xff0c;要求时间复杂度为O(N)&#xff0c;空间复杂度为O(1)。OJ链接 力扣OJ链接-移除元素删除排序数组中的重复项。力扣OJ链接-删除有序数组中的重复项合并两个有序数组。力扣OJ链接-合并两个有序数组 2.3.1 移除元素 1…

【力扣】2619. 数组原型对象的最后一个元素——认识原型与原型链

【力扣】2619. 数组原型对象的最后一个元素——认识原型与原型链 文章目录 【力扣】2619. 数组原型对象的最后一个元素——认识原型与原型链题目解决方案概述全局上下文函数上下文事件处理程序构造函数上下文类上下文显式 / 隐式绑定绑定方法和永久 this 上下文 方法 1&#xf…

ubuntu终端指令集 shell编程基础(一)

磁盘指令 连接与查看&#xff1a;磁盘与 Ubuntu 有两种连接方式&#xff1b;使用ls /dev/sd*查看是否连接成功&#xff0c;通过df系列指令查看磁盘使用信息。若 U 盘已挂载&#xff0c;相关操作可能失败&#xff0c;需用umount取消挂载。磁盘操作&#xff1a;使用sudo fdisk 磁…

基于Spark的电商供应链系统的设计与实现

目录 1.研究背景与意义 2、国内外研究现状 3、相关理论与技术 &#xff08;一&#xff09;分布式计算系统Spark &#xff08;二&#xff09;数据仓库Hive &#xff08;三&#xff09;读取服务器本地磁盘的日志数据Flume &#xff08;四&#xff09;分布式消息队列Kafka …

使用TortoiseGit配合BeyondCompare实现在Git仓库中比对二进制文件

使用TortoiseGit的比对工具可以直接右键&#xff0c;点击选择比对和上一版本的变化差异&#xff1a; 但是TortoiseGit只能支持比对纯文本文件的变化差异&#xff0c;如果尝试比对二进制文件&#xff0c;会提示这不是一个有效的文本文件&#xff1a; BeyondCompare可以比对二进制…