【数据结构】二叉树———Lesson2
Hi~!这里是奋斗的小羊,很荣幸您能阅读我的文章,诚请评论指点,欢迎欢迎 ~~
💥💥个人主页:奋斗的小羊
💥💥所属专栏:C语言
🚀本系列文章为个人学习笔记,在这里撰写成文一为巩固知识,二为展示我的学习过程及理解。文笔、排版拙劣,望见谅。
目录
- 前言
- 一、TOP-K问题
- 二、二叉树的链式结构
- 2.1前中后序遍历
- 2.2节点个数
- 2.3叶子个数
- 2.4高度 / 深度
- 2.5第K层节点数
- 2.6查找值为x的节点
- 2.7二叉树销毁
- 2.8相关OJ题
- 2.9层序遍历
- 总结

前言
在TOP-K问题中有一种方法能在占用很小空间的情况下高效地找出最大或最小的前K个数。
在上篇文章介绍树时说树是递归定义的,因此二叉树的遍历、二叉树的搜索、二叉树的深度、高度、节点数、二叉树的路径求解等问题,基本都会用递归解决。
一、TOP-K问题
接上篇文章,我们简单地了解了TOP-K问题,介绍了如何从比较大的数据量中快速找出最大(最小)的前K个数据。
| 方法一:
用这些较大的数据量建堆,循环Top、Pop,找出最大(最小)的前K个数。
但是这个方法有个致命缺陷,它只适合数据量还不是特别大的情况,因为如果数据量非常大时我们还建堆的话,这对空间的消耗是很大的,那我们就要想别的办法了。如果数据海量,但我们现在只有1GB的内存,直接建堆显然行不通。
| 方法二:
将这海量数据分成合适的若干份分别建堆,找出每份中的最大(最小)的前K的数,再将这些数建堆,循环Top、Pop K次就能找到最大(最小)的前K个数。
但是这个方法也不是特别好,因为1GB的内存还是比较大的,假如这个问题非要搞我们,它有海量的数据但是只给我们1KB的内存,甚至更狠一点只给我们100Byte的空间,这时候方法二就显得力不从心了,因为这个若干份将会非常大,非常不理想。
| 方法三:
先从这海量数据中拿出前K个数建小堆(大堆),然后再不断拿出剩下的数和堆顶数据比较,如果大(小)于堆顶就替换掉堆顶,再向下调整保证堆成立,当这海量的数据全都比完后,留在堆内的数就是这海量数据中最大(最小)的前K个数。
这个方法需要注意的是如果要求我们找最大的前K个数要建小堆,最小的前K个数要建大堆。当然K也不能太大,要是我们现在可用的内存连这K个数都装不下那就有点扯淡了。
方法三代码如下:
void test1()
{FILE* pf = fopen("data.txt", "w");if (pf == NULL){perror("fopen fail");return;}//产生随机的100000个数存到磁盘中for (int i = 0; i < 100000; i++){//rand函数产生的随机数有重复,+i减少重复的数int ret = rand() + i;fprintf(pf, "%d\n", ret);}fclose(pf);pf = NULL;
}void test2()
{FILE* pf = fopen("data.txt", "r");if (pf == NULL){perror("fopen fail");return;}int k = 0;scanf("%d", &k);int* arr = (int*)malloc(k * sizeof(int));if (arr == NULL){perror("malloc fail");return;}//读取k个数到数组中for (int i = 0; i < k; i++){fscanf(pf, "%d", &arr[i]);}//k个数建小堆for (int i = (k - 1 - 1) / 2; i >= 0; i--){AdjustDown(arr, i, k);}//读取剩下的数与堆顶比较int ret = 0;while (fscanf(pf, "%d", &ret) > 0){if (ret > arr[0]){arr[0] = ret;AdjustDown(arr, 0, k);}}//留在堆内的数就是所有数中最大的前K个数for (int i = 0; i < k; i++){printf("%d ", arr[i]);}fclose(pf);pf = NULL;
}int main()
{srand((unsigned int)time(NULL));test1();test2();return 0;
}
这里又有个问题,我们怎么知道这K个数就是最大的前K个数呢?如何验证?
为了验证我们这个程序有没什么问题,这里有个简单的小方法,我们可以手动地在已经产生了100000个随机数的文件中修改K个使它们一定是最大的K个数,然后再运行程序看看是否有问题。运行前先把产生随机数的函数屏蔽掉。

可以看到此时打印出来的10个数就是我们故意放进去的最大的10个数。
二、二叉树的链式结构
在上篇文章中简单地了解了二叉树的链式存储,即用链表来表示一棵二叉树,用链表来指示元素的逻辑关系。
通常每个节点由三个域组成,一个数据域和两个指针域,分别用左指针和右指针来指向左孩子和右孩子。链式结构又分为二叉链和三叉链,当前我们学习的是二叉链,三叉链会在后面的学习中学到。
typedef int BTDataType;
//二叉链
typedef struct BinTreeNode
{struct BinTreeNode* pleft;//左孩子struct BinTreeNode* pright;//右孩子BTDataType data;
}BTNode;
二叉树的创建方式比较复杂,后续我们会深入学习,这里为了测试下面将要介绍的二叉树遍历,我们先手动创建一棵链式二叉树。
#define _CRT_SECURE_NO_WARNINGS#include <stdio.h>
#include <stdlib.h>typedef int BinTreeType;typedef struct BinaryTreeNode
{BinTreeType data;struct BinaryTreeNode* left;struct BinaryTreeNode* right;
}BTNode;BTNode* BuyNode(BinTreeType x)
{BTNode* node = (BTNode*)malloc(sizeof(BTNode));if (node == NULL){perror("malloc fail");return;}node->data = x;node->left = node->right = NULL;return node;
}BTNode* GreatBinaryTree()
{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;return node1;
}int main()
{BTNode* root = GreatBinaryTree();return 0;
}
2.1前中后序遍历

二叉树的操作离不开树的遍历,按照规则,二叉树的遍历有:前序、中序、后序(前根序、中根序、后根序)的递归结构遍历。
- 前序: 访问顺序为根节点、左子树、右子树
A B D N N N C E N N F N N
- 中序: 访问顺序为左子树、根节点、右子树
N D N B N A N E N C N F N
- 后序: 访问顺序为左子树、右子树、根节点
N N D N B N N E N N F C A
代码实现:
void PrevOrder(BTNode* root)
{if (root == NULL){printf("N ");return;}printf("%d ", root->data);PrevOrder(root->left);PrevOrder(root->right);
}void InOrder(BTNode* root)
{if (root == NULL){printf("N ");return;}InOrder(root->left);printf("%d ", root->data);InOrder(root->right);
}void PostOrder(BTNode* root)
{if (root == NULL){printf("N ");return;}PostOrder(root->left);PostOrder(root->right);printf("%d ", root->data);
}



2.2节点个数
如何计算节点的个数呢?可能有同学会想到用上面学到的前中后序遍历二叉树++计数:
int TreeSize(BTNode* root)
{int size = 0;if (root == NULL){printf("N ");return;}size++;printf("%d ", root->data);TreeSize(root->left);TreeSize(root->right);return size;
}
但这样是行不通的,因为上面我们前中后序遍历二叉树是递归实现的,每一次递归函数栈帧内都重新定义了size。
那可能又有同学说用static修饰size不就好了,但是这个方法也不太能行得通。
int TreeSize(BTNode* root)
{static int size = 0;if (root == NULL){printf("N ");return;}size++;printf("%d ", root->data);TreeSize(root->left);TreeSize(root->right);return size;
}

可以看到用static修饰后这个方法也只能计算一次,因为static修饰的变量在静态区,程序运行结束才销毁。
我们可以考虑用递归的思想解决这个问题。因为一个二叉树的节点个数是左子树节点个数+右子树节点个数+1(根节点),左子树的节点个数又是它的左子树节点个数+右子树节点个数+1(根节点),所以我们可以用递归解决这个问题。

递归计算节点数代码如下:
int TreeSize(BTNode* root)
{if (root == NULL){return 0;}return TreeSize(root->left) + TreeSize(root->right) + 1;
}
2.3叶子个数
如果节点的左指针和右指针都指向NULL,那这个节点就是叶子,如果节点为空就返回0。
int TreeLeafSize(BTNode* root)
{if (root == NULL){return 0;}if (root->left == NULL && root->right == NULL){return 1;}return TreeLeafSize(root->left) + TreeLeafSize(root->right);
}
2.4高度 / 深度
一个二叉树的高度是左子树高度和右子树高度大的一个再加一,左子树的高度又是它的左子树高度和右子树高度大的一个再加一,这显然又是一个递归问题。
int TreeHight(BTNode* root)
{ if (root == NULL){return 0;}int lefthight = TreeHight(root->left);int leftright = TreeHight(root->right);return lefthight > leftright ? lefthight + 1 : leftright + 1;
}
这里需要注意要用一个值来接收左右子树的高度,不要写成下面这种:
int TreeHight(BTNode* root)
{ if (root == NULL){return 0;}return TreeHight(root->left) > TreeHight(root->right) ? TreeHight(root->left) + 1 : TreeHight(root->right) + 1;
}
虽然下面这种看起来更简单,但是当二叉树的深度比较深时,这个代码的时间消耗是非常非常非常大的。
2.5第K层节点数
求第K层的节点数,就是相对于第二层来说求第K-1层节点数,相对于第三层来说求第K-2层节点数,也可以用递归解决,当节点不为空且K==1时返回1。
int TreeKSize(BTNode* root, int k)
{if (root == NULL){return 0;}if (k == 1){return 1;}return TreeKSize(root->left, k - 1) + TreeKSize(root->right, k - 1);
}
2.6查找值为x的节点
查找值为x的节点可以用前序遍历二叉树解决,当节点值等于x时返回节点指针,如果不等于则查找左子树,如果左子树找到了就返回节点指针,如果没找到(返回NULL)则查找右子树,不管找没找到都返回右子树的返回值。
BTNode* TreeFind(BTNode* root, BinTreeType x)
{if (root == NULL){return NULL;}if (root->data == x){return root;}BTNode* node = TreeFind(root->left, x);if (node)//如果为空则左子树没找到{return node;}return TreeFind(root->right, x);
}
2.7二叉树销毁
//二叉树销毁
void TreeDestroy(BTNode* root)
{assert(root);if (root == NULL){return;}TreeDestroy(root->left);TreeDestroy(root->right);free(root);
}
二叉树销毁函数调用完记得给root置NULL。
2.8相关OJ题
Leetcode—单值二叉树
bool isUnivalTree(struct TreeNode* root) {if (root == NULL){return true;}if (root->left && root->left->val != root->val){return false;}if (root->right && root->right->val != root->val){return false;}return isUnivalTree(root->left) && isUnivalTree(root->right);
}
Leetcode—相同的树
bool isSameTree(struct TreeNode* p, struct TreeNode* q) {if (p == NULL && q == NULL){return true;}if (p == NULL || q == NULL){return false;}if (p->val != q->val){return false;}return isSameTree(p->left, q->left) && isSameTree(p->right, q->right);
}
Leetcode—对称二叉树
bool _isSymmetric(struct TreeNode* p, struct TreeNode* q) {if (p && q){if (p->val != q->val){return false;}return _isSymmetric(p->left, q->right) && _isSymmetric(p->right, q->left);}if (p == q){return true;}return false;
}
bool isSymmetric(struct TreeNode* root) {if (root == NULL){return true;}return _isSymmetric(root->left, root->right);
}
Leetcode—二叉树的前序遍历
int TreeSize(struct TreeNode* root)
{if (root == NULL){return 0;}return TreeSize(root->left) + TreeSize(root->right) + 1;
}
void PreOrder(struct TreeNode* root, int* arr, int* pi)
{if (root == NULL){return;}//每次递归都会建立新的栈帧空间,不同的栈帧空间内相同的变量之间互不影响,//而我们需要的是每次函数递归都要改变下标,所以需要传地址。arr[(*pi)++] = root->val;PreOrder(root->left, arr, pi);PreOrder(root->right, arr, pi);
}
int* preorderTraversal(struct TreeNode* root, int* returnSize) {*returnSize = TreeSize(root);//在开辟空间前可以先算出节点个数以开辟合适的空间int* arr = (int*)malloc(*returnSize * sizeof(int));int i = 0;PreOrder(root, arr, &i);return arr;
}
函数每次递归都会建立独立的栈帧空间,同一个变量在不同的栈帧空间中互不影响,如果我们想让某一变量在每次函数递归都改变,则应该传变量地址。
Leetcode—另一棵树的子树
bool isSameTree(struct TreeNode* p, struct TreeNode* q)
{if (p == NULL && q == NULL){return true;}if (p == NULL || q == NULL){return false;}if (p->val != q->val){return false;}return isSameTree(p->left, q->left) && isSameTree(p->right, q->right);
}
bool isSubtree(struct TreeNode* root, struct TreeNode* subRoot){if (root == NULL){return false;}if (root->val == subRoot->val && isSameTree(root, subRoot)){return true;}return isSubtree(root->left, subRoot) || isSubtree(root->right, subRoot);
}
我们知道二叉树是由根节点和左右子树构成,因此我们可以先判断两个根节点是否相等,如果相等且左右子树也相等则两个二叉树互为子树;如果根节点不相等则递归判断左子树或右子树。
2.9层序遍历
顾名思义层序遍历就是一层一层的遍历二叉树,规则是将根节点插入队列中,取出根节点后将根节点的两个子节点(也就是第二层)带入队列中,取出左节点后又将左节点的两个子节点带入队列中,依次遍历完整个二叉树。
//层序遍历
void TreeLevelOrder(BTNode* root)
{assert(root);Que q;QueueInit(&q);if (root){QNodePush(&q, root);}while (!QNodeEmpty(&q)){BTNode* front = QNodeFront(&q);QNodePop(&q);printf("%d ", front->data);if (front->left){QNodePush(&q, front->left);}if (front->right){QNodePush(&q, front->right);}}QNodeDestroy(&q);
}
总结
- 二叉树由根节点、左子树和右子树组成,每个子树也是一个二叉树。递归方法很适合处理这种具有递归结构的数据结构,例如通过递归函数不断地遍历左右子树。递归的思想可以帮助我们分解复杂问题,将大问题转化为相同结构的小问题,从而简化解题过程。
相关文章:
【数据结构】二叉树———Lesson2
Hi~!这里是奋斗的小羊,很荣幸您能阅读我的文章,诚请评论指点,欢迎欢迎 ~~ 💥💥个人主页:奋斗的小羊 💥💥所属专栏:C语言 🚀本系列文章为个人学习…...
mongodb数据导出与导入
一、先去检查mongodump mongodump --version 如果报 mongodump version: built-without-version-string 或者其他的较老的版本,直接去下载最新的【传送门】 【以Ubuntu18.04为例】 安装工具 假设你下载的是 .tgz 文件(适用于 Linux 系统)&am…...
电路学习——经典运放电路之滞回比较器(施密特触发器)(2024.07.18)
参考链接1: 电子设计教程29:滞回比较器(施密特触发器) 参考链接2: 滞回比较器电路详细分析 参考链接3: 比较器精髓:施密特触发器,正反馈的妙用 参考链接4: 比较器反馈电阻选多大?理解滞后效应,轻…...
NVIDIA Container Toolkit 安装与配置帮助文档(Ubuntu,Docker)
NVIDIA Container Toolkit 安装与配置帮助文档(Ubuntu,Docker) 本文档详细介绍了在 Ubuntu Server 22.04 上使用 Docker 安装和配置 NVIDIA Container Toolkit 的过程。 概述 NVIDIA 容器工具包使用户能够构建和运行 GPU 加速容器。即可以在容器中使用NVIDIA显卡。 架构图如…...
JavaWeb day01-HTML入门
Web前端 课程安排 HTML、CSS简介 HTML快速入门 实现标题排版 新闻标题样式...
驱动框架——CMSIS第一部分 RTE驱动框架介绍
一、介绍CMISIS 什么是CMSIS(cortex microcontrol software interface standard一种软件标准接口),官网地址:https://arm-software.github.io/CMSIS_6/latest/General/index.html 包含的core、driver、RTOS、dsp、nn等部分&…...
Debezium日常分享系列之:Debezium2.7版本PostgreSQL数据库连接器
Debezium日常分享系列之:Debezium2.7版本PostgreSQL数据库连接器 一、概述二、连接器的工作原理安全快照初始快照的默认工作流程行为临时快照触发临时增量快照触发临时阻塞快照增量快照增量快照流程Debezium 如何解决具有相同主键的记录之间的冲突快照窗口触发增量快照具有附加…...
保障信息系统安全保护等级调整期间的安全性
保障信息系统安全保护等级调整期间的安全性: 策略与实践 在当今数字化时代,信息系统已成为企业和组织运营的核心支撑。为了适应不断变化的业务需求和安全威胁环境,信息系统安全保护等级的调整成为必要之举。然而,这一调整过程可能…...
实战:shell编程之全量命令练习
概叙 槽点~~~~~~~! 往期shell相关文章回顾,有兴趣的可以自行阅读和练习。 科普文:一文搞懂Vim-CSDN博客 科普文:jvm笔记-CSDN博客 科普文:一天学会shell编程-CSDN博客 科普文:Linux服务器巡检小结_lin…...
在 CentOS 7 上编译安装 Python 3.11
安装必要的依赖 首先,你需要安装一些开发工具和库,以便编译 Python 和 OpenSSL: yum -y groupinstall "Development tools" yum install -y wget gcc-c pcre pcre-devel zlib zlib-devel libffi-devel zlib1g-dev openssl-devel …...
Qt 4.8.7 + MSVC 中文乱码问题深入分析
此问题很常见,然而网上关于此问题的分析大多不够深刻,甚至有错误;加之Qt5又更改了一些编码策略,而很多文章并未提及版本问题,或是就算提了,读者也不重视。这些因素很容易让读者产生误导。今日我彻底研究透了…...
IDEA的常见代码模板的使用
《IDEA破解、配置、使用技巧与实战教程》系列文章目录 第一章 IDEA破解与HelloWorld的实战编写 第二章 IDEA的详细设置 第三章 IDEA的工程与模块管理 第四章 IDEA的常见代码模板的使用 第五章 IDEA中常用的快捷键 第六章 IDEA的断点调试(Debug) 第七章 …...
arcgis怎么选取某个指定区域地方的数据,比如从全国乡镇数据选取长沙市乡镇数据
一共5个步骤,没一句废话,耐心看完。看完你就会在任何软件选取指定范围的数据了。 一、如图,先将数据加载到arcgis里面,我们要选取里面长沙市的范围数据。 二、选取长沙市的语句 “市” like ‘长沙%’ 切记,切记&…...
二、链表(1)
203.移除链表元素 创建一个虚拟哨兵头节点,就不用考虑原本头结点要不要删除 # Definition for singly-linked list. # class ListNode: # def __init__(self, val0, nextNone): # self.val val # self.next next class Solution:def remove…...
KAFKA搭建教程
KAFKA搭建教程 期待您的关注 KAFKA学习笔记 帮助更多人 目录 KAFKA搭建教程 1.下载Kafka并解压 2.添加环境变量 3.修改 server.properties 文件 4.将kafka复制到其它节点 5.修改node1、node2节点的broker.id 6.将master的环境变量同步到node1、 node2 7.启动zookeeper…...
Linux网络——套接字与UdpServer
目录 一、socket 编程接口 1.1 sockaddr 结构 1.2 socket 常见API 二、封装 InetAddr 三、网络字节序 四、封装通用 UdpServer 服务端 4.1 整体框架 4.2 类的初始化 4.2.1 socket 4.2.2 bind 4.2.3 创建流式套接字 4.2.4 填充结构体 4.3 服务器的运行 4.3.1 rec…...
SpringBoot源码深度解析
今天,聊聊SpringBoot的源码,本博客聊的版本为v2.0.3.RELEASE。目前SpringBoot的最新版为v3.3.2,可能目前有些公司使用的SpringBoot版本高于我这个版本。但是没关系,因为版本越新,新增的功能越多,反而对Spri…...
【Qt】常用控件
文章目录 QWidgetenabledgeometrywindow framewindowTitlewindowIconqrc资源管理windowOpacitycursorfonttoolTipfocusPolicystyleSheet 按钮类PushButtonRadioButtonCheckBoxSignals 显示类LabelLCDNumberProgressBarCalendar 输入类LineEditTextEditComboBoxSpinBoxDateTimeE…...
electron 主进程和渲染进程通信
在Electron中,主进程(main process)和渲染进程(renderer process)之间的通信是非常重要的,因为Electron应用通常会将用户界面(由Web技术如HTML, CSS, 和JavaScript构建)和原生功能(如系统对话框、文件I/O等)分开处理。主进程管理应用的生命周期和创建渲染进程,而渲染…...
【ARM】MDK-解决CMSIS_DAP.DLL missing报错
【更多软件使用问题请点击亿道电子官方网站】 1、 文档目标 记录解决CMSIS_DAP.DLL missing的报错情况,对应相关报错信息,供后续客户参考,快速解决客户问题。 2、 问题场景 客户进行硬件调试时,发现Target设置内有CMSIS_DAP.DL…...
多云管理“拦路虎”:深入解析网络互联、身份同步与成本可视化的技术复杂度
一、引言:多云环境的技术复杂性本质 企业采用多云策略已从技术选型升维至生存刚需。当业务系统分散部署在多个云平台时,基础设施的技术债呈现指数级积累。网络连接、身份认证、成本管理这三大核心挑战相互嵌套:跨云网络构建数据…...
java_网络服务相关_gateway_nacos_feign区别联系
1. spring-cloud-starter-gateway 作用:作为微服务架构的网关,统一入口,处理所有外部请求。 核心能力: 路由转发(基于路径、服务名等)过滤器(鉴权、限流、日志、Header 处理)支持负…...
51c自动驾驶~合集58
我自己的原文哦~ https://blog.51cto.com/whaosoft/13967107 #CCA-Attention 全局池化局部保留,CCA-Attention为LLM长文本建模带来突破性进展 琶洲实验室、华南理工大学联合推出关键上下文感知注意力机制(CCA-Attention),…...
五年级数学知识边界总结思考-下册
目录 一、背景二、过程1.观察物体小学五年级下册“观察物体”知识点详解:由来、作用与意义**一、知识点核心内容****二、知识点的由来:从生活实践到数学抽象****三、知识的作用:解决实际问题的工具****四、学习的意义:培养核心素养…...
【Go】3、Go语言进阶与依赖管理
前言 本系列文章参考自稀土掘金上的 【字节内部课】公开课,做自我学习总结整理。 Go语言并发编程 Go语言原生支持并发编程,它的核心机制是 Goroutine 协程、Channel 通道,并基于CSP(Communicating Sequential Processes࿰…...
C++中string流知识详解和示例
一、概览与类体系 C 提供三种基于内存字符串的流,定义在 <sstream> 中: std::istringstream:输入流,从已有字符串中读取并解析。std::ostringstream:输出流,向内部缓冲区写入内容,最终取…...
mysql已经安装,但是通过rpm -q 没有找mysql相关的已安装包
文章目录 现象:mysql已经安装,但是通过rpm -q 没有找mysql相关的已安装包遇到 rpm 命令找不到已经安装的 MySQL 包时,可能是因为以下几个原因:1.MySQL 不是通过 RPM 包安装的2.RPM 数据库损坏3.使用了不同的包名或路径4.使用其他包…...
JS设计模式(4):观察者模式
JS设计模式(4):观察者模式 一、引入 在开发中,我们经常会遇到这样的场景:一个对象的状态变化需要自动通知其他对象,比如: 电商平台中,商品库存变化时需要通知所有订阅该商品的用户;新闻网站中࿰…...
【笔记】WSL 中 Rust 安装与测试完整记录
#工作记录 WSL 中 Rust 安装与测试完整记录 1. 运行环境 系统:Ubuntu 24.04 LTS (WSL2)架构:x86_64 (GNU/Linux)Rust 版本:rustc 1.87.0 (2025-05-09)Cargo 版本:cargo 1.87.0 (2025-05-06) 2. 安装 Rust 2.1 使用 Rust 官方安…...
2025年渗透测试面试题总结-腾讯[实习]科恩实验室-安全工程师(题目+回答)
安全领域各种资源,学习文档,以及工具分享、前沿信息分享、POC、EXP分享。不定期分享各种好玩的项目及好用的工具,欢迎关注。 目录 腾讯[实习]科恩实验室-安全工程师 一、网络与协议 1. TCP三次握手 2. SYN扫描原理 3. HTTPS证书机制 二…...

