Hot100:二叉树的中序遍历(Binary Tree Inorder Traversal)递归 / 显式栈 ACERS 解析

副标题 / 摘要 二叉树遍历是树题模板的起点,中序遍历则是“递归思维”和“显式栈模拟”最典型的一题。本文按 ACERS 结构拆解 LeetCode 94,把左-根-右的访问顺序、迭代栈写法和工程迁移价值一次讲清。 预计阅读时长:10~12 分钟 标签:Hot100、二叉树、DFS、栈、中序遍历 SEO 关键词:Hot100, Binary Tree Inorder Traversal, 二叉树的中序遍历, 中序遍历, 显式栈, LeetCode 94 元描述:从递归到显式栈,系统讲透 LeetCode 94 二叉树中序遍历,并给出工程场景迁移与多语言实现。 目标读者 正在刷 Hot100,希望把树遍历模板固定下来的同学 刚从数组 / 链表过渡到树结构,容易把前序、中序、后序顺序写混的开发者 需要在 BST、表达式树、抽象语法树里复用“左-根-右”思想的工程师 背景 / 动机 中序遍历本身不复杂,但它的训练价值很高: 它是“递归 = 隐式栈,迭代 = 显式栈”最容易建立直觉的一题 它能帮助你稳定掌握“先一路向左,再回退访问根,再转向右子树”的过程 在 二叉搜索树(BST) 里,中序遍历天然得到有序序列,工程迁移价值很强 很多人第一次写树题不是逻辑不会,而是: 不清楚访问顺序到底是谁先谁后 迭代版不知道什么时候入栈、什么时候出栈 一旦树为空或只有单边链,代码就容易写乱 这题把模板练熟,后面的验证 BST、找第 k 小元素、恢复二叉搜索树等题会更顺。 核心概念 中序遍历:按照 左子树 -> 根节点 -> 右子树 的顺序访问 DFS(深度优先搜索):树遍历最常见的组织方式,中序遍历就是 DFS 的一种访问顺序 显式栈:把递归调用栈手动写出来,用栈保存“回头还要处理的节点” 树高 h:空间复杂度通常写成 O(h),平衡树约为 O(log n),极端退化链表时是 O(n) A — Algorithm(题目与算法) 题目还原 给定二叉树根节点 root,返回它的 中序遍历 结果。 ...

2026年3月6日 · 6 分钟 · map[name:Jeanphilo]

先等价迁移,再行为改造:AI 协作时代最稳的工程工作流

把复杂改造拆成“对齐基线”和“设计优化”两波,控制变量、提升可回滚性与排障效率。

2026年3月5日 · 2 分钟 · map[name:Jeanphilo]

手写一个基础消息代理:发布、订阅、重试与失败契约

用一个可运行的 Go 版本基础消息代理,讲透发布订阅、重试语义、失败契约、吞吐与积压估算,以及从朴素实现到工程可用实现的关键取舍。

2026年2月28日 · 8 分钟 · map[name:Jeanphilo]

Git Worktree 使用教程:同仓库并行开发多个分支

标题 Git Worktree 使用教程:同仓库并行开发多个分支 副标题 / 摘要 git worktree 让你在同一个 Git 仓库下,同时打开多个分支对应的工作目录,不用来回 checkout 和 stash。 本文覆盖常用命令、典型场景、常见坑,以及你最关心的 hotfix 分支创建方式(先进入 worktree 再建分支 / 一步建分支)。 目标读者 正在同时处理多个需求分支(feature / bugfix / hotfix)的开发者 经常需要切到 main 修紧急问题,但不想污染当前工作目录的人 需要对比不同分支代码、并行运行不同版本服务的工程师 背景 / 动机 很多人第一次遇到并行任务时,会用这套流程: git stash git checkout main # 修 bug git checkout feature/xxx git stash pop 这套流程不是不能用,但有几个问题: stash 容易忘记清理或弹错时机 来回切分支容易打断当前开发节奏 长时间运行测试/构建会占住当前工作目录 对比两个分支代码时不够直观 git worktree 的价值就是:在一个仓库里同时拥有多个工作目录,各做各的事,互不干扰。 核心概念 worktree(工作树):同一仓库的一个额外工作目录 共享对象库:多个 worktree 共享同一套 Git 对象数据(不是多个独立 clone) 独立工作目录:每个 worktree 有自己的文件内容、当前分支、未提交修改 分支占用限制:同一分支不能同时被多个 worktree 检出(Git 会保护你) stash 是仓库级别:不是 worktree 级别,多个 worktree 共享同一个 stash 列表 结构示意(ASCII): ...

2026年2月25日 · 4 分钟 · map[name:Jeanphilo]

Docker 常用使用教程:从入门到 Compose 实战(含 save/load 与权限排障)

标题 Docker 常用使用教程:从入门到 Compose 实战(含 save/load 与权限排障) 副标题 / 摘要 很多人会 docker run,但一到离线交付和挂载权限就卡住。 本文按“能直接落地”的顺序,带你走完 Docker 常用命令、Dockerfile、Compose、save/load,以及最常见的 UID/GID 权限问题。 目标读者 想系统掌握 Docker 日常用法的开发者 需要用 Docker Compose 跑本地或测试环境的工程师 经常遇到“挂载目录写不进去”权限问题的人 背景 / 动机 在“我的机器能跑、你的机器跑不起来”的场景里,Docker 的价值不是概念,而是交付稳定性。 但实际使用中,常见断点通常在三处: 命令会用,但不知道整套流程怎么串起来 需要离线迁移时,不清楚 save/load 怎么和 Compose 配合 挂载宿主目录后,容器用户与宿主目录属主不一致导致 Permission denied 核心概念 镜像(Image):应用运行模板,分层存储,可复用 容器(Container):镜像的运行实例 仓库(Registry):镜像分发中心(如 Docker Hub) 卷(Volume):由 Docker 管理的数据持久化目录 Bind Mount:把宿主机目录直接挂载进容器 Compose:用一个 compose.yaml 管理多容器应用 一、安装与最小验证 先确保 Docker CLI 与 daemon 可用: docker --version docker info docker run --rm hello-world 如果 hello-world 能成功输出欢迎信息,说明基础环境已就绪。 ...

2026年2月12日 · 3 分钟 · map[name:Jeanphilo]

LeetCode 146:LRU 缓存设计(O(1))哈希表 + 双向链表实战

副标题 / 摘要 这题不是“背答案题”,而是缓存系统的基本功:如何在常数时间内同时满足“快速访问”和“按最近最少使用淘汰”。本文从朴素方案推到最优结构,并给出可运行的多语言实现。 预计阅读时长:14~18 分钟 标签:LRU、哈希表、双向链表、系统设计 SEO 关键词:LRU Cache, LeetCode 146, 哈希表, 双向链表, O(1) 元描述:通过哈希表 + 双向链表实现 LRU 缓存,get/put 平均 O(1),附工程场景、常见坑与六语言实现。 A — Algorithm(题目与算法) 题目重述 设计并实现一个满足 LRU 约束的数据结构 LRUCache: LRUCache(int capacity):用正整数容量初始化 int get(int key):若 key 存在返回 value,否则返回 -1 void put(int key, int value): key 已存在:更新 value,并视作最近使用 key 不存在:插入新键值对 若超出容量:淘汰最久未使用的 key 并要求 get 和 put 平均时间复杂度为 O(1)。 示例 1(操作序列) LRUCache cache = new LRUCache(2) cache.put(1, 1) // 缓存: {1=1} cache.put(2, 2) // 缓存: {1=1, 2=2} cache.get(1) // 返回 1,且 1 变成最近使用 cache.put(3, 3) // 容量满,淘汰 key=2 cache.get(2) // 返回 -1 cache.put(4, 4) // 淘汰 key=1 cache.get(1) // 返回 -1 cache.get(3) // 返回 3 cache.get(4) // 返回 4 示例 2(更新已有键) LRUCache cache = new LRUCache(2) cache.put(1, 10) cache.put(1, 99) // 更新 value,且 1 视作最近使用 cache.get(1) // 返回 99 目标读者 正在刷 LeetCode 中等题、想吃透“数据结构组合技”的同学 做后端/中间件,需要实现或优化本地缓存的工程师 面试中经常被问到 LRU,但只记住结论、没掌握细节的人 背景 / 动机 缓存是“空间换时间”,但空间是有限的。 当缓存满了,必须淘汰一些键。LRU(Least Recently Used,最近最少使用)假设: ...

2026年2月11日 · 13 分钟 · map[name:Jeanphilo]

LeetCode 138:随机链表的复制(Copy List with Random Pointer)深拷贝全解析

副标题 / 摘要 这道题的难点不是遍历链表,而是正确复制 random 指针所形成的“跨节点引用关系”。本文从朴素思路推导到哈希映射法,讲清为什么它稳定、可维护、易工程落地。 预计阅读时长:12~16 分钟 标签:链表、深拷贝、哈希表、随机指针 SEO 关键词:LeetCode 138, Copy List with Random Pointer, 随机链表复制, 深拷贝, 哈希映射 元描述:用两趟遍历 + 映射表完成随机链表深拷贝,系统讲解正确性、复杂度、工程实践与六语言实现。 A — Algorithm(题目与算法) 题目重述 给定一个长度为 n 的链表,每个节点有: val next random(可指向任意节点或 null) 要求构造该链表的深拷贝并返回新头节点。 新链表中的任何指针都不能指向原链表节点。 输入 / 输出表示 题面常用 [val, random_index] 表示每个节点: val:节点值 random_index:random 指向的节点下标;若为空则为 null 你的函数入参只有 head,输出复制链表的头节点。 示例 1 输入: [[7,null],[13,0],[11,4],[10,2],[1,0]] 输出: [[7,null],[13,0],[11,4],[10,2],[1,0]] 解释: 输出与输入的“值与引用关系”一致,但节点是全新对象。 示例 2 输入: [[1,1],[2,1]] 输出: [[1,1],[2,1]] 解释: 第一个节点 random 指向第二个节点,第二个节点 random 指向自己。 目标读者 刷 LeetCode 时对 random 指针题目不够稳的开发者 想厘清“浅拷贝 vs 深拷贝”差异的同学 希望把算法思路迁移到工程对象复制场景的工程师 背景 / 动机 普通链表只要复制 val 和 next,逻辑很直观; 但随机链表多了一个 random 指针,它可能: ...

2026年2月11日 · 9 分钟 · map[name:Jeanphilo]

LeetCode 19:删除链表的倒数第 N 个结点(双指针一趟扫描)ACERS 全解析

副标题 / 摘要 这题的核心不是“删除节点”,而是“如何在单链表里定位倒数第 N 个节点的前驱”。本文从朴素思路推导到一趟双指针解法,用 ACERS 结构讲透正确性、边界处理与工程迁移。 预计阅读时长:12~15 分钟 适用场景标签:链表基础、双指针、面试高频 SEO 关键词:LeetCode 19, Remove Nth Node From End of List, 删除链表倒数第 N 个结点, 快慢指针, 哨兵节点 元描述(Meta Description):删除链表倒数第 N 个结点的完整 ACERS 解析:从暴力到一趟双指针,含复杂度、常见坑、工程示例与 Python/C/C++/Go/Rust/JS 代码。 A — Algorithm(题目与算法) 题目重述 给你一个链表,删除链表的倒数第 n 个结点,并返回链表的头结点。 输入输出 项目 类型 含义 head ListNode 单链表头结点 n int 倒数第 n 个位置 返回值 ListNode 删除目标节点后的头结点 示例 1 输入: head = [1,2,3,4,5], n = 2 输出: [1,2,3,5] 解释:倒数第 2 个节点是 4,删除后得到 [1,2,3,5]。 ...

2026年2月11日 · 11 分钟 · map[name:Jeanphilo]

LeetCode 2:两数相加(Add Two Numbers)链表进位从朴素到最优解

副标题 / 摘要 这题本质是把「小学竖式加法」搬到链表:同位相加、处理进位、走到末尾后可能还要补一个新节点。文章将从朴素思路推到最优单遍解法,并给出工程场景与多语言实现。 预计阅读时长:12~15 分钟 标签:链表、进位、模拟、LeetCode 2 SEO 关键词:Add Two Numbers, 两数相加, 逆序链表, 进位, LeetCode 2 元描述:用哨兵节点 + 单遍遍历在 O(max(m,n)) 时间完成两条逆序数字链表求和,附常见坑、工程应用和六语言代码。 A — Algorithm(题目与算法) 题目重述 给你两个非空链表,表示两个非负整数。 数字按逆序存储,且每个节点存储一位数字。 请将两个数相加,并返回同样逆序存储的结果链表。 题目保证除数字 0 外,这两个数都不会以 0 开头。 输入输出描述 项目 含义 输入 两个链表 l1、l2,每个节点值在 0~9 输出 一个新链表,表示 l1 + l2 的结果(逆序) 示例 1 输入: l1 = [2,4,3], l2 = [5,6,4] 解释: 342 + 465 = 807 输出: [7,0,8] 示例 2 输入: l1 = [9,9,9,9,9,9,9], l2 = [9,9,9,9] 解释: 9999999 + 9999 = 10009998 输出: [8,9,9,9,0,0,0,1] 目标读者 刚开始刷链表题,想建立稳定解题模板的同学 对「进位」和「边界处理」容易写错的中级开发者 希望把算法思维迁移到工程数据流处理的工程师 背景 / 动机 看似只是 LeetCode 入门题,但它练的能力非常实用: ...

2026年2月11日 · 12 分钟 · map[name:Jeanphilo]

先定不变量与契约,再写实现:Evans/Fowler 实战法

标题 先定不变量与契约,再写实现:Evans/Fowler 实战法 副标题 / 摘要 很多人理解“先定不变量与契约”时,会觉得只是“多写几行校验”。这篇文章给出更精确的答案:它的本质是固定责任归属,让调用方可以依赖行为语义,而不是猜测实现细节。 目标读者 正在做业务系统设计、代码评审的工程师 觉得“代码能跑,但改需求总出坑”的团队 想把 DDD/契约思想落到日常开发的人 背景 / 动机 常见开发顺序是“先把功能跑通,再补规则”。短期看速度快,长期会出现三个问题: 业务规则散落在多个 service/controller 里 调用方只能通过读实现猜行为 改一个需求会牵动大量分支判断 Evans/Fowler 这一脉的核心不是“写得更学术”,而是先明确系统必须成立的事实,再让实现为这些事实服务。 核心概念 不变量(Invariant):无论任何路径,始终为真的业务规则。 例如:已支付订单不能再次支付。 契约(Contract):对外可依赖的行为承诺,至少包含前置条件、后置条件、失败语义。 例如:cancel(order) 只接受可取消状态,成功后状态必须是 CANCELLED,否则抛明确异常。 接口 vs 契约:接口是签名,契约是语义保证。 同一个函数签名,可以有强契约,也可以完全没有契约。 契约分层(建议团队统一术语) 前面的 cancel(order) 示例主要覆盖了行为契约与失败契约。 在真实项目里,建议把契约至少拆成下面 6 类,一起设计: 数据契约:输入/输出的数据形状、类型、取值范围、单位、精度、是否可空。 例:金额必须 > 0,币种必须是 ISO 4217,时间必须是 UTC。 状态契约:状态机允许哪些迁移,不允许哪些迁移。 例:订单只能 CREATED -> PAID -> SHIPPED,不能 SHIPPED -> CREATED。 不变式契约:跨方法、跨状态始终成立的事实。 例:订单总额 = 明细金额之和 + 运费 - 优惠;库存不可为负。 行为契约:调用成功时,调用方可以依赖什么结果与语义。 例:reserve_stock() 成功后,一定返回预留记录 ID,且库存已被占用。 失败契约:违约/异常时返回什么错误、错误是否可重试、是否有副作用残留。 例:重复请求返回 409;超时返回 503 且标记 retryable=true。 副作用契约:方法会修改哪些外部状态(DB、缓存、消息、文件),顺序如何,失败如何补偿。 例:先写 DB 再写 outbox;缓存删除失败不影响主事务提交。 实践指南 / 步骤 先写目的,不写实现 明确本次功能要改变什么业务结果。 列不变量清单 逐条写出“绝对不能被破坏”的规则。 定义契约 为核心行为定义前置条件、后置条件、失败语义,并补齐数据/状态/副作用契约。 再落实现 数据库、框架、缓存、消息等实现细节后置。 用测试锁契约 测试验证的是契约,不是某一版实现细节。 可运行示例 示例 1:无契约(可运行,但语义模糊) class Order: def __init__(self, status): self.status = status def cancel(order: Order) -> Order: if order.status != "CREATED": return order order.status = "CANCELLED" return order if __name__ == "__main__": order = Order("PAID") after = cancel(order) print(after.status) 问题:失败是“静默返回”,调用方必须自己猜“这次到底算成功还是失败”。 ...

2026年2月11日 · 4 分钟 · map[name:Jeanphilo]