
1. 题目全景拆解LeetCode 707到底在考什么如果你是准备面试的开发者或者正在系统刷题补数据结构基本功那LeetCode 707几乎是一道绕不开的题。它叫“设计链表”英文名Design Linked List属于LeetCode热门100题序列中的常客。第一次看到这个题名很多人觉得简单不就是实现一个链表吗但真正动手写的时候才发现这道题的难点根本不在“链表”本身而在于边界条件的处理和对索引语义的理解。我见过不少刷了几百题的人在这道题上栽跟头原因不是不会写链表而是对“第几个节点”这件事的理解不够精确。先搞清楚这道题到底要求什么。题目要求你设计一个自定义链表类需要支持五个核心操作get(index)获取链表中第index个节点的值、addAtHead(val)在链表头部插入节点、addAtTail(val)在链表尾部追加节点、addAtIndex(index, val)在指定索引位置插入节点、deleteAtIndex(index)删除指定索引位置的节点。看起来就是最基础的五种操作但实现质量的高低直接反映你对链表数据结构的掌控程度。这道题的适用人群很广正在备考大厂算法面试的求职者、计算机专业正在上数据结构课的学生、以及想夯实基础的初级工程师。它没有复杂的算法思想不涉及动态规划、回溯、贪心这些高级技巧纯粹考察基本功和代码细节。也正因为如此它在面试中出现的频率极高尤其是对于那些岗位要求扎实数据结构的团队面试官很喜欢拿这道题做代码质量的试金石。从面试角度来说这道题能考察的点非常丰富你能不能把边界条件想全你的代码风格是否清晰你对虚拟头节点dummy node这个常用技巧是否熟悉你会不会在索引越界时做出正确的防御性处理题目本身不限制你用单链表还是双链表实现也不限制用C、Java还是Python这种自由度本身就是一种考察——看你有没有足够的经验去选择适合当前场景的实现方案。2. 核心功能需求解析五个操作背后的隐藏规则2.1 get操作从0开始的索引陷阱get(index)要求获取链表中第index个节点的值如果索引无效则返回-1。这里最容易被忽视的是索引的起始位置。链表节点的索引从0开始计数也就是说get(0)获取的是头节点。这个看似简单的规则在实际编码中会带来很多细节问题。比如你在遍历时用for循环循环变量i从0开始还是从1开始用while循环时cur指针应该前进几步这些都是需要精确到每一行代码的问题。我见过很多人写get操作时循环条件写成i index或者i index结果要么漏掉最后一个节点要么多走了一步。这两种错误在实际运行中并不会立刻暴露因为只要测试用例中的索引值不触及边界结果可能是对的。但一旦遇到get(0)或者get(链表长度-1)这样的边界用例问题就立刻显现。另外get操作的另一个隐藏规则是链表为空时任何索引都无效直接返回-1。这个判断必须在遍历之前完成否则会出现空指针解引用的问题。很多人喜欢把索引有效性判断放在循环里做这虽然不影响结果但代码可读性会大打折扣。提示get操作的核心逻辑可以概括为“索引合法且有节点才取值否则返回-1”。先判断索引是否小于0或大于等于链表长度再启动遍历。2.2 addAtHead与addAtTail两个最容易被低估的操作addAtHead(val)看起来最简单创建一个新节点把它指向当前头节点然后更新头节点指针。但这里有一个非常经典的坑——当链表为空时你不仅要更新头节点指针还要更新尾节点指针。如果你实现的链表类内部同时维护了尾节点指针tail pointer那么addAtHead在链表为空时必须将尾节点也指向新节点。否则后续的addAtTail操作会找不到正确的尾节点位置导致链表结构错乱。这个细节在单链表中特别容易被忽略因为很多人在设计类时只知道维护size和head忘记了tail。addAtTail(val)的道理类似。如果链表为空尾节点即头节点所以你需要同时更新头尾指针。如果链表非空就让当前尾节点的next指向新节点然后更新尾节点。边界条件都在“空链表”这个特殊场景下集中出现所以写代码前先把空链表的情况单独想清楚能省掉后面一大堆debug时间。很多初学者会有疑问为什么addAtTail也要维护尾指针直接遍历到链表末尾再插入不就行了这么做的确可以但时间复杂度会从O(1)退化到O(n)。既然题目考察的是设计链表那么高效实现应该是一个基本要求。如果你使用单链表同时维护头指针和尾指针就可以让头部和尾部插入都达到O(1)的时间复杂度。2.3 addAtIndex最复杂、最考验细节的插入操作addAtIndex(index, val)是整个题目的重头戏因为它同时涉及索引有效性判断和插入位置的前驱节点定位两个难点。先看索引有效性。题目规定如果index等于链表的长度则说明新节点会被追加到链表末尾如果index大于链表长度则插入无效什么都不做如果index小于0则等同于在头部插入。这三条规则每一条都需要谨慎对待。其中最容易出错的是“index等于链表长度”这一条。因为这意味着你需要在当前尾节点之后插入新节点而当你使用虚拟头节点遍历时cur指针刚好会走到nullptr的位置。这时候如果你直接执行cur-next newNode就会访问空指针导致程序崩溃。再看插入位置的前驱节点定位。具体的定位方式取决于你有没有使用虚拟头节点如果使用虚拟头节点dummy head那么你只需要让cur从dummy开始向后移动index步cur就会恰好落在目标位置的前驱节点上。然后执行新节点的指针重连即可。如果没有使用虚拟头节点那么当index为0时你需要单独处理“在头部插入”的场景因为此时没有前驱节点。而index大于0时才可以使用常规的前驱节点遍历逻辑。这个操作之所以细节多是因为指针重连的顺序不容有失。正确顺序是先把新节点的next指向当前节点的next再把当前节点的next指向新节点。如果顺序颠倒你会先丢失后续链表的引用导致链表从中间断开。这个经典错误几乎每个学链表的人都会犯几次区别只是有没有在debug时意识到问题所在。2.4 deleteAtIndex删除操作里最容易“断链”的地方deleteAtIndex(index)删除指定索引的节点。与addAtIndex相似它同样需要定位到目标节点的前驱节点。删除操作有一个极其重要的细节你必须通过前驱节点来删除而不是直接定位到目标节点本身。为什么因为单链表是单向的如果cur已经指向了目标节点你无法知道它的前驱是谁。除非你维护了prev指针即双指针遍历否则删除操作无法完成。所以标准做法是让cur停留在目标节点的前一个位置然后执行cur-next cur-next-next同时释放被删除节点的内存在Java和Python中这一步由GC处理但在C中必须手动delete。释放内存这个细节在LeetCode的在线评测环境中往往不会暴露问题但在实际工程中却至关重要。C的内存泄漏是长期运行服务的大忌所以如果你用C实现这道题一定要记得delete被删除的节点。复杂度方面五个操作中get、addAtIndex、deleteAtIndex的平均时间复杂度都是O(n)——这里的n是链表长度。因为最坏情况下你都需要从头部遍历到目标位置附近。addAtHead和addAtTail如果维护了头尾指针则可以做到O(1)复杂度。空间复杂度方面不考虑用于存储数据的节点空间辅助空间为O(1)。3. 方案选型与代码结构设计单链表、双链表与虚拟头节点之争3.1 为什么推荐使用虚拟头节点在设计链表类的过程中第一个关键决策是用不用虚拟头节点。虚拟头节点dummy node是一个不存储有效数据的节点挂在链表最前面让真正的头节点变成dummy-next。我强烈推荐使用虚拟头节点原因只有一个它能消除头节点插入和删除的特殊情况。如果不使用dummy你需要在addAtHead、deleteAtIndex(0)等场景中单独处理头节点指针的更新这会增加大量if-else分支代码复杂度陡增。而使用dummy后头节点也变成了“有前驱”的普通节点所有插入和删除操作可以统一用同一套逻辑处理。用虚拟头节点还有一个额外的好处处理addAtIndex时cur的定位逻辑变得更统一。无论index是0、是链表长度还是中间的任意值你都可以让cur从dummy开始移动index步然后执行统一的重连逻辑。这种统一性让代码更容易写对也更容易Review。3.2 单链表与双链表的取舍题目没有规定必须使用单链表还是双链表这是一个需要你自己做权衡的设计决策。单链表的实现相对简单每个节点只需要存储val和next指针。但它的缺点是get和addAtIndex操作从头部遍历到目标位置平均需要O(n)时间。这也意味着虽然addAtHead和addAtTail可以做到O(1)但整体效率仍然受制于遍历。双链表实现中每个节点额外存储一个prev指针。这样当你定位到目标节点后可以向前回溯某些操作会变得更灵活。比如删除节点时双链表可以直接定位目标节点而不必费劲寻找它的前驱。更重要的是在LeetCode的后续题目中——比如LRU缓存机制、设计浏览器历史记录等——双链表往往是更合适的底层结构。但双链表的缺点是代码量更大指针维护更复杂。在LeetCode 707这道题里用单链表完全可以通过所有测试用例所以双链表属于“锦上添花”而非“必须”。我的建议是如果你是初学用单链表把逻辑理清楚最重要如果你已经有了一定基础可以尝试用双链表实现一遍加深对指针操作的理解。3.3 类成员变量的设计与选择设计链表类时需要类的内部维护哪些成员变量这个选择直接决定后续所有操作的实现方式。最核心的成员变量是三个size记录链表的当前长度。这个变量必可不少因为它让get和delete操作可以提前判断索引是否有效避免无意义的遍历。head头节点指针。如果使用虚拟头节点head通常指向dummy节点。tail尾节点指针。这个变量不强制要求但如果你希望addAtTail达到O(1)复杂度就必须维护它。这里有一个值得注意的权衡维护tail指针虽然让尾部操作变快但在addAtIndex和deleteAtIndex操作中你需要额外判断“操作位置是否影响尾节点”。比如在尾部插入时插入后需要更新tail删除尾节点时删除后也需要更新tail。这些额外的判断逻辑增加了代码的复杂度所以有些人会选择不维护tail让addAtTail遍历到链表末尾牺牲一点效率换取实现的简洁性。我个人在LeetCode 707这道题上选择了维护tail指针因为这样能把addAtTail的复杂度控制在O(1)而且在面试中展示出来的设计思路更完整。如果你不习惯维护tail写出来也完全没问题这道题并不会因为你的addAtTail是O(n)就判错。重要的是你想清楚每种选择的利弊并能在面试中讲清楚自己为什么这么做。4. 完整实现与代码剖析C版手写链表全流程4.1 C实现代码下面是我用C实现的完整代码版本采用单链表虚拟头节点尾指针的方案。为了照顾大多数面试场景我保留了手动内存管理。class MyLinkedList { private: struct ListNode { int val; ListNode* next; ListNode(int x) : val(x), next(nullptr) {} }; ListNode* dummyHead; ListNode* tail; int size; // 辅助函数返回第index个节点的前驱节点如果存在 ListNode* getPrevNode(int index) { ListNode* cur dummyHead; for (int i 0; i index; i) { cur cur-next; } return cur; } public: MyLinkedList() { dummyHead new ListNode(0); tail dummyHead; size 0; } int get(int index) { if (index 0 || index size) return -1; ListNode* cur getPrevNode(index 1); return cur-val; } void addAtHead(int val) { ListNode* newNode new ListNode(val); newNode-next dummyHead-next; dummyHead-next newNode; if (size 0) { tail newNode; } size; } void addAtTail(int val) { ListNode* newNode new ListNode(val); tail-next newNode; tail newNode; size; } void addAtIndex(int index, int val) { if (index size) return; if (index 0) index 0; ListNode* prev getPrevNode(index); ListNode* newNode new ListNode(val); newNode-next prev-next; prev-next newNode; // 如果插入位置是原链表末尾需要更新尾指针 if (index size) { tail newNode; } size; } void deleteAtIndex(int index) { if (index 0 || index size) return; ListNode* prev getPrevNode(index); ListNode* toDelete prev-next; prev-next toDelete-next; // 如果删除的是尾节点更新尾指针 if (index size - 1) { tail prev; } delete toDelete; size--; } };4.2 为什么get操作要传入index 1很多读者第一次看这段代码会对get函数里的getPrevNode(index 1)感到疑惑为什么获取第index个节点要找它的第index1个前驱原因在于我的getPrevNode(index)函数定义的是“返回第index个节点的前驱节点”。当cur从dummyHead开始移动index步后cur会停在原链表中第index-1个节点的位置如果index为0cur就停在dummyHead位置。所以如果我想获取第index个节点的值我需要先走到第index个节点本身也就是要从dummy移动index1步。如果我想在第index个位置插入新节点我需要的是第index个节点的前驱也就是要从dummy移动index步。两种场景的移动步数不一样这是单链表编码中最容易混淆的点。理解这个逻辑后你会发现getPrevNode这个辅助函数非常实用因为插入、删除、查询三种操作最终都归结为“找到某个前驱节点”。4.3 尾指针更新的时机判断在addAtIndex和deleteAtIndex中尾指针的更新是最容易遗漏的细节。在addAtIndex里如果插入位置index恰好等于插入前的链表长度size说明新节点会被追加到链表末尾。插入操作执行完毕后tail必须指向新节点。如果忘记更新后续的addAtTail会追加在旧的尾节点后面导致链表结构出错。在deleteAtIndex里如果删除的索引index恰好等于size - 1说明删除的是尾节点。删除后尾节点应该变为它的前驱节点也就是prev。这里的逻辑是prev-next toDelete-next因为toDelete是尾节点所以toDelete-next为nullptrprev成为新的尾节点。因此将tail更新为prev是正确的。这两个更新点你可以在写完代码后用几个边界用例自行验证比如在一个空链表中addAtHead、在尾部addAtIndex、删除最后一个节点。只要这几个用例跑通你的尾指针逻辑基本就没有问题了。4.4 其他语言的实现要点如果你用Java实现代码结构与C类似只是不需要手动管理内存而且要注意Java中的引用赋值。Java版本删除节点时只需要让前驱节点的next跳过目标节点即可目标节点会被GC自动回收。如果用Python实现可以利用Python类的简洁性把代码写得很短。Python中链表的节点定义与C不同不需要指针概念直接使用对象引用。这里给出一个Python的参考实现方便刷题时对照。class ListNode: def __init__(self, val0, nextNone): self.val val self.next next class MyLinkedList: def __init__(self): self.dummy ListNode(0) self.tail self.dummy self.size 0 def get(self, index: int) - int: if index 0 or index self.size: return -1 cur self.dummy.next for _ in range(index): cur cur.next return cur.val def addAtHead(self, val: int) - None: new_node ListNode(val) new_node.next self.dummy.next self.dummy.next new_node if self.size 0: self.tail new_node self.size 1 def addAtTail(self, val: int) - None: new_node ListNode(val) self.tail.next new_node self.tail new_node self.size 1 def addAtIndex(self, index: int, val: int) - None: if index self.size: return if index 0: index 0 prev self.dummy for _ in range(index): prev prev.next new_node ListNode(val) new_node.next prev.next prev.next new_node if index self.size: self.tail new_node self.size 1 def deleteAtIndex(self, index: int) - None: if index 0 or index self.size: return prev self.dummy for _ in range(index): prev prev.next to_delete prev.next prev.next to_delete.next if index self.size - 1: self.tail prev self.size - 1从这段代码可以看出Python与C在逻辑上完全一致只是语法更简洁。刷题时如果你用Python由于没有指针和内存管理的负担可以更专注于逻辑本身这也是很多人在面试中优先选择Python的原因之一。5. 常见错误与避坑经验我在调试中踩过的那些坑5.1 索引边界错误多走一步或少走一步链表操作中索引边界错误是最常见的错误类型没有之一。典型的错误有两种第一种是get操作中循环条件写错。正确写法是cur从dummyHead开始移动index1步到达目标节点但有人会写成移动index步导致返回的是目标节点的前驱的值。另一种是走过头循环条件写成i index导致访问到nullptr程序直接报错。我之前调试过一位学员的代码他在get操作中写了for (int i 0; i index; i)结果当index等于链表长度减1时刚好遍历到nullptr然后取val程序崩溃。这种错误非常隐蔽因为当index不是边界值时结果是正确的只有测试到最后一个节点才会暴露。建议的做法是在编写循环时明确标注循环不变式——cur到底停在哪个位置。你可以在代码旁边写注释// cur从dummy出发移动index步后指向索引为index-1的节点。有了这个注释循环条件就不容易写错。5.2 虚拟头节点方案中的size维护问题使用虚拟头节点后很多人会混淆size的语义。size应该记录真实节点的个数不包括dummy节点。但在addAtHead和deleteAtIndex操作中size的增减时机往往会出错。一个典型错误是在addAtHead中先对size进行自增再判断是否为空链表以更新尾指针。虽然在这道题中这种顺序错误不会直接影响结果但会让代码的逻辑混乱不利于后续维护。更严重的错误是在deleteAtIndex中忘记对size进行自减导致size始终大于实际链表长度。这样后续的get和addAtIndex都会因为索引判断失真而产生错误行为——明明链表已经为空了get(0)却不会返回-1而是访问到空指针。建议的做法是在每个操作的最后统一更新size并且在更新前确认操作已经成功执行。比如addAtIndex中先完成指针重连和尾指针更新最后才size。这样即使中途出错size也不会被错误修改。5.3 空链表场景下的特判遗漏空链表size为0是另一个高频出错场景。很多人在写addAtHead时只处理了链表的头部插入忘记了空链表时需要更新尾指针。同理在addAtTail时如果链表为空那么新节点既是头节点也是尾节点此时不能简单地把tail-next指向新节点因为tail可能为nullptr。解决思路是在编写每个操作之前先问自己一个问题——“这个操作在空链表的情况下会发生什么”如果空链表会导致空指针访问就需要提前特判如果空链表下操作仍然安全就可以跳过特判。在单链表虚拟头节点的实现中由于dummyHead始终存在addAtHead在空链表下是安全的但后续必须更新tail指针。5.4 内存管理问题C中的delete与Python/Java的GCC实现时删除节点后忘记delete会导致内存泄漏。这在LeetCode的评测中通常不会报错但在实际工程中是严重问题。如果你在面试中写C面试官很可能会追问你内存管理的问题这时候你如果能答出“删除节点后需要delete并且要防止double-free”会是一个很好的加分项。double-free是指对同一块内存释放两次。在deleteAtIndex中如果你先delete了toDelete然后函数结束后又通过其他路径再次delete它就会触发double-free错误。这种情况在链表操作中不常见但在复杂操作中比如同时修改多个指针时可能发生。为了避免这个问题删除节点后立即将指针置为nullptr是一个好习惯。Python和Java没有内存管理问题但这并不代表代码就绝对安全。GC机制虽然处理了节点的回收但如果你在代码中仍然持有对已删除节点的引用会导致该节点无法被GC回收形成隐性的“内存泄漏”——在长时间运行的程序中这种问题同样需要警惕。5.5 调试链表题的通用技巧链表题的调试在普通IDE里不太直观因为指针跳转后你很难回看“上一个状态”。我调试链表题时有一套独门方法第一在关键操作前后打印链表结构。写一个辅助函数printList()从头节点开始遍历并输出所有节点的值。在addAtIndex和deleteAtIndex前后分别调用对比两次输出的差异就能快速定位到指针操作出错的位置。第二用小规模用例手动模拟。不要一上来就跑大量随机测试而是手工构造几个边界用例空链表、只有一个节点、两个节点、删除头节点、删除尾节点、在末尾插入。每个用例都手动推演一遍指针变化然后在代码中打断点验证。第三善用断言标记状态。在关键位置加上assert语句比如assert(size 0)、assert(tail ! nullptr)等。这些断言在debug版本中能够快速捕捉到逻辑错误帮助你缩小问题范围。提示链表题调试的最大杀器是“小步验证”。每次修改一个操作就用最极端的用例去测不要等所有操作写完后一起调试。6. 从LeetCode 707到工程实践链表在真实项目中的延伸价值6.1 链表的典型应用场景很多人刷完这道题后会问链表在实际项目中到底有什么用这个问题值得认真回答因为只有理解了链表的实际价值你才能真正掌握它而不只是停留在刷题层面。链表的第一个典型应用是实现LRU缓存。LRULeast Recently Used最近最少使用缓存淘汰策略是操作系统、数据库、Redis等系统中常见的缓存算法。它的经典实现就是哈希表双向链表的组合哈希表负责O(1)地查找节点双向链表负责记录访问顺序。每次访问某个key时把它对应的节点移动到链表头部当缓存满了删除链表尾部的节点。LeetCode 146就是这道题的经典版本如果你能把707的双链表实现吃透再去看146会轻松很多。链表的第二个典型应用是实现文件系统的目录结构。很多嵌入式设备、操作系统的文件系统使用链表来组织目录项因为文件数量是动态变化的链表天然支持高效的插入和删除操作。链表的第三个典型应用是实现消息队列。在嵌入式开发、网络编程中消息队列常用链表实现。消息的到来是异步的队列长度是变化的链表可以灵活地分配和释放内存正好适合这种场景。热词中出现的“嵌入式链表代码示例”正是这类应用的体现。6.2 从单链表到循环链表的思维升级如果你觉得707已经掌握了我建议你继续练习一下循环链表Circular Linked List。热词中出现“循环单链表”和“单循环链表”说明这个变体也是面试中的常客。循环链表的尾节点指向头节点形成一个环。它的优势在于从任意节点出发都能走遍整个链表在某些需要循环访问的场景比如操作系统的进程调度轮询中特别方便。但循环链表也带来了新的挑战遍历时如何判断是否回到了起点在插入和删除操作中如何处理头尾相接的指针关系一种常见的面试题变体是“判断链表中是否有环”。这道题可以用快慢指针法解决快指针每次走两步慢指针每次走一步如果两者相遇说明存在环。如果你在707的基础上能快速理解并实现环形链表的操作你的链表基本功就已经很扎实了。6.3 链表与数组的取舍什么时候不该用链表链表虽然灵活但它并不是万能的。在实际工程中很多场景下数组或动态数组的表现优于链表。缓存局部性是链表最大的短板。数组在内存中是连续存储的CPU缓存可以一次性加载多个相邻元素而链表的节点在内存中是分散的每次访问next指针都可能触发一次缓存未命中。在数据量较大时链表的遍历速度通常明显慢于数组。占用空间是链表的另一个问题。由于每个节点需要额外的指针域链表的存储开销比数组高。如果是双向链表每个节点还要额外保存prev指针开销更大。在嵌入式系统中内存非常宝贵链表的这个特性需要认真权衡。那什么时候该用链表呢核心判断标准是是否需要频繁的中间插入和删除。如果操作主要集中在两端比如栈和队列动态数组其实也够用但如果需要在中间位置高频插入和删除节点比如实现文本编辑器的撤销列表、音乐播放器的播放列表链表就比数组有优势因为数组的中间插入需要移动大量元素时间复杂度是O(n)。6.4 刷完707后我建议你继续做这几道题如果你把707彻底弄懂了建议按顺序刷下面几道链表题它们能帮你把链表的知识体系串联起来LeetCode 206 反转链表这道题是链表操作中最经典的题目考察指针反转的顺序。迭代法和递归法都值得掌握面试中出现频率极高。LeetCode 876 链表的中间结点考查快慢指针技巧。快指针走两步慢指针走一步快指针到达末尾时慢指针恰好指向中间节点。LeetCode 19 删除链表的倒数第N个结点同样可以用快慢指针解决。快指针先走N步然后快慢指针一起走快指针到达末尾时慢指针就停在倒数第N个节点的前驱节点位置。LeetCode 146 LRU缓存正如前面提到的它是链表工程价值的最佳体现。如果你能独立完成这道题你的链表水平就已经达到中级面试的要求了。这几道题有一个共同点它们都不依赖复杂的算法思想而是考察你对指针操作的敏感度。如果你在707上能把边界条件处理得滴水不漏做这些题目时就会感觉特别顺畅。7. 个人心得链表题目在面试中的真实地位作为一个刷过大量LeetCode、也参与过技术面试的人我想最后聊点题外话。链表题在算法面试中的权重正在发生变化。一方面随着CS基础知识在面试中的回归链表题作为数据结构基本功的代表仍然频繁出现在第一轮的电话面试和在线笔试中。另一方面真正的高难度面试题很少单独考察链表链表更多是作为某个综合题的组成部分——比如结合哈希表、结合二叉树、结合并发编程来考察。这意味着什么意味着你不需要在链表题上追求“最优解中的最优解”但必须保证基础操作一次写对。面试官不会因为你把一个简单的链表题写出了O(1)级别的优化而惊叹但一定会因为你把边界条件写得乱七八糟而皱眉。707这道题正好能帮你在“一次写对”这个目标上做大量训练。我个人在实际刷题时的一个体会是链表题是少数“写代码之前先把图画清楚”收益极大的题型。每次动手实现前先在纸上画出链表结构标明头节点、尾节点、dummy节点然后用箭头标出指针变化的顺序。这个方法看起来笨但远比直接敲代码高效。我用这种方法练习707之后再做其他链表题准确率有了明显提升。最后再分享一个小技巧707这道题你至少应该写三遍。第一遍是理解题意第二遍是独立完成并确保通过所有测试用例第三遍是限时10分钟以内完成。第三遍的目标是训练肌肉记忆让你在面试中即使有点紧张也能流畅地写出正确代码。链表这种基础数据结构值得你在它身上花这些时间。