数据结构——链式二叉树

前言:哈喽小伙伴们,上篇文章我们讲述了一个特殊的二叉树——使用数组实现的堆的基本知识之后呢,从这篇文章开始,我们就正式进入普通二叉树的介绍啦,二叉树真正的难点——递归,即将来临,小伙伴们注意不要掉队哦


目录

一.链式二叉树

二.遍历二叉树

三.二叉树的实现

1.二叉树的定义

2.创建二叉树节点

四.二叉树的操作

1.先序遍历

2.中序遍历

3.后序遍历

4.节点个数

递归分析

5.叶节点数

6.树的高度

7.第k层节点数

8.查找指定值节点

9.销毁二叉树

四.完整代码展示

1.BinaryTree.h

2.BinaryTree.c

3.test.c

五.总结


一.链式二叉树

在前边的文章中,我们已经了解到,二叉树可以有顺序存储和链式存储两种方式,在堆的文章中,我们讲解了顺序存储的完全二叉树,那么现在,我们一起来认识一下链式存储的普通二叉树

我们知道,二叉树的规则是,每个节点至多有两个子节点,而两个子节点及其后续的子节点组成的整体,又可以分别称为左右子树,如右图所示,1为根节点,2和3则一起构成左子树,4、5、6则构成右子树。 而这两个子树,同样可以看做是由一个根节点和左右子树构成的新树

所以,任何一个二叉树都可以被拆解为三部分:

  1. 根节点
  2. 左子树
  3. 右子树

由此看来,二叉树和递归离不开关系,后续二叉树的各种基本操作,也都是通过递归来实现的


二.遍历二叉树

 二叉树的遍历有三种方式:

1.前(先)序遍历:先遍历树的根节点,再遍历它的左子树,最后是右子树。

2.中序遍历:先遍历树的左子树,再遍历它的根节点,最后是右子树。

3.后序遍历:先遍历树的左子树,再遍历它的右子树,最后是根节点。

我们以这棵树为例:

前序遍历即为:1 2 3 4 5 6

但是这样写其实并不合理,因为我们是用链表来写二叉树的结构的,所以对于节点2来说,它并不是没有右子树,而是右子树是空树

同样的,3、5、6同样可以作为一颗树,不过它们的左右子树都是空树罢了

所以合理的遍历方式应该把空树也带上,我们这里用N表示,于是:

前序遍历:1    2    3    N    N    N    4    5    N    N    6    N    N,如果用图形来表示,如下:

每一个方框都可以看做是一个新的树,从左到右依次为:根,左,右。如此,我们便也能写出中序遍历和后序遍历:

中序遍历:N    3    N    2    N    1    N    5    N    4    N    6    N,图形如下:

后序遍历:N    N    3    N    2    N    N    5    N    N    6    4    1,图形如下:

了解了二叉树的基本架构之后,我们就开始来实现二叉树的各个功能啦。


三.二叉树的实现

1.二叉树的定义

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

二叉树的定义并不难写,需要数据变量,以及指向左子树和右子树的两个指针


2.创建二叉树节点

//创建树节点
TreeNode* CreateTreeNode(TreeNode* root,BTDataType x)
{TreeNode* tmp = (TreeNode*)malloc(sizeof(TreeNode));if (tmp == NULL){perror("CreateTree->malloc");exit(-1);}root = tmp;root->data = x;root->left = NULL;root->right = NULL;return root;
}

二叉树节点的创建也不难,起初我们需要将两个指针都指向NULL

为了方便下文对二叉树的操作进行讲解,我们手动创建一颗如下的二叉树:

	TreeNode root;TreeNode* node1 = CreateTreeNode(&root, 1);TreeNode* node2 = CreateTreeNode(&root, 2);TreeNode* node3 = CreateTreeNode(&root, 3);TreeNode* node4 = CreateTreeNode(&root, 4);TreeNode* node5 = CreateTreeNode(&root, 5);TreeNode* node6 = CreateTreeNode(&root, 6);node1->left = node2;node1->right = node4;node2->left = node3;node4->left = node5;node4->right = node6;

四.二叉树的操作

二叉树的操作,基本上都和递归紧密相连。

1.先序遍历

先序遍历,也就是按照:根,左子树,右子树的顺序遍历,下面我直接给出代码:

//先序遍历
void PrevOrder(TreeNode* root)
{if (root == NULL){printf("N ");return;}printf("%d ", root->data);//根PrevOrder(root->left);//左子树PrevOrder(root->right);//右子树
}

没错,代码就是这么简单。

想要按照我们上边所讲解的遍历方法进行二叉树的遍历,递归是最好的选择

我们来分析以下:

首先,如果我们遇到叶节点,那么他就没有左右子树,这时候我们再去递归调用它的左右子树时,就打印一个N,证明我们遇到了空节点

随后,我们按照根,左,右的顺序,先打印根节点的数据,再先后去递归打印它的左右子树的节点数据

递归确实是一个难以理解的重点知识,但是博主却有一个小妙招,可以分享给大家:

我们继续来看上边的代码,如果我去掉递归调用,那么我剩下的代码是:

void PrevOrder(TreeNode* root)
{if (root == NULL){printf("N ");return;}printf("%d ", root->data);//根
}

这样就可以看做是一个打印节点数据的代码

那么我在打印完根节点的数据之后,想打印它左子树的根节点数据,就把它左子树的地址传给这个函数,也就是:

    PrevOrder(root->left);//左子树

打印完左子树,再去打印右子树,于是就把右子树的地址传过去:

    PrevOrder(root->right);//右子树

 要记住的是,每当我们用递归调用时,都是一层一层的套用该函数直到遇到某个限制条件,到达最后一层时,才会终止当前的函数,并返回上一层函数,直到返回至第一层为止

来看结果:

而中序,后序遍历,就和上边是大差不差,只需要改变递归调用函数的顺序。


2.中序遍历

中序遍历的顺序为:左子树,根,右子树,所以:

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

先递归调用左子树,再打印根节点数据,再递归调用右子树

结果如下:


3.后序遍历

后序遍历的顺序为:左子树,右子树,根,所以:

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

在左右子树全部都递归调用之后,再打印根节点数据

结果如下:


4.节点个数

二叉树的节点个数该怎么统计呢???

有小伙伴会说,这简单啊,用个计数器,遍历的时候顺便计数不就好啦

这确实是一种方法,但是却不够简便,我们不妨来思考思考有没有更简单一点的方法:

二叉树的节点个数,是不是就等于它的根节点,加上它的左子树,右子树的节点个数

那我就能得出下边的代码:

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

结果如下:

 

简不简单就问你?有没有问题?

如果根节点为空,就说明是空树,节点个数为0,返回0

如果不是空树,那我就返回左子树的节点个数+右子树的节点个数 + 根节点(也就是1)

怎么样?有没有被递归给圈粉?递归是如此的奇妙。 


递归分析

递归问题的基本思想就是把大型的,复杂的问题拆解成多个子问题,简单的问题

以上述代码为例,我们要求出一个二叉树的节点个数,而这棵二叉树又可以拆解为一个一个的子二叉树我们将每个子树的节点个数统计出来,在整合起来,就得出了总的节点个数。 

当我们面对递归问题时,要做的就是找到递归的两个要点

  1. 终止条件
  2. 递归部分

拿上述代码为例,终止条件就是:

    if (root == NULL)
    {
        return 0;
    }

而递归部分就是:

    return TreeSize(root->left) + TreeSize(root->right) + 1;


5.叶节点数

叶节点也就是左右子树都为空的节点,那么叶节点个数该怎么求呢?

很显然,与上边的想法类似,也就是左子树的叶节点个数+右子树的叶节点个数

如此一来,我们便能写出代码:

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

当为空树时,自然没有叶节点,返回0

当根节点存在,且它的左右子树都为空时,说明它是叶节点,返回1

上述两种都不满足,则返回左右子树叶节点之和,实现递归

结果如下:


6.树的高度

二叉树的高度,也可以叫做深度、层数

2那么该如何求出二叉树的高度呢???

我们仍然利用上边的思想,把根和左右子树独立开,不难得出,求树的高度就可以变成求左右两棵子树的高度较大的那一个再 + 1

//树高度
int TreeHight(TreeNode* root)
{if (root == NULL){return 0;}int lefthight = TreeHight(root->left);int righthight = TreeHight(root->right);return lefthight > righthight ? lefthight + 1 : righthight + 1;
}

首先仍然是要判断是否为空树

然后我们要把左右子树的高度定义出来

最后我们使用三目运算符来实现比较左右子树的高度并进行返回。

结果如下:


7.第k层节点数

二叉树还有一种操作,那就是求其某一层的节点数,这该怎么求呢???

有一种写法是,分别求二叉树的前k层和前k-1层,再相减,但是这显然会非常麻烦

所以,我们仍然可以采用把我们上边的递归思想

求二叉树的第k层,可以等价为是求其左右子树的第k-1层节点数之和

//第k层的节点个数
int LeveKSize(TreeNode* root,int k)
{assert(k > 0);if (root == NULL){return 0;}if (k == 1){return 1;}return LeveKSize(root->left, k - 1) + LeveKSize(root->right, k - 1);
}

当k = 1时,即第一层,只有一个根节点,返回1,反之就返回其左右子树的第k-1层节点数之和

来看结果:

首先是第三层,有3个节点;

 然后是第四层,没有节点:


8.查找指定值节点

对于二叉树,同样有查找给定的值的节点的操作,并返回它的地址,这又该如何实现呢???

很明显,如果这个值不是根节点,就要让左右子树去分别查找

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

首先判断空树

然后判断根节点的值是否为要查找的数据是就直接返回根节点地址,反之就开始查找左右子树

我们先查找左子树,这里要注意一点,要临时定义一个指针变量来判断左子树中是否能找到节点

如果存在,就会返回其地址,不存在,ret就为空,这样我们就接着去查找右子树


9.销毁二叉树

二叉树该如何销毁呢???

从根节点开始一个一个遍历销毁吗???但是这样每销毁一个根节点,我们都要先去记录它的两个左右子树的地址,未免有点太麻烦了些

既然不能从上到下,那我们就从下到上呗,不要忘了,还有后序遍历呢。

//销毁树
void TreeDestroy(TreeNode* root)
{if (root == NULL)return;TreeDestroy(root->left);TreeDestroy(root->right);free(root);
}

四.完整代码展示

1.BinaryTree.h

#include<stdio.h>
#include<stdlib.h>
#include<string.h>
#include<assert.h>typedef int BTDataType;typedef struct TreeNode
{BTDataType data;struct TreeNode* left;struct TreeNode* right;
}TreeNode;//创建树
TreeNode* CreateTreeNode(TreeNode* root,BTDataType x);
//销毁树
void TreeDestroy(TreeNode* root);
//先序遍历
void PrevOrder(TreeNode* root);
//中序遍历
void InOrder(TreeNode* root);
//后序遍历
void PostOrder(TreeNode* root);
// 层序遍历
void LevelOrder(TreeNode* root);
//节点个数
int TreeSize(TreeNode* root);
//叶节点个数
int TreeLeafSize(TreeNode* root);
//树高度
int TreeHight(TreeNode* root);
//第k层的节点个数
int LeveKSize(TreeNode* root,int k);
// 二叉树查找值为x的节点
TreeNode* BinaryTreeFind(TreeNode* root, BTDataType x);

2.BinaryTree.c

#include "BinaryTree.h"//创建树节点
TreeNode* CreateTreeNode(TreeNode* root,BTDataType x)
{TreeNode* tmp = (TreeNode*)malloc(sizeof(TreeNode));if (tmp == NULL){perror("CreateTree->malloc");exit(-1);}root = tmp;root->data = x;	root->left = NULL;root->right = NULL;return root;
}
//销毁树
void TreeDestroy(TreeNode* root)
{if (root == NULL)return;TreeDestroy(root->left);TreeDestroy(root->right);free(root);
}
//先序遍历
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);//根
}
//节点个数
int TreeSize(TreeNode* root)
{if (root == NULL){return 0;}return TreeSize(root->left) + TreeSize(root->right) + 1;
}
//叶节点个数
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);
}
//树高度
int TreeHight(TreeNode* root)
{if (root == NULL){return 0;}int lefthight = TreeHight(root->left);int righthight = TreeHight(root->right);return lefthight > righthight ? lefthight + 1 : righthight + 1;
}
//第k层的节点个数
int LeveKSize(TreeNode* root,int k)
{assert(k > 0);if (root == NULL){return 0;}if (k == 1){return 1;}return LeveKSize(root->left, k - 1) + LeveKSize(root->right, k - 1);
}
// 二叉树查找值为x的节点
TreeNode* BinaryTreeFind(TreeNode* root, BTDataType x)
{if (root == NULL)return NULL;if (root->data == x)return root;TreeNode* ret = BinaryTreeFind(root->left, x);if (ret)return ret;return BinaryTreeFind(root->right, x);
}

3.test.c

#include "BinaryTree.h"int main()
{TreeNode root;TreeNode* node1 = CreateTreeNode(&root, 1);TreeNode* node2 = CreateTreeNode(&root, 2);TreeNode* node3 = CreateTreeNode(&root, 3);TreeNode* node4 = CreateTreeNode(&root, 4);TreeNode* node5 = CreateTreeNode(&root, 5);TreeNode* node6 = CreateTreeNode(&root, 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");//int Treesize = TreeSize(node1);//printf("Treesize = %d\n", Treesize);//int TreeLeafsize = TreeLeafSize(node1);//printf("TreeLeafsize = %d\n", TreeLeafsize);//int Treehight = TreeHight(node1);//printf("Treehight = %d\n", Treehight);int LeveKsize = LeveKSize(node1, 4);printf("LeveKsize = %d\n", LeveKsize);TreeDestroy(node1);node1 = NULL;return 0;
}

五.总结

二叉树的基本知识和操作到这里就结束啦,二叉树与递归关系颇深。

虽然博主对于递归讲解的也不是那么清晰透彻,但是递归的知识真的是要靠自己一步一步的去理解,去深入。

希望小伙伴们都可以努力去拿下递归,这是每一个优秀程序员都必须要克服的!!!

最后还是求一求一键三连!!!

我们下期再见啦!

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

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

相关文章

unity旋转选中效果

代码和预制体 函数PlayAnim&#xff08;&#xff09;中的角度要根据按钮数量手动填好 using System; using DG.Tweening; using DG.Tweening.Core; using DG.Tweening.Plugins.Options; using UnityEngine;// Token: 0x0200001B RID: 27 public class BtnParentScript : Base…

QT5.4.1无法打开文件

问题描述&#xff1a;起初是在QT代码中运行打开文件代码&#xff1a; QString gFilename QFileDialog::getOpenFileName(this,"open File",path,"*", nullptr,QFileDialog::DontUseNativeDialog);时&#xff0c;出现了堵塞情况&#xff0c;经过多次实验一…

海鹰数据虾皮:为Shopee卖家提供的完美数据分析工具

在如今的电子商务领域中&#xff0c;如何更好地了解市场动态、优化商品策略以提高运营效果成为了卖家们关注的重要问题。而海鹰数据&#xff08;Haiying Data&#xff09;作为一款专为Shopee平台设计的数据分析工具&#xff0c;为卖家们提供了市场趋势、商品分析、关键词优化、…

改善男性生理健康,缓解前列腺问题

前列腺是男性生殖系统中非常重要的一部分&#xff0c;它对男性的生理健康都着至关重要的作用。但是随着年龄的增长&#xff0c;前列腺问题也越来越普遍&#xff0c;给男性带来了很大的困扰。前列宝是Mission Hill推出的一款番茄红素多效复合胶囊&#xff0c;主要针对男性前列腺…

11 款顶级的免费 iPhone 数据恢复软件

iPhone 拥有巨大的存储容量。您可以在 iPhone 设备上存储图像、文档和视频等数据。有时&#xff0c;您的 iPhone 会发生许多意外事件&#xff0c;例如意外删除&#xff0c;从而导致数据丢失。这里有 11 个最好的免费 iPhone 数据恢复软件&#xff0c;您可以免费下载&#xff0c…

喜报!MIAOYUN入选成都高新区“瞪羚企业”

近日&#xff0c;成都高新区科技创新局公示了《2022年领先科技园区政策拟支持企业&#xff08;机构&#xff09;名单&#xff08;第二批次&#xff09;》&#xff0c;即最新一批的高新区“瞪羚企业”名单。成都元来云志科技有限公司&#xff08;简称“MIAOYUN”&#xff09;以其…

JVM 类加载机制(七)

1.加载&#xff0c;验证&#xff0c;准备&#xff0c;解析&#xff0c;初始化 ​ JVM 类加载机制分为五个部分&#xff1a;加载&#xff0c;验证&#xff0c;准备&#xff0c;解析&#xff0c;初始化&#xff0c;下面我们就分别来看一下这五个过程。 1.1. 加载 ​ 加载是类加…

Spring Cloud Stream 4.0.4 rabbitmq 发送消息多function

使用 idea 创建 Springboot 项目 添加 Spring cloud stream 和 rabbitmq 依赖 pom文件 <?xml version"1.0" encoding"UTF-8"?> <project xmlns"http://maven.apache.org/POM/4.0.0" xmlns:xsi"http://www.w3.org/2001/XMLSchem…

学生信息管理系统

摘 要 学生成绩管理系统是典型的信息管理系统(MIS)&#xff0c;其开发主要包括后台数据库的建立和维护以及前端应用程序的开发两个方面。经过分析&#xff0c;我们使用Microsoft公司的C语言开发工具&#xff0c;将与C语言技术与数据库SQL2008相结合进行设计。首先&#xff0c;…

【多线程】线程的三种常见创建方式

文章目录 线程创建方式1——Thread线程创建方式2——Runnable线程创建方式2——匿名内部类线程创建方式3——Callable、FutureTask,带返回值 线程其实是程序中的一条执行路径。 那怎样的程序才是多线程程序呢&#xff1f; 例如12306网站就是支持多线程的&#xff0c;因为同时可…

一下午终于配好:如何用vs code连接远程主机jupyter server(notebook/lab)

因为教研室的机器有2060&#xff0c;笔记本只有集成显卡&#xff0c;虽然也可用浏览器访问&#xff0c;但是vs code不论从界面还是扩展功能来说&#xff0c;都有更好的编程体验&#xff0c;所以想通过vs code远程连接jupyter server。 要实现该需求总体需要三个步骤&#xff1…

Python推导式详细讲解

更多资料获取 &#x1f4da; 个人网站&#xff1a;ipengtao.com 在Python中&#xff0c;推导式是一种简洁而强大的语法特性&#xff0c;可以用来创建列表、集合、字典等数据结构。本文将深入探讨Python中的三种推导式&#xff1a;列表推导式、集合推导式和字典推导式&#xff…

喜讯:麦田(苏州)医学科技有限公司立项项目获得2024年度浙江省医药卫生科技计划资助的公告

喜讯&#xff1a;麦田&#xff08;苏州&#xff09;医学科技有限公司立项项目获得2024年度浙江省医药卫生科技计划资助的公告 我们麦田&#xff08;苏州&#xff09;医学科技有限公司非常荣幸地宣布&#xff0c;由我们联合浙江省人民医院、杭州市红十字会医院、杭州师范大学共同…

Docker-多容器应用

一、概述 到目前为止&#xff0c;你一直在使用单个容器应用。但是&#xff0c;现在您将 MySQL 添加到 应用程序堆栈。经常会出现以下问题 - “MySQL将在哪里运行&#xff1f;将其安装在同一个 容器还是单独运行&#xff1f;一般来说&#xff0c;每个容器都应该做一件事&#x…

强化学习——简单解释

一、说明 最近 OpenAI 上关于 Q-star 的热议激起了我温习强化学习知识的兴趣。这是为强化学习 (RL) 新手提供的复习内容。 二、强化学习的定义 强化学习是人类和其他动物用来学习的学习类型。即&#xff0c;通过阅读房间来学习。&#xff08;从反馈中学习&#xff09;。让我解…

基于深度学习CRNN的水表读数识别系统

1.研究背景与意义 项目参考AAAI Association for the Advancement of Artificial Intelligence 研究背景与意义 随着科技的不断发展&#xff0c;深度学习技术在各个领域都取得了显著的成果。其中&#xff0c;基于深度学习的图像识别技术在计算机视觉领域具有重要的应用价值。…

Linux环境搭建(Ubuntu22.04)+ 配置共享文件夹(Samba)

Linux开发环境准备 搭建Linux开发环境所需要的软件如下&#xff1a; VMware虚拟机&#xff1a;用于运行Linux操作系统的虚拟机软件之一&#xff0c;VMware下载安装在文章中不做说明&#xff0c;可自行百度谢谢Ubuntu光盘镜像&#xff1a;用于源代码编译&#xff0c;有闲置计算…

maven生命周期回顾

目录 文章目录 **目录**两种最常用打包方法&#xff1a;生命周期&#xff1a; 两种最常用打包方法&#xff1a; 1.先 clean&#xff0c;然后 package2.先 clean&#xff0c;然后install 生命周期&#xff1a; 根据maven生命周期&#xff0c;当你执行mvn install时&#xff0c…

大数据Hadoop-HDFS_架构、读写流程

大数据Hadoop-HDFS 基本系统架构 HDFS架构包含三个部分&#xff1a;NameNode&#xff0c;DataNode&#xff0c;Client。 NameNode&#xff1a;NameNode用于存储、生成文件系统的元数据。运行一个实例。 DataNode&#xff1a;DataNode用于存储实际的数据&#xff0c;将自己管理…

Python (二) 读写excel文件

程序员的公众号&#xff1a;源1024&#xff0c;获取更多资料&#xff0c;无加密无套路&#xff01; 最近整理了一波电子书籍资料&#xff0c;包含《Effective Java中文版 第2版》《深入JAVA虚拟机》&#xff0c;《重构改善既有代码设计》&#xff0c;《MySQL高性能-第3版》&…