ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

LeetCode 237 无头指针链表节点删除:LeetCode-Go 中的值拷贝解法与内存语义详解

LeetCode 237 无头指针链表节点删除:LeetCode-Go 中的值拷贝解法与内存语义详解 LeetCode 237 无头指针链表节点删除LeetCode-Go 中的值拷贝解法与内存语义详解【免费下载链接】LeetCode-Go✅ Solutions to LeetCode by Go, 100% test coverage, runtime beats 100% | LeetCode 题解项目地址: https://gitcode.com/GitHub_Trending/le/LeetCode-Go导读LeetCode 237Delete Node in a Linked List是链表操作中极具代表性的一道题它要求你在拿不到链表头结点head的情况下仅凭待删除节点本身完成单链表节点的删除。本文基于 LeetCode-Go 仓库中 237 题题解文档 展开结合仓库源码 237. Delete Node in a Linked List.go、链表数据结构实现 与对应的单元测试带你彻底理解值拷贝 指针跳跃这一经典解法的原理、边界条件与内存语义并掌握在 Go 语言中如何验证该解法。题目核心为什么这道题不给你头结点题目原文要求Write a function todelete a nodein a singly-linked list. You willnotbe given access to theheadof the list, instead you will be given access tothe node to be deleteddirectly.翻译过来就是删除单链表中的某个节点但你拿不到链表的头结点函数唯一能拿到的是待删除节点本身的指针。这与常规的链表删除操作有本质区别常规删除需要从头遍历找到待删节点的前驱节点然后让pre.Next cur.Next本题删除无法回溯到前驱因为单链表节点只有Next指针没有Prev指针。题目的约束条件Constraints也为此提供了保证链表节点数量范围[2, 1000]节点值范围-1000 Node.val 1000链表中每个节点的值唯一这一点保证按值查找不会产生歧义待删除的node一定在链表中并且一定不是尾节点not a tail node。其中不是尾节点这一条是整套解法成立的前提后文会详细展开。核心思路用下一个节点的值覆盖自己原文档的解题思路非常精炼其实就是把后面的结点都覆盖上来即可。或者直接当前结点的值等于下一个结点Next 指针指向下下个结点这样做也可以只不过中间有一个结点不被释放内存消耗多一些。由于拿不到前驱节点无法修改前驱的Next指针我们换一个视角不删除物理上的自己而是让自己变成下一个节点。具体分两步值覆盖把node.Next.Val拷贝到node.Val当前节点继承下一个节点的值指针跳跃把node.Next指向node.Next.Next跳过一个节点。从外部观察者的角度看链表中某个值消失了剩下的节点依然连续、有序效果等同于删除了目标节点。Go 实现与逐行讲解LeetCode-Go 仓库中的完整解法如下源码见 237. Delete Node in a Linked List.gopackage leetcode import ( github.com/halfrost/LeetCode-Go/structures ) // ListNode define type ListNode structures.ListNode /** * Definition for singly-linked list. * type ListNode struct { * Val int * Next *ListNode * } */ func deleteNode(node *ListNode) { node.Val node.Next.Val node.Next node.Next.Next }逐行解读代码作用type ListNode structures.ListNode类型别名直接复用仓库 structures/ListNode.go 中定义的公共链表节点结构避免每个题解重复定义node.Val node.Next.Val将后继节点的值拷贝到当前节点完成值覆盖node.Next node.Next.Next让当前节点直接指向后继的后继完成指针跳跃其中ListNode结构体定义在 structures/ListNode.go 中// ListNode 是链接节点 // 这个不能复制到*_test.go文件中。会导致Travis失败 type ListNode struct { Val int Next *ListNode }可以看到这就是标准的单链表节点一个Val字段存值一个Next指针指向下一个节点。本题解法只依赖这两个字段因此可以放心使用。图解执行过程以示例 2 为例链表4 - 5 - 1 - 9删除值为1的节点第三个节点删除前 4 - 5 - 1 - 9 ↑ node 指向值为 1 的节点 Step 1值覆盖node.Val node.Next.Val 4 - 5 - 9 - 9 ↑ 当前节点值变成 9但 Next 仍指向原 9 节点 Step 2指针跳跃node.Next node.Next.Next 4 - 5 - 9 - 9 ↑ ↑ └—— Next 跳过原 9 节点直接指向 nil 最终链表4 - 5 - 9从逻辑层面看值为 1 的节点被删除了从物理层面看被跳过的原 9 号节点仍然存在只是不再被链表可达。为什么不能删除尾节点是硬性前提解法的两条语句都要求node.Next非空node.Next.Val需要读取后继节点的值node.Next.Next需要读取后继节点的后继指针。如果node是尾节点则node.Next nil访问node.Next.Val会直接触发nil 指针解引用在 Go 中表现为运行时 panic。这正是题目约束node isnot a tailnode的原因——它保证了node.Next一定存在解法天然安全。从源码结构看237. Delete Node in a Linked List.go 的deleteNode函数没有做任何空指针防御正是依赖这一约束。读者在自行扩展该题、去掉约束时需要自行补充node nil || node.Next nil的判断。内存语义Go 与 C/C 的本质差异原文档特别提到一个细节只不过中间有一个结点不被释放内存消耗多一些。这背后是 Go 与 C/C 在内存管理上的本质差异C/C 视角标准解法通常是node-val node-next-val; node-next node-next-next; delete node-next;或类似写法必须手动释放被跳过的节点否则造成内存泄漏Go 视角Go 拥有自动垃圾回收GC被跳过的节点一旦不再被任何引用指向就会被 GC 自动回收无需也不能手动释放。因此 LeetCode-Go 的题解只做值覆盖与指针跳跃两步干净利落。所以原文档所说的内存消耗多一些是指在deleteNode执行完毕到 GC 真正回收之间那个被跳过的节点对象仍然占用堆内存但从长期运行角度看Go 的 GC 会保证其最终被回收不存在 C/C 那种需要开发者负责的泄漏问题。这也是使用 Go 编写算法题解时值得留意的一个语言特性。复杂度分析指标分析时间复杂度O(1)。仅执行两次指针/值拷贝不依赖链表长度也没有遍历空间复杂度O(1)。只使用常数个临时空间甚至不需要临时变量对比常规的从头遍历找前驱删除方式O(n) 时间本题解法在时间上是最优的——这也是该解法被 LeetCode 官方收录、并被大量题解复用的原因。整个 LeetCode-Go 仓库以100% test coverage为质量标准见 gotest.sh 中go test -covermodeatomic -coverprofilecoverage.txt ./leetcode/...的整仓覆盖率收集方式本题的 O(1) 解法同样配有对应测试验证。测试验证仓库中的单元测试LeetCode-Go 为本题编写了测试文件 237. Delete Node in a Linked List_test.go。其中值得关注的有两点1. 直接调用deleteNode的冒烟测试deleteNode(structures.Ints2List([]int{1, 2}))这一行构建了1 - 2的链表直接调用deleteNode删除头节点值为 1 的节点非尾节点验证了题解不会 panic、且链表逻辑正确。因为deleteNode的入参是待删除节点指针而非头结点测试无法像常规题目那样通过返回结果断言所以采用执行不崩溃的冒烟方式覆盖该函数。2. 复用removeElementsLeetCode 203 题解法做行为验证测试主体通过removeElements(structures.Ints2List(p.one), p.n)来构造预期行为——该函数是 LeetCode 203 题Remove Linked List Elements的解法采用虚拟头节点 前驱指针的常规删除方式与 237 题解法形成对照func removeElements(head *ListNode, val int) *ListNode { if head nil { return head } newHead : ListNode{Val: 0, Next: head} pre : newHead cur : head for cur ! nil { if cur.Val val { pre.Next cur.Next } else { pre cur } cur cur.Next } return newHead.Next }两种删除思路放在同一个测试文件中恰好可以对照学习203 题有头结点虚拟头节点统一边界处理遍历时用pre.Next cur.Next摘除目标节点是改前驱的经典写法237 题无头结点node.Val node.Next.Valnode.Next node.Next.Next是改自己的取巧写法。辅助数据结构工具测试中还使用了 structures/ListNode.go 提供的两个常用转换函数Ints2List(nums []int) *ListNode把整数切片构造成链表是测试中构造输入的标准方式List2Ints(head *ListNode) []int把链表还原成整数切片方便断言该函数内置了 100 层深度限制若链表出现环会主动 panic 提示避免死循环见structures/ListNode.go中limit : 100的注释。变体与易错点总结围绕本题可以从以下几个角度做延伸思考如果题目改为删除尾节点必须退回常规解法先找到前驱节点再摘除或者直接置空值覆盖法在尾节点上必然空指针异常。如果题目改为给定头结点可以直接用常规遍历删除逻辑更直观但时间变为 O(n)237 题的取巧之处正在于 O(1) 完成删除。如果链表节点值不唯一值覆盖法不受影响因为题目直接给了节点指针而非值但若采用按值查找 删除值不唯一会引入歧义需要按指针定位。如果节点是结构体而非整型值覆盖法要求类型支持赋值拷贝若节点携带大体积数据或内部指针值拷贝的开销与语义需要额外评估Go 中结构体赋值是浅拷贝共享引用字段。内存释放差异Go 依赖 GC 自动回收被跳过的节点无需手动释放换用 C/C 实现时必须自行delete被跳过的节点否则内存泄漏。小结LeetCode 237 的值拷贝删除法是链表专题中最具巧思的解法之一在拿不到头结点、无法访问前驱的约束下通过node.Val node.Next.Val; node.Next node.Next.Next两步完成 O(1) 时间的节点删除。LeetCode-Go 仓库用极简的 Go 实现 配套单元测试完整呈现了这一思路同时依托 structures 公共模块统一了链表节点的定义与测试工具充分体现了以公共数据结构支撑全部题解的工程化组织方式。掌握本题后建议对照 203 题给定头结点删除一并阅读两种删除范式互为镜像是理解单链表指针操作的最佳入门组合。【免费下载链接】LeetCode-Go✅ Solutions to LeetCode by Go, 100% test coverage, runtime beats 100% | LeetCode 题解项目地址: https://gitcode.com/GitHub_Trending/le/LeetCode-Go创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
返回列表