事件驱动库 libev 使用详解

article/2025/10/8 15:26:22

C/C++Linux服务器开发/后台架构师知识体系

libev 是一个通过 C 语言编写的,高性能的事件循环库,支持多种事件类型,与此类似的事件循环库还有 libevent、libubox 等,在此详细介绍下 libev 相关的内容。

简介

这是一个简单而且高性能的事件库,支持常规的 IO、定时器等事件,而且没有任何依赖,同时支持多线程模式。

关于 libev 详见官网 http://software.schmorp.de (国内会被墙),其帮助文档可以参考 官方文档,安装完之后,可通过 man 3 ev 查看帮助信息,文档也在源码中保存了一份,可以通过 man -l ev.3 命令查看。

安装

安装可以源码安装,或者在 CentOS 中,最简单可通过如下方式安装。

----- 安装库
# yum install libev libev-devel

示例程序

如下是一个简单的示例程序。

#include <ev.h>
#include <stdio.h>// every watcher type has its own typedef'd struct with the name ev_TYPE
ev_io stdin_watcher;
ev_timer timeout_watcher;// all watcher callbacks have a similar signature
// this callback is called when data is readable on stdin
static void stdin_cb (EV_P_ ev_io *w, int revents)
{puts("stdin ready");// for one-shot events, one must manually stop the watcher// with its corresponding stop function.ev_io_stop(EV_A_ w);// this causes all nested ev_run's to stop iteratingev_break(EV_A_ EVBREAK_ALL);
}// another callback, this time for a time-out
static void timeout_cb (EV_P_ ev_timer *w, int revents)
{puts("timeout");// this causes the innermost ev_run to stop iteratingev_break(EV_A_ EVBREAK_ONE);
}int main (void)
{// use the default event loop unless you have special needs// struct ev_loop *loop = EV_DEFAULT; /* OR ev_default_loop(0) */EV_P EV_DEFAULT;// initialise an io watcher, then start it// this one will watch for stdin to become readableev_io_init(&stdin_watcher, stdin_cb, /*STDIN_FILENO*/ 0, EV_READ);ev_io_start(EV_A_ &stdin_watcher);// initialise a timer watcher, then start it// simple non-repeating 5.5 second timeoutev_timer_init(&timeout_watcher, timeout_cb, 5.5, 0.);ev_timer_start(EV_A_ &timeout_watcher);ev_run(EV_A_ 0); /* now wait for events to arrive */ev_loop_destroy(EV_A);return 0;
}

可以通过如下命令编译。

----- 编译示例程序
$ gcc -lev example.c -o example

执行过程

libev 是一个事件循环,首先需要注册感兴趣的事件,libev 会监控这些事件,当事件发生时调用相应的处理函数,也就是回调函数。其处理过程为:

  1. 初始化一个事件循环。可以通过 ev_default_loop(0) 或者 EV_DEFAULT 初始化,两者等价。
  2. 定义事件类型。在 ev.h 中定义了各种类型的,如 ev_ioev_timerev_signal 等。
  3. 注册感兴趣事件。这里被称为 watchers ,这个是 C 结构体。
  4. 启动监控。启动上步注册的事件,如 ev_io_start()ev_timer_start() 等。
  5. 启动 libev 循环。重复 1, 2 步,然后启动 libev 事件循环,直接执行 ev_run() 即可。

循环体

可以通过 ev_default_loop() 初始化一个默认循环,如果支持多实例可以使用 ev_loop_new() 创建一个新的,其中包括了部分入参用来标示如何进行处理。

可用标示有。

enum {/* the default */EVFLAG_AUTO      = 0x00000000U, /* not quite a mask *//* flag bits */EVFLAG_NOENV     = 0x01000000U, /* do NOT consult environment */EVFLAG_FORKCHECK = 0x02000000U, /* check for a fork in each iteration *//* debugging/feature disable */EVFLAG_NOINOTIFY = 0x00100000U, /* do not attempt to use inotify */
#if EV_COMPAT3EVFLAG_NOSIGFD   = 0, /* compatibility to pre-3.9 */
#endifEVFLAG_SIGNALFD  = 0x00200000U, /* attempt to use signalfd */EVFLAG_NOSIGMASK = 0x00400000U  /* avoid modifying the signal mask */
};

事件设置方式

这里以 struct ev_io 为例,有如下的两种设置方式。

将回调函数和关心的事件同时注册。

struct ev_io wstdin;
ev_io_init(&wstdin, stdin_hook, /*STDIN_FILENO*/ 0, EV_READ);
ev_io_start(EV_A &wstdin);

一般来说,在设置了回调函数之后,很少会进行修改,在首次调用的时候需要修改关注的事件,那么此时就可以将设置回调和设置事件分开。

struct ev_io wstdin;
ev_init(&wstdin, stdin_hook);
... ... /* some time later. */
ev_io_set(&wstdin, /*STDIN_FILENO*/ 0, EV_READ);
ev_io_start(EV_A &wstdin);

定制适配

在使用 libev 时,可以作如下的适配。

初始化

在 libev 中,通过 struct ev_loop 结构定义了一个具体的循环实例,包含了事件循环所需要的所有数据,而同时 libev 提供了很多宏定义适配多实例模式,也就是需要使用多个 loop ,可以通过 #define EV_MULTIPLICITY 1 宏进行定义。

使用多实例模式时,一般函数的第一个入参就是 loop ,为此,libev 提供了一系列宏适配单实例和多实例模式,允许不修改代码直接编译即可。例如,EV_P_ 作为函数定义、函数声明的第一个参数,当是多实例时,实际为 struct ev_loop *loop, ,而在单实例模式中就是空。

不过在定义初始化的时候有点问题,没有找到合适的宏定义,建议添加如下内容。

#if EV_MULTIPLICITY
# define EV_DEFAULT_DEC EV_P = EV_DEFAULT
#else
# define EV_DEFAULT_DEC ev_default_loop(0)
#endif

这样就可以在开始初始化的时候直接使用 EV_DEFAULT_DEC 即可。

定制化

默认 libev 会使用 config.h 作为配置文件,不过这个文件可能会根项目的配置文件冲突,可以通过宏 EV_CONFIG_H 定义,如果使用的时 CMake 作管理,那么通过如下方式定义。

ADD_DEFINITIONS(-DEV_CONFIG_H="evconfig.h")

在该文件中就可以进行部分的定制,例如可以使用如下内容。

#ifndef EV_EVCONFIG_H_
#define EV_EVCONFIG_H_#define EV_PERIODIC_ENABLE 1
#define EV_STAT_ENABLE     1
#define EV_PREPARE_ENABLE  1
#define EV_CHECK_ENABLE    1
#define EV_IDLE_ENABLE     1
#define EV_FORK_ENABLE     1
#define EV_CLEANUP_ENABLE  1
#define EV_SIGNAL_ENABLE   1
#define EV_CHILD_ENABLE    1
#define EV_ASYNC_ENABLE    1
#define EV_EMBED_ENABLE    1
#define EV_WALK_ENABLE     0 /* not yet */#define EV_USE_EPOLL       1
#define HAVE_EPOLL_CTL     1
#define HAVE_SYS_EPOLL_H   1#define EV_AVOID_STDIO     1#endif

使用示例

除了基础的 IO、定时器、信号的处理之外,同时还提供了一些循环中经常使用的 hook 处理,以及一些常用的场景。

### 基础事件能力
struct ev_io        IO事件,包括了Socket、Pipe
struct ev_timer     定时器,采用的是相对时间,基于Bin-Heap
struct ev_periodic  定时器,采用的是UTC时间,基于Bin-Heap
struct ev_signal    = 信号处理
struct ev_child     = SIGCHLD信号的处理
struct ev_stat      文件的监控,Linux中用的是inotify机制### 扩展事件
struct ev_fork      = 是否是在子进程中运行 使用forks数组
struct ev_embed
struct ev_async     = 异步事件,内部采用Pipe实现 使用asyncs数组### 循环流程Hook
struct ev_idle      空闲 使用idles数组
struct ev_prepare   每次循环在阻塞之前调用 使用prepares数组
struct ev_check     每次循环在事件处理之后调用 使用checks数组
struct ev_cleanup   当循环退出时会调用 使用cleanups数组,会在ev_loop_destroy()中触发

如下简单介绍各种事件的使用方法。

Timer Watcher

可以设置定时器的启动时间,以及循环的时间间隔。

#include <time.h>
#include <stdio.h>
#include <stdint.h>#include "libev/ev.h"ev_timer timeout_watcher;
ev_timer repeate_watcher;
ev_timer oneshot_watcher;// another callback, this time for a time-out
static void timeout_cb (EV_P_ ev_timer *w, int revents)
{(void) w;(void) revents;printf("timeout at %ju\n", (uintmax_t)time(NULL));/* this causes the innermost ev_run to stop iterating */ev_break(EV_A_ EVBREAK_ONE);
}
static void repeate_cb (EV_P_ ev_timer *w, int revents)
{(void) w;(void) revents;printf("repeate at %ju\n", (uintmax_t)time(NULL));
}
static void oneshot_cb (EV_P_ ev_timer *w, int revents)
{(void) w;(void) revents;printf("oneshot at %ju\n", (uintmax_t)time(NULL));ev_timer_stop(EV_A_ w);
}int main (void)
{time_t result;EV_DEFAULT_DEC; /* OR ev_default_loop(0) */result = time(NULL);printf("  start at %ju\n", (uintmax_t)result);/* run only once in 2s later */ev_timer_init(&oneshot_watcher, oneshot_cb, 2.0, 0.);ev_timer_start(EV_A_ &oneshot_watcher);/* run in 5 seconds later, and repeat every second */ev_timer_init(&repeate_watcher, repeate_cb, 5., 1.);ev_timer_start(EV_A_ &repeate_watcher);/* timeout in 10s later, and also quit. */ev_timer_init(&timeout_watcher, timeout_cb, 10., 0.);ev_timer_start(EV_A_ &timeout_watcher);/* now wait for events to arrive. */ev_run(EV_A_ 0);ev_loop_destroy(EV_A);return 0;
}

Periodic Watcher

Periodic 可以理解为类似于 crontab ,不像 timer 基于的是相对时间,改调度基于的是日历时间或者说是墙上时间。

这也就意味着,时间会受手动调整的影响,有可能比真实的时间快或者慢。

例如,启动一个 periodic 时钟在 10 秒后触发,但是此时又将时间调整到了一个月之后,那么这一事件将在 1month+10seconds 之后触发,而如果使用的是 timer ,那么无论时间如何调整都会在 10s 后触发。

#include <time.h>
#include <stdio.h>
#include <stdint.h>#include "ev.h"#define log_info(fmt, args...) printf("%ju " fmt, time(NULL), ##args)static void minute_tick_hook(EV_P_ ev_periodic *w, int revents)
{(void) loop;(void) w;(void) revents;log_info("invoking\n");
}int main (void)
{struct ev_loop *loop = ev_default_loop(0);ev_periodic minute_tick;ev_periodic_init(&minute_tick, minute_tick_hook, 0., 60., 0);ev_periodic_start(EV_A_ &minute_tick);log_info("start\n");/* now wait for events to arrive. */ev_run(EV_A_ 0);ev_loop_destroy(EV_A);return 0;
}

Signal Watcher

在收到 SIGINT 时做些清理,直接退出。

#include <ev.h>
#include <stdio.h>
#include <signal.h>static void sigint_cb(EV_P_ ev_signal *w, int revents)
{(void) revents;printf("catch SIGINT, signal number %d.\n", w->signum);ev_break(EV_A_ EVBREAK_ALL);
}int main (void)
{ev_signal wsig;// use the default event loop unless you have special needsEV_DEFAULT_DEC; /* OR ev_default_loop(0) */ev_signal_init(&wsig, sigint_cb, SIGINT);ev_signal_start(EV_A_ &wsig);ev_run(EV_A_ 0); // now wait for events to arriveev_loop_destroy(EV_A);return 0;
}

Child Watcher

fork 一个新进程,给它安装一个 child 处理器等待进程结束,实际上会等待接受 SIGCHLD 信号,然后调用相应的事件。

#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>#include "libev/ev.h"static void child_cb (EV_P_ ev_child *w, int revents)
{(void) revents;ev_child_stop(EV_A_ w);printf ("process %d exited with status %x\n", w->rpid, w->rstatus);
}int main (void)
{/* use the default event loop unless you have special needs */EV_DEFAULT_DEC; /* OR ev_default_loop(0) */ev_child cw;pid_t pid = fork();if (pid < 0) {  /* error */perror("fork()");exit(EXIT_FAILURE);} else if (pid == 0) {  /* child, the forked child executes here */sleep(1);exit(EXIT_SUCCESS);}printf("parent %d child %d forked.\n", getpid(), pid);/* parent */ev_child_init(&cw, child_cb, pid, 0);ev_child_start(EV_A_ &cw);/* now wait for events to arrive */ev_run(EV_A_ 0);ev_loop_destroy(EV_A);return 0;
}

在 libev 中,实际上也是通过注册一个 SIGCHILD 信号进行处理的,其回调函数是 childcb

Fork Watcher

在 libev 中提供了一个 fork 事件的监控,libev 会在循环中自动检测是否调用了 fork() 函数,如果是那么会重新设置事件驱动回调函数。

注意,默认不会自动检测,需要设置相关的参数,例如 ev_default_loop(EVFLAG_FORKCHECK),这样才会在每次循环的时候检测。

除了自动判断,也可以在 fork() 子进程之后调用 ev_loop_fork() 函数。

#include <ev.h>
#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>static void fork_callback(EV_P_ ev_fork *w, int revents)
{(void) w;(void) revents;printf("[%d] fork callback\n", getpid());
}static void timeout_callback(EV_P_ ev_timer *w,int revents)
{(void) w;(void) revents;printf("[%d] time out\n", getpid());
}int main(void)
{struct ev_loop *loop;ev_fork wfork;ev_timer wtimer;loop = ev_default_loop(EVFLAG_FORKCHECK);ev_fork_init(&wfork, fork_callback);ev_fork_start(EV_A_ &wfork);ev_timer_init(&wtimer, timeout_callback, 1., 1.);ev_timer_start(EV_A_ &wtimer);pid_t pid;pid = fork();if (pid < 0) {return -1;} else if (pid == 0) {printf("[%d] Child\n", getpid());//ev_loop_fork(EV_A);ev_run(EV_A_ 0);ev_loop_destroy(EV_A);return 0;}printf("[%d] Parent\n", getpid());ev_run(EV_A_ 0);ev_loop_destroy(EV_A);return 0;
}

在如上的示例中,使用的是多实例模式,会在子进程中重新执行,所以最好的方式是,如果不需要最好直接关闭。

另外,在创建 epoll 对象时,入参使用了 EPOLL_CLOEXEC 参数,也就意味着在 fork 进程时会自动关闭文件描述符。

Async Watcher

通常用于多个线程之间的事件同步,该事件允许在不同的线程中发送事件消息,内部使用 PIPE 进行通讯。

#include <stdio.h>
#include <unistd.h>
#include <pthread.h>
#include <sys/syscall.h>#include <ev.h>#define log_info(fmt, args...) printf("%ju lwpid(%lu) " fmt, time(NULL), syscall(SYS_gettid), ##args)struct ev_loop *work_loop = NULL;
static struct ev_async wasync;static void async_cb(EV_P_ struct ev_async *w, int revents)
{(void) w;log_info("async hook call, event %d loop %p.\n", revents, loop);
}void *ev_create(void *p)
{(void) p;log_info("worker thread start!\n");sleep(3);ev_async_init(&wasync, async_cb);ev_async_start(work_loop, &wasync);ev_run(work_loop, 0);return NULL;
}int main(void)
{int num = 0;pthread_t tid;work_loop = ev_loop_new(EVFLAG_AUTO);pthread_create(&tid, NULL, ev_create, NULL);log_info("main thread start!\n");while(1) {log_info("send async #%d times.\n", num);ev_async_send(work_loop, &wasync);sleep(1);num++;}return 0;
}

如上,实际上启动顺序是不影响的,每次起一个线程都会建立一个 PIPE 管道。

多实例

也就是在多线程中,初始化不同的实例。

#include <ev.h>
#include <stdio.h>
#include <unistd.h>
#include <pthread.h>
#include <sys/syscall.h>#define log_info(fmt, args...) printf("%ju lwpid(%lu) " fmt, time(NULL), syscall(SYS_gettid), ##args)static void repeate_hook(EV_P_ ev_timer *w, int revents)
{(void) w;(void) revents;(void) loop;log_info("repeate\n");
}void *child1(void *arg)
{(void) arg;EV_P = ev_loop_new(0);ev_timer wtimer;log_info("child thread started.\n");ev_timer_init(&wtimer, repeate_hook, 0., 1.);ev_timer_start(EV_A_ &wtimer);ev_run(EV_A_ 0);return NULL;
}int main (void)
{EV_DEFAULT_DEC; /* default */ev_timer wtimer;pthread_t tid1;pthread_create(&tid1, NULL, child1, NULL);ev_timer_init(&wtimer, repeate_hook, 0., 1.);ev_timer_start(EV_A_ &wtimer);/* now wait for events to arrive. */ev_run(EV_A_ 0);pthread_join(tid1, NULL);return 0;
}

注意,全局只能有一个默认的 struct ev_loop ,在线程中,需要通过 ev_loop_new() 再新建一个。


推荐:

C/C++Linux服务器开发/高级架构师群:960994558 群内提供一些免费的C/C++Linux服务器开发/高级架构师学习资料资料包括C/C++,Linux,golang技术,Nginx,ZeroMQ,MySQL,Redis,fastdfs,MongoDB,ZK,流媒体,CDN,P2P,K8S,Docker,TCP/IP,协程,DPDK,ffmpeg等)
在这里插入图片描述


http://chatgpt.dhexx.cn/article/84RlyfYQ.shtml

相关文章

数据库和实例

这篇文章跟数据库开发有什么关系呢&#xff1f;我感觉呢只从字面上看确实没有什么关系&#xff0c;可是了解的话跟大牛讨论时他们最起码不会被鄙视、面试时可能也会有用。如果你再深入的钻研下去你就会发现里面的内容好“丰满”。作为一个菜鸟&#xff08;指本人&#xff09;以…

【达梦数据库实例创建】

达梦数据库实例创建 系统环境&#xff1a;银河麒麟V10 数据库版本&#xff1a;dm8 一、创建数据库实例 1.通过命令切换到路径/dm8/tool 2.执行命令进入配置助手 ./dbca.sh 选择一般用途&#xff0c;自动调整数据库性能参数&#xff0c;进行下一步操作&#xff1a; 设置数据…

SQL 经典实例

《SQL经典实例》 1&#xff0c;在WHERE子句中引用别名列 直接引用-报错 把查询包装为一个内嵌视图&#xff0c;这样就可以引用别名列了 select * from ( select sal as salary, comm as commission from emp ) x where salary < 5000同理&#xff1a;当你想在 WHERE 子句中…

SQL示例数据库

在本教程中&#xff0c;我们将向您介绍在整个教程中使用的SQL示例数据库。以下数据库关系图 - HR 示例数据库&#xff1a; 在这个HR 示例数据库有7个表&#xff1a; employees表存储员工的数据信息。 jobs表存储工作数据信息&#xff0c;包括职位和工资范围。 departments表…

数据库案例

目录 微信朋友圈设计 用户及用户关系 发朋友圈 CDN 发布表 相册表 时间线 刷朋友圈 删除、拉黑、标签、不让他看、三天可见该怎么办 谁可以看 标签 第二步的权限控制 评论和赞 微信朋友圈设计 用户及用户关系 肯定有用户表作为基础 用户关系表&#xff0c;用户…

SQL数据库编写及示例

一、 数据库编写 1、数据库常用约束 主键约束: primary key 外键约束: foreign key (references) 唯一值约束: unique 默认值约束: default 检查约束: check 非空约束: not null 标识列: identity 2、创建数据表注意事项 主外键数据类型必须一致 列与列之间用&#xff0c;间隔…

sqlserver:什么是数据库实例?

环境&#xff1a; window server 2019 datacentersqlserver2014 x64 问题&#xff1a; 什么是SQL server实例&#xff1f;数据库的对象架构是怎样设计的&#xff1f; 先把官方的解释贴出来&#xff1a; https://docs.microsoft.com/zh-cn/sql/relational-databases/databases…

【转】数据库设计实例一学习

​​​​​​数据库1​​​​​​​​​​​​​​​​​​​​​对多&#xff0c;1对1&#xff0c;M对N学习 以RBAC为例。​​​​​​​​​​​​​​ 于 RBAC&#xff08;Role-based Access Control&#xff09;权限访问控制。也就是说一个用户可以有多个角色&#xff…

sql server 数据库设计实例

本实例为综合实例,考察数据库原理中的,sql脚本的编写,创建——增删改查,视图和索引的创建等;数据库ER图,关系模式;以及一些高级的应用包括:触发器,函数和存储过程。 (一). 数据库设计题目如下 有一个图书出版发行管理系统,其主要业务规则如下: 一个作者可以编写多…

达梦数据库创建及数据库实例管理

一、配置助手创建和删除数据库 数据库配置助手创建数据库调用 dbca.sh 图形化界面创建数据库&#xff1a;[dmdbaDCA02 tool]$ ./dbca.sh2021-01-11 11:43:45 [com.dameng.dbca.Startup] [INFO] 启动 DBCA 指定数据库名称、实例名称&#xff08;单机情况下数据库和实例名称可以…

MySql 数据库操作实例

MySql 数据库操作实例 案例描述创建插入数据内外连接~问题问题1&#xff1a;查询周星星的成绩问题2&#xff1a;查询所有人的平均成绩以及其他信息1&#xff09;查询所有人的平均成绩2&#xff09;查询平均成绩最高的前三名3&#xff09;查询平均成绩排名第三的学生信息 问题3&…

数据库五个经典实例

创建数据库链接&#xff0c;需要用到connection对象&#xff0c;recordset对象。 对数据库进行操作&#xff0c;需要用到command对象&#xff0c;parameter对象。这两个对象成对出现。 connection对象&#xff1a;创建数据库链接。在对数据库进行操作的前提步骤。 recordset…

Oracle 数据库实例介绍

文章目录 数据库实例介绍实例结构实例配置读写实例与只读实例实例生命周期实例标识Oracle 根目录Oracle 主目录Oracle SID 实例启动与关闭启动实例与数据库管理员登录启动实例加载数据库打开数据库只读模式数据库文件检查 关闭数据库与实例关闭模式关闭数据库正常关闭异常关闭 …

Python:二叉树遍历

二叉树遍历共有四种方法&#xff0c;分别是前序遍历、中序遍历、后序遍历和层次遍历。 前序遍历&#xff1a; 父节点——左孩子——右孩子 中序遍历&#xff1a;左孩子——父节点——右孩子 后序遍历&#xff1a;左孩子——右孩子——父节点 层次遍历&#xff1a;利用队列解…

【算法】二叉树遍历的几种常见方法

二叉树遍历的几种常见方法 一. 二叉树分类&#xff1a; 完全二叉树满二叉树扩充二叉树平衡二叉树 二. 二叉树的四种遍历方式&#xff1a; 前序遍历&#xff08;先根&#xff0c;再左&#xff0c;最后右&#xff09;中序遍历&#xff08;先左&#xff0c;再根&#xff0c;最…

二叉树遍历的非递归算法

非递归的算法主要采用的是循环出栈入栈来实现对二叉树的遍历&#xff0c;下面是过程分析 以下列二叉树为例&#xff1a;&#xff08;图片来自懒猫老师《数据结构》课程相关内容&#xff09; 1.前序遍历 前序遍历的顺序为&#xff1a;根结点->左子树->右子树 基本过程&a…

二叉树的中序遍历算法

一&#xff0c;简介 二叉树的中序遍历在计算机行业有着重要的作用&#xff0c;其中一个应用就是判断一棵二叉树是否二叉排序树。 下面介绍递归和非递归两种方式实现中序遍历。 二&#xff0c;递归实现 递归实现非常简单&#xff0c;左根右依次进行即可。 void mid_scan2(n…

JavaScript算法 — 二叉树遍历

目录 1、构造二叉树2、递归遍历3、非递归遍历3.1 先序3.2 中序3.3 后序 1、构造二叉树 树节点&#xff1a; // 二叉树节点的构造函数 function TreeNode(val, left, right) {this.val (valundefined ? 0 : val)this.left (leftundefined ? null : left)this.right (righ…

二叉树遍历算法之一:前序遍历

递归实现前序遍历 二叉树的前序遍历是指从根节点出发&#xff0c;按照先根节点&#xff0c;再左子树&#xff0c;后右子树的方法遍历二叉树中的所有节点&#xff0c;使得每个节点都被访问一次。 当调用遍历算法的时候前序遍历的具体过程如下&#xff1a; 首先访问根节点&…

二叉树遍历小结

前言 二叉树是相当重要的数据结构&#xff0c;目前我还只会玩玩它的遍历&#xff08;年轻不懂事没好好学&#xff0c;不然早就达到人生巅峰了&#xff09;&#xff0c;LeetCode上二叉树的简单题&#xff0c;大部分通过遍历加一点小逻辑即可解决&#xff0c;所以总结一下几种遍…