刷算法题这件事,链表是个很神奇的分水岭:数组、哈希表大家基本都能混过去,但到了链表这里,很多人的脑子就开始打架。链表算法本身并不难,它可能是所有基础数据结构里代码量最少的一类,但它也是唯一一门逼着你时刻去思考“当前指针指向哪里、下一步应该指向哪里”的课。很多人写链表题会陷入一种状态:代码写完一跑,空指针异常;加个特判又跑,结果整个链表的结构错乱;最后对着编辑器的报错信息发呆半小时。
我刚开始学链表时也有过同样的困惑,后来带过不少实习生、帮人复盘过好几场挂在链表题上的面试,才慢慢总结出来:大多数人卡住,不是语法不会写,而是脑海里缺少一张动态的“指针关系图”。这篇文章我会从链表和数组的底层差异讲起,接着把遍历、插入、删除、逆序这些基础操作逐个拆开,再延伸到栈、队列、环路检测、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 遍历链表的核心心法
遍历是链表一切操作的基础。你想找一个值、统计
