news 2026/4/18 9:18:29

从“能跑”到“能读”:Lc.450删除二叉搜索树中的节点 代码重构记录

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
从“能跑”到“能读”:Lc.450删除二叉搜索树中的节点 代码重构记录

最近在做 LeetCode 450(删除二叉搜索树中的节点)时,我写出了一版执行用时 0ms,击败 100% 用户的代码。 从算法效率上讲,它是完美的:纯指针操作,没有递归开销,逻辑硬核。 但从工程角度讲,它是十足的垃圾代码,可能下周我自己都看不明白我写的什么玩意了。

今天这篇博客,我想通过复盘这段代码,聊聊如何把代码从“能跑”进化到“能读”。


第一阶段:原始代码

我的核心思路非常直接:物理断开与重连。 不搞递归那种值拷贝,直接找到节点,找到它的前驱(左子树最右节点),把前驱拎出来,物理替换掉目标节点。

这是我提交的原始版本(已通过所有测试用例):

// 此代码虽然性能极致,但是问题一堆,写得一塌糊涂。 class Solution { public: TreeNode* deleteNode(TreeNode* root, int key) { // 【段落 1】 处理空节点的情况 if (!root) { return nullptr; } // 【段落 2】 处理key节点是根节点的情况 // 这里的逻辑:没有父节点,直接操作 root if (root->val == key) { if (!root->left && !root->right) { return nullptr; } else if (!root->left && root->right) { return root->right; } else if (root->left && !root->right) { return root->left; } else if (root->left && root->right) { //复杂的双子节点处理逻辑 TreeNode* old1 = root;// 【角色:被删节点】 (这里就是root本身) TreeNode* old2 = root;// 【角色:前驱节点的父节点】 (初始跟被删节点一样) root = root->left; while (root->right) {//潜在巨雷 root去当游标了 old2 = root; root = root->right; } if (old1 == old2) { //说明这个左节点就是左子树最右的节点了 root->right = old1->right; return root; } else if (old1 != old2) { old2->right = nullptr; root->right = old1->right; TreeNode* t = root; while (t->left) { t = t->left; } t->left = old1->left; return root; } } } // 【段落 3】 key节点不是根节点的情况 TreeNode* old0 = root; // 保存原始根 (最后要返回它) TreeNode* old1 = root; // 【角色:被删节点的父节点】 (上半部分没有,因为上半部分没父节点) TreeNode* old2 = root; // 【角色:被删节点 / 目标节点】 (初始是root,后面会变成目标) // 1. 先找到要删除的节点 while (root && root->val != key) {//潜在巨雷 root去当游标了 if (root->val > key) { old1 = root;// old1 紧跟 root ,充当父节点 root = root->left; } else { old1 = root;// old1 紧跟 root ,充当父节点 root = root->right; } } // 没找到直接返回 if (!root) { return old0; } // 到这里,root 指向的就是【要被删除的节点】 // 下面的逻辑开始处理删除 root (即 old2) 的操作 // 情况 1: 叶子节点 if (!root->left && !root->right) { if (old1->left == root) { old1->left = nullptr; return old0; } else { old1->right = nullptr; return old0; } } // 情况 2: 只有右孩子 else if (!root->left && root->right) { if (old1->left == root) { old1->left = root->right; return old0; } else { old1->right = root->right; return old0; } } // 情况 3: 只有左孩子 else if (root->left && !root->right) { if (old1->left == root) { old1->left = root->left; return old0; } else { old1->right = root->left; return old0; } } // 情况 4: 左右孩子都有 (最晕的地方来了) else if (root->left && root->right) { // 此时 root 是【要被删除的节点】 old2 = root;// 【角色:被删节点】 --> 对应上半部分的 old1 TreeNode* old3 = root;// 【角色:前驱节点的父节点】 --> 对应上半部分的 old2 // 开始寻找前驱 (左子树的最右节点) root = root->left; while (root->right) { old3 = root;// old3 紧跟,记录前驱的父亲 root = root->right; } // 循环结束后,root 变成了【前驱节点】(用来顶替被删节点的那个人) // 下面的逻辑和上半部分是完全镜像的,只是变量名下标+1了 // 特例:前驱节点的父节点 就是 被删节点本身 (即左子树没有右分支) if (old3 == old2) { if (old1->left == old2) { root->right = old2->right; old1->left = root; return old0; } else { root->right = old2->right; old1->right = root; return old0; } } // 通用情况:前驱节点在深处 // 此时:old1(爷爷), old2(被删), old3(前驱父), root(前驱) else if (old3 != old2) { old3->right = nullptr; root->right = old2->right; TreeNode* t = root; while (t->left) { t = t->left; } t->left = old2->left; if (old1->left == old2) { old1->left = root; return old0; } else { old1->right = root; return old0; } return old0; } } return root; } };

第二阶段:局部分析

这代码就是我一边想一边写的产物,脑子里在想各种情况如何处理,然后就实时写出各分支,游标、变量都是随时需要随时定义随时使用。等写完我自己再复盘的时候都没绷住,虽然能通过,但是实在是太有槽点了,以至于我必须新开一篇blog记录一下,我到底是怎么写出这种代码,以及该如何改进的。

我们一行行来分析问题出在哪:

1. 变量命名陷阱

TreeNode* old0 = root; TreeNode* old1 = root; TreeNode* old2 = root; TreeNode* old3 = root; // 在某些分支里还有 old3
  • 问题:old家族大团结,我的小巧思。但old是什么意思?旧的?老的?old0old3之间是什么关系?

这种命名迫使阅读者必须在脑子里维护一张映射表(old1=父亲, old2=目标…)。一旦代码逻辑变复杂,这张表瞬间就会崩塌。代码失去了自解释能力。

更要命的是,上下两部分的old1、old2甚至代表的含义都不一样。

2. 逻辑分裂

if (root->val == key) { ... } // 处理 Root // ... while (root && root->val != key) { ... } // 处理非 Root
  • 问题:上半部分和下半部分的删除逻辑(叶子、单边、双边)是完全一样的!

仅仅因为根节点没有父节点,我就把代码复制了一遍。这导致任何一次逻辑修正(比如修复 BST 断链 bug)都需要改两个地方,漏改一处就是事故。

3. 指针复用混乱

while (root && root->val != key) { root = root->left; // root 变成了游标 } return old0; // 最后不得不返回保存的 old0
  • 问题:root指针在函数里身兼数职:它是树的入口,是遍历的游标,又是某些分支的返回值。
    这种“一鱼多吃”的做法极大地增加了认知负荷。输入参数应尽量保持只读或语义稳定。

4. 控制流层层递进

代码缩进呈现可怕的>>>>箭头状。

  • 问题:ififelseelse
    这种深层嵌套掩盖了主要逻辑流。

第三阶段:架构调整

在开始改代码之前,我们需要先在脑海里重构模型

如何消除 Root 和非 Root 的区别?如何让逻辑变清晰? 答案是:逻辑分层

原始代码是“边找边删”,重构代码应该是流水线作业:

  1. Search(定位):统一找到目标节点target和它的父节点parent
  2. Decide(决策):决定谁来接班(replacement)。
  3. Link(缝合):统一执行指针连接操作。

核心:在算法眼里,Root 只是一个parentnullptr的普通节点。引入parent变量,且专门处理一下parentnullptr时的情况,两套逻辑瞬间就能合并为一套。


第四阶段:重构

基于上述分析,保留我核心的“物理断开前驱”的高效逻辑,重构后的代码如下。

请对比阅读:

class Solution { public: TreeNode* deleteNode(TreeNode* root, int key) { // 1. 处理空树,减少一层嵌套 if (!root) return nullptr; // 2. 统一查找逻辑 // 使用语义化变量:target (目标), parent (父亲) TreeNode* target = root; TreeNode* parent = nullptr; while (target && target->val != key) { parent = target; // 父节点紧跟其后 if (key < target->val) { target = target->left; } else { target = target->right; } } // 没找到?直接返回 if (!target) return root; // 3. 决定谁来接班 (replacement) TreeNode* replacement = nullptr; // 情况 A: 左右双全 (最复杂的逻辑,保留原版的高效思路) if (target->left && target->right) { // 语义化变量:predecessor (前驱), predParent (前驱的父亲) TreeNode* predecessor = target->left; TreeNode* predParent = target; // 寻找左子树的最右节点 while (predecessor->right) { predParent = predecessor; predecessor = predecessor->right; } // 处理前驱的“身后事” if (predParent != target) { predParent->right = predecessor->left; // 爷爷接管孙子的左孩子 predecessor->left = target->left; // 前驱接管目标的左臂膀 } // 前驱接管目标的右臂膀 predecessor->right = target->right; // 选定接班人 replacement = predecessor; } // 情况 B: 只有左孩子 else if (target->left) { replacement = target->left; } // 情况 C: 只有右孩子 else if (target->right) { replacement = target->right; } // 情况 D: 叶子节点 (replacement 保持为 nullptr) else { replacement = nullptr;//这一块可以不写,写也只是为了清晰一点。 } // 4. 【缝合阶段】统一执行连接 // 这一步彻底消除了 Root 和 非 Root 的代码重复 if (!parent) { // 如果没有父亲,说明删的是 Root,直接换头 root = replacement; } else { // 如果有父亲,让父亲指向新的接班人 if (parent->left == target) { parent->left = replacement; } else { parent->right = replacement; } } // 此时整棵树的结构已完整,统一返回 return root; } };

总结

old0/old1/old2/old3target/parent/predecessor/predParent,从“两坨逻辑”到“一条流水线”。

这次重构没有改变算法的时间复杂度(依然是 0ms),但它改变了代码的生命周期

  • 原始代码:写完即死,难以维护,我自己写自己看都绷不住。
  • 重构代码:逻辑清晰,结构稳健,任何人接手捋捋都能看懂。

写出计算机能跑的代码只能算是一种本能,而写出让人能读懂,最起码能让未来的自己看懂的代码(难绷),才是真正的本事。以前看到过一句话,好看的武器一定好用。我想好的代码也应该具备这种品质:优雅美丽,高效简洁。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/4/18 8:01:38

2025 年值得选择的 TVC 视频制作服务推荐

在 2025 年&#xff0c;我们为您精心整理了年度最值得选择的 TVC 视频制作服务商榜单。这些顶级的 AI TVC 视频服务商各具特色&#xff0c;能够满足不同企业的需求。无论是品牌宣传还是产品推广&#xff0c;它们都能为您提供高质量的视频制作&#xff0c;帮助您在激烈的市场竞争…

作者头像 李华
网站建设 2026/4/18 8:05:30

青年演员李俊霆获Astria盛典“最佳演技突破演员”

12月14日&#xff0c;Astria星雅奖全球电视剧颁奖盛典在泰国曼谷举行。前段时间热播的谍战纪实传奇剧《沉默的荣耀》,与英国传记历史剧《王冠》第六季、战争剧《SAS&#xff1a;叛逆勇士》&#xff0c;共同获评最佳电视连续剧&#xff1b;英国演员加里奥德曼、莫妮卡多兰分获最…

作者头像 李华
网站建设 2026/4/14 19:35:12

【必藏】后端开发寒冬已至?AI算法岗年薪35W+,百万缺口等你来填!

当前后端开发岗位面临锐减、薪资停滞困境&#xff0c;而AI算法岗因百万级人才缺口迎来红利期&#xff0c;平均年薪达35W。传统开发者可通过Python/Java基础和分布式系统经验&#xff0c;借助主流框架快速转型AI工程。文章提供大模型应用开发实战资源&#xff0c;帮助开发者抓住…

作者头像 李华
网站建设 2026/4/18 8:27:20

平台运营指南:新榜小豆芽指纹浏览器的专属安全方案

做抖音、小红书、视频号这类高风控平台运营&#xff0c;每天都在 “踩雷边缘” 试探 —— 多账号共用 IP 被批量限流&#xff0c;私信漏看错失变现机会&#xff0c;逆向方案突然失效导致账号登不上&#xff0c;甚至辛苦运营的账号直接被封。而同类工具只做基础账号管理&#xf…

作者头像 李华
网站建设 2026/4/17 12:27:41

Looki L1:当AI睁开“双眼”,感知物理世界的革命已然到来

各专栏更新如下&#x1f447; 大模型初探分享零基础AI学习经历 OAI-5G开源通信平台实践 OpenWRT常见问题分析 5G CPE 组网技术分享 Linux音视频采集及视频推拉流应用实践详解 得力工具提升工作效率 Looki L1&#xff1a;当AI睁开“双眼”&#xff0c;感知物理世界的革命已…

作者头像 李华
网站建设 2026/4/17 20:20:33

HTML可视化监控TensorRT推理过程中的GPU利用率

HTML可视化监控TensorRT推理过程中的GPU利用率 在部署深度学习模型到生产环境时&#xff0c;开发者常常面临一个棘手的问题&#xff1a;明明模型结构没有变化&#xff0c;为什么实际推理延迟居高不下&#xff1f;吞吐量始终上不去&#xff1f;这时候&#xff0c;仅仅看日志或跑…

作者头像 李华