LeetCode 230 self.k 写法的执行顺序学习笔记

这篇是我重做 230. Kth Smallest Element in a BST · 二叉搜索树中第 K 小的元素 时整理的笔记。上一篇用的是 nonlocal 加剪枝的写法,这次换成 self.k、不剪枝的写法,重点看递归到底按什么顺序执行。 These are my notes from redoing LeetCode 230. The previous post used nonlocal with pruning. This time I use self.k with no pruning, and focus on the exact order in which the recursion runs. 一句话总结本质: 二叉搜索树(BST)的中序遍历(in-order traversal)是升序,所以每个节点在自己的调用里把 k 减 1,减到 0 的那个节点就是第 k 小。 In one sentence: A BST’s in-order traversal is ascending, so each node decrements k once, inside its own call, and the node that brings k to 0 is the k-th smallest. ...

5 min · 1049 words · Jiaju
LC 78 子集的回溯决策树:往下走 = append,往上退 = pop / Backtracking decision tree for LC 78 Subsets

回溯(Backtracking)学习笔记:走迷宫、子集与全排列

这篇是我做 78. Subsets · 子集 时整理的回溯(backtracking)笔记。一开始我能背模板,但说不清 path.pop() 到底在干嘛、为什么子集不会重复。后来配合动画一步步走,才真的看懂。 This is my note on backtracking, written while working on LeetCode 78 Subsets. At first I could recite the template, but I couldn’t explain what path.pop() was actually doing, or why subsets never come out duplicated. Stepping through it with animations is what finally made it click. 一句话总结本质: 回溯就是在一棵决策树上做 DFS。每走一步都是三件事:做选择(append)→ 往下走(递归)→ 撤销选择(pop)。撤销是为了回到岔路口,让下一个选项拿到一条干净的 path。 Backtracking is DFS on a decision tree. Every step is three moves: choose (append) → explore (recurse) → un-choose (pop). Un-choosing walks you back to the fork, so the next option starts from a clean path. ...

8 min · 1595 words · Jiaju

子集回溯树学习笔记:节点的 start 和它自己的循环

这篇接着上一篇 回溯学习笔记。我在给 nums = [1,2,3,4] 画 78. Subsets 的递归树时,好几次把节点和循环对错了位置,比如把 range(2,4) 安到 [1,2,3] 头上。这篇把怎么对齐整理清楚。 This follows my earlier backtracking note. While drawing the recursion tree of LC 78 Subsets for nums = [1,2,3,4], I kept attaching loops to the wrong nodes, for example putting range(2,4) on [1,2,3]. This note is about how to line them up. 一句话总结: 节点的 start 是几,它自己的循环就是 range(几, 4)。 循环里的每个 i 产生一个孩子,孩子的 start 是 i + 1。 A node’s own loop always uses its own start: range(start, 4). Each i in that loop makes one child, and that child’s start is i + 1. ...

7 min · 1300 words · Jiaju
二叉树后序递归:左 → 右 → 自己,处理顺序 2 → -4 → -6 → -1 → 3 → 1 / Binary tree post-order recursion: left → right → self

二叉树递归调用顺序学习笔记(左 → 右 → 自己)

这篇是我做 124. Binary Tree Maximum Path Sum · 二叉树中的最大路径和 时整理的笔记。卡住我的不是公式,而是递归到底按什么顺序走。 This is a note from working on LeetCode 124. What tripped me up wasn’t the formula. It was the order the recursion actually runs in. 一句话总结本质: 每进入一个节点,先一路往左钻到底;左边返回后,同一个节点才去右边;进了右子树,又重新「先左后右」。两边都返回后,才处理自己。完整规则:左 → 右 → 自己(后序遍历,post-order)。 Every time you enter a node, you dive all the way left first. Once the left side returns, that same node goes right. Inside the right subtree, “left first, then right” starts over. Only after both sides return does the node process itself. The full rule: left → right → self (post-order). ...

5 min · 876 words · Jiaju

LeetCode 230 二叉搜索树第 K 小元素学习笔记

这篇是我刷 230. Kth Smallest Element in a BST · 二叉搜索树中第 K 小的元素 时整理的笔记。重点不在代码多难,而在于亲眼看着递归一步步走,搞清楚它什么时候往右走、什么时候停。 一句话总结本质: 二叉搜索树(BST)的中序遍历(in-order traversal)就是升序,所以按中序数到第 k 个节点,它就是第 k 小。 一、为什么中序遍历 = 升序 BST 的规则是:左子树所有值 < 根 < 右子树所有值。中序遍历的顺序是左 → 根 → 右,正好把这条规则从小到大"读"出来。 所以不用排序、不用存数组,只要边遍历边倒数:每访问一个节点就 k -= 1,k 变成 0 的那个节点就是答案。 二、代码 class Solution: def kthSmallest(self, root: Optional[TreeNode], k: int) -> int: ans = None def dfs(node): nonlocal k, ans if not node: return dfs(node.left) if k == 0: return # found on the left k -= 1 # visit node if k == 0: ans = node.val return dfs(node.right) dfs(root) return ans 几个关键行: dfs(node.left):先一路往左钻到最小值。 k -= 1:这就是"访问"当前节点,相当于数了一个。 if k == 0: ans = node.val:数够了,记答案。 if k == 0: return(紧跟在 dfs(node.left) 后面):剪枝(pruning),左边已经找到答案了,当前节点和右边都不用看。 三、逐步图解(可以点) 用题目示例的树 [5,3,6,2,4,null,null,1]。节点按中序位置从左到右排开,所以屏幕上从左到右就是从小到大。 ...

2 min · 320 words · Jiaju
Seattle skyline with the Space Needle and Mt. Rainier at sunset

Seattle🌊 & Rainier⛰️

Pacific Northwest · Road Trip Seattle 🌊 & Rainier ⛰️ 📅 Sep 30 – Oct 1🚗 自驾🏔 雷尼尔雪山🌇 湖边晚霞 01Sep 30 · Wed准备 & 接机 5:00 PM🚗 拿车 6:00 PM🛒 T&T 购物买菜 地图 ↗ 7:00 PM✈️ 去 Sea-Tac(Seattle–Tacoma 机场)接机 地图 ↗ 02Oct 1 · Thu上山看雪,下山看日落 7:00 AM👭 出发去接 Yolo 8:00 AM☕️ Post & Pour 网红打卡 地图 ↗打卡完继续往雪山开 Then🏔 Mt. Rainier 雷尼尔雪山 地图 ↗记得带国家公园门票 / 年卡,出发前看一眼路况和天气 Sunset🌇 下山 → 看晚霞夕阳二选一:Medina Park 地图 ↗ | Kerry Park 地图 ↗(封面这张就是 Kerry Park 的经典机位) 03🥢 Dinner晚饭候选 🍜Ramen DANBO福冈豚骨拉面 地图 ↗ 🍲沸点 Boiling Point台式小火锅 地图 ↗ 🐑快乐小羊 Happy Lamb蒙古火锅 地图 ↗ 🥟鼎泰丰 Din Tai Fung小笼包 地图 ↗ See you in the Emerald City ✨

Jiaju

手搓 Agent:权限护栏与 Hook 系统学习笔记

这是我手搓 coding agent 系列的第 3、4 节笔记(前两节是最小 agent loop 和多工具 dispatch)。 前两节做出来的 agent 有个吓人的特点:模型说调什么工具,循环就直接执行,包括 rm -rf /。这节就是给它装刹车,并且把刹车做成一个可扩展的系统。 一句话总结这两节的本质: s03:在工具执行前插一道检查,决定放行 / 询问 / 拒绝。 s04:把这道检查从「写死在循环里」抽象成「往事件上挂回调」,以后加功能不用再改循环。 一、s03 权限护栏:三道闸门 之前的循环是"信任模型"——handler(**args) 直接执行。s03 只做一件事:在执行之前加一段检查。整个循环、工具、dispatch 表都没动,只在执行前多调了一个 check_permission()。 核心是三道闸门,从严到松: 工具要执行了 │ 闸门1 DENY_LIST ──命中──→ ⛔ 直接拒,连问都不问 │ 没命中 闸门2 RULES ──命中──→ 闸门3 ask_user ──你按 N──→ 拒绝 │ 没命中 └──你按 y──→ 放行 │ ✅ 执行 闸门 1:硬拒绝表(deny list) 一张黑名单,命中直接拒: DENY_LIST = ["rm -rf /", "sudo", "shutdown", "reboot", "mkfs", "dd if=", "> /dev/sda"] def check_deny_list(command: str) -> str | None: for pattern in DENY_LIST: if pattern in command: # 字符串包含就算命中 return f"Blocked: '{pattern}' is on the deny list" return None # 没命中 → None 这里有个贯穿全节的约定:命中返回理由字符串(真值),没命中返回 None(假值)。这样上层一句 if reason: 就能判断。 ...

4 min · 639 words · Jiaju

给 Agent 加工具:Tool Dispatch 学习笔记

这篇是我学「给 Agent 加工具」(s02 Tool Use)时整理的笔记。接着上一篇手搓最小 Agent Loop往下走。 一句话总结这一节的核心: 给 agent 加一个工具,只需要"加一行"——循环代码完全不动,新工具注册进一张**派发表(dispatch map)**就行。 我用的还是通义千问 qwen-plus(OpenAI 兼容接口)。 一、为什么要给它多个工具 s01 里 agent 只有一个 bash。读文件要 cat,写文件要 echo "..." > file,找文件要 find。问题是:模型脑子里想的是"读这个文件",却要拼出一条 cat path/to/file 的命令——多了一层翻译,浪费 token,还容易拼错。 s02 的做法:给它 5 个专用工具,让它直接表达意图。 工具 干什么 read_file 读文件 write_file 写文件 edit_file 把文件里一段旧文字换成新文字 glob 按模式找文件,如 *.py bash 兜底,其它都不合适时才用 system prompt 里写一句"能用专用工具就别用 bash",模型就会优先挑合适的工具。 二、新概念:工具派发(dispatch) s01 只有 bash,模型一说要工具,代码闭眼调 run_bash 就行。现在有 5 个工具,模型说"我要 read_file",代码得按名字找到对应的那个 Python 函数去执行——这就是 dispatch。 实现靠一张派发表:一个把"工具名字符串"映射到"真正函数"的字典。 TOOL_HANDLERS = { "bash": run_bash, "read_file": run_read, "write_file": run_write, "edit_file": run_edit, "glob": run_glob, } ⚠️ 易错点:值是 run_bash,不要加括号。 run_bash(不加括号)= 把函数本身存进表,需要时再调 run_bash()(加括号)= 立刻执行它,存进表的是执行结果,错了 ...

3 min · 429 words · Jiaju

手搓最小 Agent Loop 学习笔记

这篇是我手搓最小 Agent Loop(s01)时整理的笔记。一句话总结它的本质: 大模型本身只会"说话"。把它的话真的执行掉、再把结果喂回去,它就从"会聊天"变成了"会干活"——这就是 agent。 我用的是通义千问 qwen-plus(走 OpenAI 兼容接口),但下面的道理换任何模型都一样。 一、Agent 到底是什么 一开始我有个困惑:大模型不是只会问答吗?它怎么还能帮我创建文件? 答案是:文件不是模型创建的,是我的代码创建的。 模型自始至终只做了一件事——输出文字。 拆开看,一个 agent 其实是三个角色凑在一起: 角色 是谁 干什么 🧠 动嘴的 大模型 只会输出文字,包括"我想用某个工具"(tool_calls) 📞 传话的 agent loop 代码 读模型的话 → 转给工具 → 把结果再转回给模型 🔧 动手的 run_bash(subprocess) 真的在电脑上执行命令 所以当我说"创建一个 hello.py",发生的是: 我说"建 hello.py" ↓ [模型] 动嘴:我想执行 echo ... > hello.py ← 只是输出文字,文件还不存在 ↓ [代码 run_bash] 动手:真的执行命令 ← ✨ 文件在这一步才诞生 ↓ [把结果喂回] → [模型] 看到成功 → 说"搞定了" ← 不再要工具,结束 关键认知:模型不会"主动调用"工具,它连发消息的能力都没有。 是我写的 for call in msg.tool_calls: 那段循环,替模型把话送到 bash 那儿。没有这段代码,模型说一万句"我想执行 ls"都没用。 ...

3 min · 446 words · Jiaju

FastAPI 进阶:搭一个能跑 Agent 的后端

这是我学 FastAPI 进阶部分整理的笔记。和入门那篇(查询参数、请求体、response_model)不同,这次的目标很明确:搭一个能真正跑 agent 的后端——能调 LLM、能存对话记忆、能扛并发、能鉴权。 我现在是 vibe coding 的方式在学:不追求闭卷默写,而是「读得懂 AI 写的代码 + 能指出哪里不对 + 能说清自己要什么」。所以下面每个知识点我都标了两件事——它对写 agent 有什么用,以及大厂面试会怎么考。 先一句话串起全篇: 一个 agent 后端的骨架 = 接口(处理请求)+ 错误处理(诚实报错)+ 异步(扛并发)+ 数据库(存记忆)+ 依赖注入(管连接和鉴权)。 一、HTTPException:出错了要「诚实地报错」 痛点:硬返回 None 是在「撒谎」 查一只不存在的宠物,如果这么写: @app.get("/pets/{pet_id}") def get_pet(pet_id: int): pet = pets_db.get(pet_id) # 没找到,pet = None return pet # 直接返回 None 后果是:状态码还是 200 OK,body 是 null。这等于明明失败了却告诉前端「成功了」。前端通常这么判断: if (response.ok) { // 200 → ok 是 true,以为成功了 showPet(data.name); // data 是 null → 💥 报错 "Cannot read 'name' of null" } 更糟的是用中括号取值 pets_db[999]——直接 KeyError,FastAPI 兜底返回 500,把「用户查了个不存在的 id」(用户的锅,4xx)甩锅成「服务器坏了」(服务器的锅,5xx)。 ...

5 min · 952 words · Jiaju