刷算法题这件事,链表是个很神奇的分水岭:数组、哈希表大家基本都能混过去,但到了链表这里,很多人的脑子就开始打架。链表算法本身并不难,它可能是所有基础数据结构里代码量最少的一类,但它也是唯一一门逼着你时刻去思考“当前指针指向哪里、下一步应该指向哪里”的课。很多人写链表题会陷入一种状态:代码写完一跑,空指针异常;加个特判又跑,结果整个链表的结构错乱;最后对着编辑器的报错信息发呆半小时。
我刚开始学链表时也有过同样的困惑,后来带过不少实习生、帮人复盘过好几场挂在链表题上的面试,才慢慢总结出来:大多数人卡住,不是语法不会写,而是脑海里缺少一张动态的“指针关系图”。这篇文章我会从链表和数组的底层差异讲起,接着把遍历、插入、删除、逆序这些基础操作逐个拆开,再延伸到栈、队列、环路检测、LRU这种偏高级的应用。内容偏底层、偏实操,会尽量把每一步为什么这么写解释清楚,适合刚开始学数据结构的新人,也适合刷题卡壳想回头补基础的朋友。
1. 为什么是链表:先搞清楚它和数组的真正差异
1.1 数组的硬边界在哪里
只要写过几年代码,你对数组肯定不陌生。数组在内存里是一段连续的空间,声明一个int arr[5],系统会直接分配一块能装下5个int的连续内存。所谓“下标访问”,本质就是基地址 + 下标 * 元素大小,所以数组随机访问的时间复杂度是O(1),这个特性在任何语言里都一样。
但连续内存也是一把双刃剑。数组一旦要在中间插入或删除元素,后面的所有元素都要一起搬动。举个最直观的例子:在数组头部插入一个新元素,原来的arr[0]要去arr[1],arr[1]要去arr[2],整条队伍都要往后挪一步。正常挪一次是O(n),如果数组容量不够,还要先申请一块更大的连续内存,再把全部数据复制过去。
你可以把数组想象成电影院里的座位,每个座位都是固定的、紧挨着的,观众编号就是下标。想在第3排和第4排之间塞进去一个人,对不起,第4排到最后所有人必须全体往右挪一个位置。如果整个影厅满了,还要换个更大的厅,让所有人重新落座。这就是连续内存最硬核的约束。
1.2 链表用“不连续”换来了什么
链表的结构则完全不一样。链表里的每个节点拥有自己的独立内存空间,节点之间不要求在物理上相邻,每个节点里除了保存数据,还保存了一个指向下一个节点的指针。整个链表就像一条用线索串起来的珠子,珠子散落在桌面各处,但这条线可以让它们从头到尾被依次访问。
访问链表中的某个节点时,只能从head开始,沿着next指针一个节点一个节点地走。想拿到第5个节点,前面4个都得先路过。所以链表随机访问的时间复杂度是O(n),这一点是天生劣势,任何优化都无法绕开。
但链表的优势也很明显:只要已经拿到了某个节点的位置,在它后面插入新节点,或者删除它的后继节点,理论上都只需要改几次指针,复杂度是O(1),完全不需要搬动其他元素。链表的元素个数也是动态的,想加一个就new一个节点,不用预先分配一大块连续内存,更不用担心扩容时的二次复制。
所以链表本质上是用“不能随机访问”的代价,换取了“灵活的增删”和“不需要连续内存”这两个能力。很多人学了链表之后觉得它比数组高级,其实不对。这两种结构没有绝对的优劣,只有适不适合当前场景。
1.3 什么时候该用链表,什么时候该用数组
我见过不少刚入行的朋友有一个误区:觉得数组操作需要搬移元素,那链表肯定全面碾压数组,于是所有需要频繁插入删除的场景都无脑上链表。但实际工程里,链表的真实表现未必比数组好。
原因在于CPU缓存。数组在内存里是连续的,遍历数组时,CPU把一段数据同时加载进高速缓存,后面的访问都能命中缓存;链表节点是分散的,每次跳转都可能导致缓存未命中,被迫从主内存重新读取数据。所以同样是遍历,在实际机器上链表往往比数组慢一个量级,尤其在节点数量非常大的时候。
如果用一句话总结选型逻辑:需要快速随机访问、数据量稳定、能用下标解决问题,优先数组;需要大量中间位置的插入删除、数据量动态变化、不依赖随机访问,才认真考虑链表。链表是“在某些约束下值得选用的结构”,不是“更好的数组”。带着这个认知去学链表,后面的很多设计选择才看得懂。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 链表基础操作动手拆解:节点定义、遍历、插入与删除
2.1 先搞定节点定义
链表的起点是节点,不管是单链表还是双向链表,节点里都至少包含数据和指针两部分。为了照顾不同语言习惯的读者,我把C++和Python两种定义都列出来。
C++最常见的写法是结构体:
cpp复制struct Node {
int val;
Node* next;
Node(int v) : val(v), next(nullptr) {}
};
创建节点时用new Node(x),用完需要手动delete释放,这是C++的基本约束。刷题场景里很多人会忽略回收,在竞赛或LeetCode这种工具环境没多大问题,但在工程代码里乱new不delete,迟早被内存泄漏教做人。
Python的写法更简单,写一个类就完事:
python复制class ListNode:
def __init__(self, val=0, next=None):
self.val = val
self.next = next
Python里所有对象本身就是引用,天然适合描述链表的指针关系。你不需要像C++那样手动管理内存,垃圾回收会帮你处理不再使用的节点。
面试时如果允许随便选语言,我建议选Python,代码量少,写起来思路清晰,可以把注意力完全放在算法本身上。如果是为了理解底层内存布局,C++是更好的学习工具,因为你对指针的每个改动都是显式的,这种“所有操作都摆在明面上”的感觉,对建立直觉特别有帮助。
2.2 遍历链表的核心心法
遍历是链表一切操作的基础。你想找一个值、统计节点个数、更新某个位置的元素、输出整个链表,底子都是遍历。单链表的遍历代码基本是固定模板:
python复制def traverse(head):
cur = head
while cur is not None:
# 在这里对当前节点做一些事,比如打印cur.val
print(cur.val)
cur = cur.next
这段代码的精髓在于理解cur = cur.next这一行。它读起来是“让cur等于cur的下一个节点”,本质上是在说“把当前关注点往右移一格”。很多第一次接触链表的人会写错成head = head.next,虽然也能跑,但当你后续需要继续使用原链表头时就傻眼了,所以遍历时最好单独用一个cur变量去“游走”,不要动头指针。
遍历的循环也大概率是初学者第一个遇到边界问题的地方。循环条件是cur is not None,意味着当cur走到最后一个节点的下一个位置时,循环自动结束。这时候如果再试图访问cur.val,就会触发空指针错误。记住一个原则:只要cur是None,不管你想拿它的val还是next,都是非法操作。
2.3 插入节点:改指针的顺序一定不能反
插入分好几种:头插、尾插、在指定节点后插入。头插和尾插本质上也是“在某个已知位置后面插入”,只要处理边界就行。先看最主要的场景——在某个节点p后面插入一个新节点new_node。
很多人第一次写,最常见的错误是先执行p.next = new_node,再执行new_node.next = p.next。这时候p.next已经被改成新节点了,原来的后继节点彻底丢失,链表从中间断掉。正确顺序必须是:
python复制def insert_after(p, new_node):
new_node.next = p.next
p.next = new_node
这个顺序用一句话记就是:先让新人拉住后面人的手,再让前面的人松手去拍新人的肩。如果顺序反了,后面的人就被弄丢了。这个“先接后断”的原则,不仅在单链表插入里适用,在很多涉及指针修改的算法里都反复出现。
在链表头部插入新节点,代码也可以优雅地处理:
python复制def insert_at_head(head, new_node):
new_node.next = head
return new_node
注意这里必须返回新节点,因为链表的头已经变了。调用方如果不接收返回值,整个链表就找不到了。C语言初学者最常见的问题之一,就是写了一个insert_at_head函数返回void,然后在外部丢了新头,整条链表等于白干。Python同样适用,无返回值的话外层没有任何办法获知新的头部。
2.4 删除节点:前驱往往比被删者更重要
单链表删除节点,一个最容易被忽略的事实是:如果只知道当前节点cur,你其实无法直接删除它。删除的本质是“让前一个节点的next指向当前节点的下一个节点”,但单链表里每个节点只保存了后继的地址,没有保存前驱。这意味着你必须从头遍历,找到cur的前一个节点prev,然后执行:
python复制prev.next = cur.next
所以单链表删除指定节点的常规时间复杂度是O(n),因为你要先找到前驱。很多面试新手会被问“已知节点p,如何在O(1)内删除它”这种题,其实有个小技巧:把p.next的值复制到p,然后让p.next指向p.next.next。但因为尾节点没有p.next,这招对尾节点无效,工程实用性有限,更多是作为一个思路拓展存在。
如果删除的场景集中在头部,那就简单了,直接让头指针指向第二个节点即可:
python复制def remove_head(head):
if head is None:
return None
return head.next
为了统一处理各种边界情况,很多标准实现会引入一个虚拟头节点(dummy node)。虚拟头节点本身不存储有效数据,它只是链表的哨兵,实际链表从dummy.next开始。这样当你要删除真正的头节点时,代码依然可以用“找前驱、改next”的统一模式,而不用为头节点单独写特判。这种方式在处理大量边界逻辑的链表算法题里尤其好用,后面讲逆序和反转的时候你会反复看到它。
3. 单链表逆序:一道题吃透递归与迭代的思维差异
3.1 先画出逆序的指针变化图
链表逆序应该算链表入门后第一道真正的分水岭题目,LeetCode第206题,面试里出现频率高得离谱。题目本身很简单:输入1->2->3->4->5->NULL,输出5->4->3->2->1->NULL。但就是这么一道题,能刷掉一大批背答案的人,因为只要题目稍加变化,比如“反转链表前N个节点”“反转区间[m,n]”,光靠背代码立刻原形毕露。
要真正搞懂逆序,建议先不要看代码,先在纸上画三个节点的链表,然后用箭头把每一步的改动画出来。拿1->2->3->NULL举例,逆序的结果是3->2->1->NULL。你会发现核心动作只有两个:把2->3改成2->1,把1->NULL保留成尾巴。逆序的本质,就是让每个节点的next指针掉头,指向它原来的前驱。
3.2 迭代法:三指针往前走
迭代解法的核心是三个指针:prev、cur、next。prev初始为None,表示逆序后链表的终点;cur初始为head,表示当前要处理翻转的节点;nxt用来提前保存原链表的后继。写成Python是下面这样:
python复制def reverse_list(head):
prev = None
cur = head
while cur is not None:
nxt = cur.next
cur.next = prev
prev = cur
cur = nxt
return prev
每一轮循环里做的事情非常机械化:先用nxt把cur的下一个节点存住,这一步是防止后面改指针时把链表弄丢;然后让cur.next指向prev,这就是让箭头掉头;接着把prev移到cur,cur移到nxt,相当于两个指针整体往右平移一格。循环结束后,cur变为None,此时prev恰好停在旧链表的尾节点,也就是新链表的头节点,直接返回prev。
这段代码之所以不容易出错,是因为“保存后继、改当前、双指针右移”三步循环足够稳定。我第一次写的时候老是忘记nxt = cur.next,结果一执行到cur.next = prev,后面的节点全部失联。后来把这句话当作铁律,每次改cur.next前先问自己一句:原来的next我保存了吗?
3.3 递归法:换个角度看问题
递归解法的代码更短,但对思维的要求更高:
python复制def reverse_list(head):
if head is None or head.next is None:
return head
new_head = reverse_list(head.next)
head.next.next = head
head.next = None
return new_head
理解递归解法要抓住一个关键假设:reverse_list(head.next)已经完成了把“以head.next为头的子链表”整个逆序的任务,并且返回了新子链表的头。这时候head还指着原来的head.next,而原head.next在被逆序后,它的next正好指向原先的尾部,也就是旧子链表的最后一个节点。我们要做的两件事很简单:让head.next.next指向head,把当前节点接到新链表的尾部;再把head.next置为None,防止旧链形成环。
很多人递归写到这里就结束了,没把head.next = None写进去,一旦链表长度大于1,就会产生环,最终导致遍历时死循环。递归的“信任下一层已经完成”是一种正向思考,但边界细节一个都不能少。
顺带说一句,迭代和递归并不只是同一道题的两种写法,它们体现了两种不同的重组思路:迭代关注“当前节点怎么翻转”,递归关注“子问题完成后怎么把当前节点拼上去”。面试时如果只背一种写法,遇到变形题很容易卡壳;把两种都吃透,很多反转类题目反而能触类旁通。递归在实际项目中还有栈溢出的风险,所以工程上我更推荐迭代方案,但理解递归对理清链表结构非常有用。
4. 从链表延伸开去:栈、队列的实现与场景
4.1 为什么栈和队列总跟链表绑在一起聊
热搜词里“队列、链表、栈等实际应用场景是什么”出现了好几次,说明很多人学到这三种结构时都处于“能看懂定义、不知道用来干嘛”的状态。栈、队列、链表这三个概念经常被放在一起讲,是因为栈和队列既可以用数组实现,也可以用链表实现;选择链表实现时,业务逻辑会变得非常自然。
先看栈。栈的核心操作是入栈、出栈、取栈顶。如果选用链表作为底层结构,你可以把链表的头部当作栈顶。入栈就是链表头插,出栈就是删除头节点,取栈顶就是访问头节点。这组操作全部是O(1),而且代码没有扩容烦恼,不用像数组那样在容量不足时把旧数据搬到新内存。
python复制class LinkedStack:
def __init__(self):
self.head = None
def push(self, val):
self.head = ListNode(val, self.head)
def pop(self):
if self.head is None:
return None
val = self.head.val
self.head = self.head.next
return val
def peek(self):
return None if self.head is None else self.head.val
再看队列。队列需要从一个方向入队、从另一个方向出队。用数组实现队列,如果空间满了要么套一个循环数组自己管理front/rear下标,要么忍受频繁的元素搬移;用链表则舒服很多:一个head负责出队,一个tail负责入队,入队就是尾插,出队就是头删,都是O(1)。
python复制class LinkedQueue:
def __init__(self):
self.head = None
self.tail = None
def enqueue(self, val):
node = ListNode(val)
if self.tail is None:
self.head = self.tail = node
return
self.tail.next = node
self.tail = node
def dequeue(self):
if self.head is None:
return None
val = self.head.val
self.head = self.head.next
if self.head is None:
self.tail = None
return val
这段队列代码里有个非常容易忽略的细节:如果队列从非空变成空,必须同时处理tail,否则下一次入队时tail还指着已经被删掉的节点,整个链表就乱套了。这种“边界引起的次生bug”,比直接写错算法更隐蔽,我自己在真实代码里被坑过不止一次。
4.2 场景不是抽象概念,而是看得见的系统机制
也许你会觉得上面这些实现看起来有点教学味,但只要往工程方向看一眼,会发现栈和队列几乎无处不在。
栈最常见的实例就是浏览器的后退功能。你在页面里不断点链接,浏览器会把每次访问记录压入一个历史栈;点击后退,就相当于执行一次出栈操作,回到上一个地址。编辑器的撤销操作同理,每一次修改压入操作历史栈,Ctrl+Z就是出栈。函数调用本身也是一套栈机制,每调用一层函数,系统就把当前的返回地址和局部变量压栈,函数返回时再弹栈,所以递归太深会导致系统栈溢出,而不是“函数太多没地方放”。
队列的实际案例就更多了。操作系统里的进程调度队列、打印任务队列、消息队列中间件,核心都是先进先出。生产者消费者模型就是最典型的队列应用:生产者把消息放进队列尾部,消费者从队列头部取消息,两者速度不一致时,队列天然起到缓冲作用。电商秒杀系统里接收的大量请求也会先放到队列中,再由后端服务慢慢消费,而不是直接把所有请求打到数据库上把系统压垮。
链式栈和链式队列在嵌入式或者不允许预分配大块内存的系统里也有特殊价值。因为链表节点是按需创建的,需要多少个就创建多少个,不会出现“为了存3个元素却分配了1024个空间”的浪费。虽然这种场景不常写业务代码的人遇到,但理解链式结构能给你多一种系统设计时的选择,至少不会在面试被问到底层实现时两眼一抹黑。
4.3 它们和“链表算法”的关系是什么
很多初学者以为栈、队列是独立于链表的新东西,其实它们是建立在底层存储结构之上的抽象逻辑。这套逻辑用一种叫“操作受限”的方式设计:栈只允许在栈顶操作,队列只允许一端入、另一端出。这样限制操作范围之后,很多算法的正确性更容易保证,因为你能确定数据的进出顺序。
链表在这里承担的角色是底层存储结构,就像地基;栈和队列是建立在地基上的业务规则。理解链表怎么实现头插、头删、尾插,就能自己徒手写一个链式栈或者链式队列。把这些基础连起来,学习树的层序遍历、图的广度优先搜索时,你会发现自己已经在不知不觉中使用队列了。
5. 链表算法的高阶操作与容易踩的坑
5.1 环形链表检测:快慢指针为什么成立
链表不一定是线性的,它可能自己形成一个环。环的存在会让很多常规操作变得危险,比如遍历一个带环链表永远不会结束。判断链表是否有环,经典解法是快慢指针。
python复制def has_cycle(head):
slow = fast = head
while fast is not None and fast.next is not None:
slow = slow.next
fast = fast.next.next
if slow is fast:
return True
return False
慢指针每次走一步,快指针每次走两步。如果链表没有环,快指针会先遇到None,函数正常结束;如果链表有环,快慢指针早晚会在环内相遇。直观理解方式有很多种,我比较常用的解释是:快指针相对于慢指针,每一轮都在接近一步。一旦两个指针都进入环,可以看作慢指针不动、快指针每轮靠近一步,这样距离一步一步缩到零,必然碰上。你不需要精确算出相遇点,只要抓住“快指针在环内相对慢指针是1步1步追赶”这个逻辑就够用了。
进阶一点的题是找出环的入口节点。结论是:当快慢指针第一次相遇后,把一个指针挪回head,另一个留在相遇点,两个指针同时一步一个节点走,再次相遇的位置就是入口。这个结论背后有数学推导,我不展开证明了。刷题时不妨记住并多做几遍,这类题一旦自己推导过一遍,同类型的变体基本不会慌。
5.2 两个链表相交问题的简易解法
另一道高频题是判断两个单链表是否相交,并找到第一个相交节点。最容易想到的方法是:把链表A的所有节点都放进哈希集合,然后遍历链表B,第一次在集合里命中的节点就是交点。这个解法的复杂度是O(m+n),空间复杂度也是O(m),面试时能通过,但不算最优。
更漂亮的解法是不用额外空间的小技巧:
python复制def get_intersection_node(head_a, head_b):
if head_a is None or head_b is None:
return None
p_a, p_b = head_a, head_b
while p_a is not p_b:
p_a = p_a.next if p_a else head_b
p_b = p_b.next if p_b else head_a
return p_a
这个代码的逻辑是:两个指针各自从头出发,走完自己链表后切到对方的链表继续走。如果两个链表有交点,那么两个指针会在第二次循环中同时走到交点;如果没交点,则两个指针会同时走到None,循环结束返回None。理解这段代码的关键在于“两个指针走过的路径总长度相同”,一个是A+B,一个是B+A。这个技巧非常值得背下来,因为代码极短,而且一旦理解之后基本不会忘。
5.3 链表题的边界检查清单
和数组题不同,链表题翻车往往不是因为核心算法想不到,而是边界条件没处理好。我总结了几个每次做链表题都必须检查的点:
- 链表为空,
head == None,代码是否还能正常走? - 链表只有一个节点,操作完会不会形成自环?
- 链表只有两个节点,反转、删除后头尾是否正常?
- 删除头节点时,头指针是否更新了?
- 遍历循环里的
cur = cur.next,是否有可能对None取next? - 反转或删除操作后,旧链表里的某些节点是否因为没被接上而导致环?
很多题目在LeetCode上是不会因为输出结果多出一个环而报错的,你用自己的本地环境调试时,遇到死循环往往要排查很久。建议每次在测试用例里主动覆盖空链表、单节点、两个节点、普通长度四类情况,能帮你省下大量排查时间。
5.4 一个真实踩坑记录
有一次我在某些在线评测系统里写缓存淘汰逻辑,需要频繁移动链表中某个节点到头部。最初我用的是单链表,每次移动之前都要从头遍历找到该节点的前驱,结果数据量一大,整个程序的性能立刻恶化。后来才意识到这种场景必须使用双向链表,因为双向链表的每个节点除了next还保留了prev指针,删除任意节点时不需要再从头遍历找前驱,可以直接通过prev拿到前一个节点。
这个经历给我一个很重要的教训:设计链表方案时,先想清楚自己要做什么操作。如果只是顺序遍历,单链表足够;如果涉及频繁的任意位置删除和移动,双向链表几乎是最优解。很多看起来“有点冗余”的结构设计,背后处理的其实都是实际的性能痛点和边界问题。
6. 面试与工程里的链表应用:LRU、有序合并和组合能力
6.1 LRU缓存淘汰:双向链表加哈希表的经典组合
实际工程里最经典的链表高级应用,应该是LRU(Least Recently Used)缓存淘汰策略。很多工具都用了类似思想:缓存空间有限,当缓存满了要淘汰最久没被使用的数据;每次访问某个数据,就把它的“新鲜度”提升到最高。
如果用双向链表加哈希表来实现,链表维护访问顺序,靠近头部的节点是最近刚被访问过的,靠近尾部的节点是最久没用的。哈希表负责快速定位某个key对应的节点,让查找时间从O(n)降到O(1)。查询时,如果key存在,先把节点从当前位置摘下来,再插入链表头部;插入新key时,先写哈希表、再在链表头部插入节点,如果缓存已满,就把链表尾部的节点删掉,同时从哈希表里移除对应key。
这套结构里双向链表的价值在于:删除中间任意节点时,可以通过prev找到前驱,不需要从头遍历。如果这里换成单链表,删除或移动一个节点就需要O(n)去找前驱,整个LRU操作就退化得毫无优势了。如果你在面试里碰到“手写LRU”这种题,建议先想清楚“选什么数据结构,为什么”,而不是一上来就闷头写代码。
6.2 合并有序链表:一个很多高级算法都依赖的基础能力
合并两个有序链表也是一道链表算法基础题,思路不难,用两个指针分别指向两条链表的头,谁小谁先接到结果链表上,然后指针后移。代码实现时,dummy节点能省去对结果头节点的各种特判。
python复制def merge_two_lists(l1, l2):
dummy = ListNode()
tail = dummy
while l1 is not None and l2 is not None:
if l1.val < l2.val:
tail.next = l1
l1 = l1.next
else:
tail.next = l2
l2 = l2.next
tail = tail.next
tail.next = l1 if l1 is not None else l2
return dummy.next
这道题本身不算很难,但它几乎是归并排序、合并多个有序链表、K路归并等一大类问题的地基。归并排序里的merge步骤、外部排序里的多路归并、数据库做有序结果集合并,背后的核心思想都和这里非常接近。所以把它练到能默写,收益远不止应付一道题。
6.3 链表在真实工程里的面孔
很多业务开发做久了,会觉得链表是纯教学概念,工作中根本不会直接写。这个观察有一定道理,因为高级语言的标准库里已经封装好了现成容器,你想用链表,直接拿语言内置的双向链表类、或者用切片模拟就够了。但这不代表链表没用,它经常以“包装后的形式”存在于系统底层。
数据库的缓冲池管理、操作系统的内存管理、浏览器历史记录、图片编辑器的撤销重做、音乐播放器的播放列表切换、网络数据包的收发缓冲,很多系统的底层都可能存在链表或者链表思想的变体。业务代码不直接写链表,是因为框架已经帮你封装好了;但在设计这些底层系统时,仍然需要有人懂链表原理并作出合适的选型。
哪怕在一张订单系统里,“删除与新增频繁”、或者“需要保持数据产生顺序”,也常会选择队列、双向链表这类结构方案来组织未提交的变更记录。和“购物车算法”这类热搜词所反映的直觉一致,很多用户交互场景中,“最近添加”“撤销最近修改”“按时间保持顺序”本身都具有栈或链表的结构特征,只是它们常常藏在集合类库里。
6.4 我的链表练习顺序建议
如果你现在刚开始刷链表题,别闷头一天做二十道。我建议按照“遍历—插入—删除—逆序—环—有序合并—LRU—综合应用”的顺序推进,每类题画图理解到能闭着眼写出思路为止。第一遍可以先模仿,第二遍合上书手写,第三遍试试看能不能给一个没学过的人把思路讲清楚。能讲清楚,才算真会。
我自己带人时发现,很多链表题画图前后是两种智商。不画图时,人的工作记忆要同时保存好几个指针的状态,非常容易超载;画完图之后,每一步指针怎么改清清楚楚,剩下的只是把图翻译成代码。所以无论你觉得自己有多熟,遇到没见过的链表题,第一反应永远是先拿笔在纸上画,而不是打开编辑器开始敲。这是链表算法学习性价比最高的习惯,没有之一。
