引言与链表基础
链表(Linked List)是一种基础且重要的线性数据结构,与数组不同,链表中的元素在内存中并非连续存储,而是通过指针(或引用)将零散的内存块串联起来。每个链表节点(Node)包含两部分:数据域(存储数据)和指针域(存储下一个节点的地址)。这种存储方式赋予了链表动态插入/删除的高效性,但牺牲了随机访问的能力。
链表的核心特点:
- 内存非连续:节点分散在内存各处,通过指针连接。
- 动态大小:无需预先指定容量,可随时申请新节点。
- 操作效率 :
- 插入/删除(已知节点位置): O ( 1 ) O(1) O(1)
- 随机访问: O ( n ) O(n) O(n)(需从头遍历)
- 空间开销:每个节点需额外存储指针。
常见链表类型:
- 单链表(Singly Linked List):每个节点只有一个指针指向后继。
- 双链表(Doubly Linked List):节点含前后两个指针,可向前/向后遍历。
- 循环链表(Circular Linked List):尾节点指向头节点,形成环。
基础操作:
- 遍历:从头节点出发,依次访问每个节点。
- 插入:在指定节点前/后插入新节点(需调整指针)。
- 删除:移除指定节点,并保证链表不断开。
- 查找:按值或位置查找节点。
本文选取力扣hot100中三道经典链表题目------206.反转链表 、141.环形链表 、21.合并两个有序链表,深入剖析其解题思路、代码实现与面试考点,帮助读者建立链表操作的思维框架,从容应对面试挑战。
206.反转链表
题目概述与链接
题目链接 :力扣206.反转链表
题意 :给定单链表的头节点 head,反转链表,并返回反转后的头节点。
示例:
- 输入:
head = [1,2,3,4,5] - 输出:
[5,4,3,2,1]
要求 :实现 O ( n ) O(n) O(n) 时间复杂度和 O ( 1 ) O(1) O(1) 空间复杂度(递归解法除外)。
核心解题思路
反转链表的本质是调整每个节点的指针方向 ,将"当前节点指向后继"改为"当前节点指向前驱"。有两种经典实现方式:迭代法 与递归法。
迭代法(三指针法)
维护三个指针:
prev:指向已反转部分的新头节点。curr:当前待反转节点。next:保存curr的后继,防止断链。
步骤:
- 初始化
prev = None,curr = head。 - 遍历链表,在每一轮中:
- 保存
next = curr.next。 - 将
curr.next指向prev(反转指针)。 prev和curr分别向后移动一位。
- 保存
- 当
curr为None时,prev即为新头节点。
时间复杂度 : O ( n ) O(n) O(n),遍历一次链表。
空间复杂度 : O ( 1 ) O(1) O(1),仅使用常数个指针。
递归法
递归的思想是:假设后续链表已经反转,当前节点只需处理与后续部分的关系。
步骤:
- 递归基:若链表为空或只有一个节点,直接返回
head。 - 递归反转
head.next之后的链表,得到新头节点new_head。 - 将
head.next.next指向head(反转指针),并将head.next置为None(断开原连接)。 - 返回
new_head。
时间复杂度 : O ( n ) O(n) O(n),递归深度为链表长度。
空间复杂度 : O ( n ) O(n) O(n),递归栈空间。
Python代码实现
迭代法
python
# Definition for singly-linked list.
# class ListNode:
# def __init__(self, val=0, next=None):
# self.val = val
# self.next = next
class Solution:
def reverseList(self, head: Optional[ListNode]) -> Optional[ListNode]:
prev, curr = None, head
while curr:
# 保存后继节点
next_node = curr.next
# 反转指针
curr.next = prev
# 移动指针
prev, curr = curr, next_node
return prev
代码说明:
prev始终指向已反转部分的头节点,curr为当前待处理节点。- 每次循环将
curr.next指向prev,然后更新prev和curr。 - 循环结束时
curr为None,prev指向原链表的尾节点(即新头节点)。
递归法
python
class Solution:
def reverseList(self, head: Optional[ListNode]) -> Optional[ListNode]:
# 递归基
if not head or not head.next:
return head
# 递归反转后续链表
new_head = self.reverseList(head.next)
# 反转当前节点与后续部分的关系
head.next.next = head
head.next = None
return new_head
代码说明:
- 递归到链表末尾,返回尾节点作为新头节点
new_head。 - 回溯过程中,将当前节点的后继节点的
next指向当前节点,实现局部反转。 - 最后将原头节点的
next置为None,避免成环。
优化思路
- 边界条件处理 :
- 空链表:直接返回
None。 - 单节点链表:直接返回
head。
- 空链表:直接返回
- 迭代法的哨兵节点:可使用哑节点简化操作,但本题直接操作指针更清晰。
- 递归的栈溢出风险:链表过长时递归深度可能超过系统栈限制,优先使用迭代法。
面试考点
- 指针操作陷阱:反转指针时若未保存后继节点,会导致断链。
- 递归与迭代的选择:面试官可能要求同时给出两种解法,并分析优劣。
- 空间复杂度 :递归法的 O ( n ) O(n) O(n) 栈空间是否可接受?
- 扩展问题 :如何反转链表的前 N N N 个节点?如何每 k k k 个节点一组反转?
141.环形链表
题目概述与链接
题目链接 :力扣141.环形链表
题意 :给定链表的头节点 head,判断链表中是否存在环。如果链表中存在环,则返回 true;否则返回 false。
示例:
- 输入:
head = [3,2,0,-4](尾节点指向索引1的节点) - 输出:
true
要求 :实现 O ( n ) O(n) O(n) 时间复杂度和 O ( 1 ) O(1) O(1) 空间复杂度。
核心解题思路
判断链表是否有环的经典方法是快慢指针(Floyd判圈算法)。其核心思想是:让两个指针以不同速度遍历链表,若链表有环,则快指针最终会追上慢指针(相遇);若无环,快指针会先到达链表尾部。
快慢指针算法
- 慢指针
slow:每次移动一步。 - 快指针
fast:每次移动两步。
步骤:
- 初始化
slow = fast = head。 - 循环条件:
fast和fast.next均不为None(保证可移动两步)。 - 每轮循环:
slow = slow.nextfast = fast.next.next- 若
slow == fast,说明相遇,有环,返回true。
- 循环结束(
fast或fast.next为None)说明无环,返回false。
数学证明 :
设链表头到环入口距离为 a a a,环入口到相遇点距离为 b b b,环长度为 c c c。
当 slow 进入环时,fast 已在环中。设此时 fast 领先 slow 距离 d d d( 0 ≤ d < c 0 \le d < c 0≤d<c)。
由于 fast 每步比 slow 多走1,经过 k k k 步后,fast 追上 slow,满足 k ≡ d ( m o d c ) k \equiv d \pmod{c} k≡d(modc)。
因此两者必在有限步内相遇。
时间复杂度 : O ( n ) O(n) O(n),最坏情况下遍历整个链表。
空间复杂度 : O ( 1 ) O(1) O(1),仅使用两个指针。
Python代码实现
python
# Definition for singly-linked list.
# class ListNode:
# def __init__(self, x):
# self.val = x
# self.next = None
class Solution:
def hasCycle(self, head: Optional[ListNode]) -> bool:
slow = fast = head
while fast and fast.next:
slow = slow.next
fast = fast.next.next
if slow == fast:
return True
return False
代码说明:
- 使用
while fast and fast.next保证快指针可安全移动两步。 - 每次移动后检查
slow == fast,若相等则存在环。 - 循环正常结束说明快指针已抵达链表末尾,无环。
优化思路
- 不同环位置的检测效率 :若环靠近头部,快指针很快追上慢指针;若环在尾部,需要遍历较长时间。但最坏情况均为 O ( n ) O(n) O(n)。
- 边界条件 :
- 空链表或单节点无环:直接返回
false。 - 单节点自成环:
head.next == head,快慢指针首次移动后即相遇。
- 空链表或单节点无环:直接返回
- 避免无限循环 :确保快指针每次移动两步,且检查
fast.next不为空。
面试考点
- 数学证明:面试官可能要求证明快慢指针必然相遇。
- 环入口定位:扩展问题------如何找到环的入口节点(力扣142题)。
- 空间复杂度要求 :若使用哈希表存储已访问节点,空间为 O ( n ) O(n) O(n),不符合 O ( 1 ) O(1) O(1) 要求。
- 变种问题:如何判断两个链表是否相交?如何找到相交节点?
21.合并两个有序链表
题目概述与链接
题目链接 :力扣21.合并两个有序链表
题意 :将两个升序链表合并为一个新的升序链表并返回。新链表应通过拼接给定的两个链表节点组成。
示例:
- 输入:
l1 = [1,2,4],l2 = [1,3,4] - 输出:
[1,1,2,3,4,4]
要求 :时间复杂度 O ( n + m ) O(n+m) O(n+m),空间复杂度 O ( 1 ) O(1) O(1)(不计递归栈)。
核心解题思路
合并有序链表的本质是比较两个链表的当前节点,将较小者接入新链表 。有两种实现方式:迭代法 与递归法。
迭代法(哨兵节点)
使用一个哑节点(dummy) 作为新链表的起始点,维护一个 tail 指针指向当前新链表的尾部。然后比较 l1 和 l2 的当前节点:
- 若
l1.val <= l2.val,将l1节点接在tail后,l1后移。 - 否则,将
l2节点接在tail后,l2后移。 - 每次操作后更新
tail指针。 - 当某一链表遍历完后,将
tail.next指向另一链表的剩余部分。
时间复杂度 : O ( n + m ) O(n+m) O(n+m),遍历两个链表各一次。
空间复杂度 : O ( 1 ) O(1) O(1),仅使用常数个指针。
递归法
递归的思想是:每次比较两个链表的头节点,将较小者作为当前节点,然后递归合并剩余部分。
步骤:
- 递归基:若
l1为空返回l2,若l2为空返回l1。 - 比较
l1.val与l2.val:- 若
l1.val <= l2.val,则l1.next = merge(l1.next, l2),返回l1。 - 否则
l2.next = merge(l1, l2.next),返回l2。
- 若
时间复杂度 : O ( n + m ) O(n+m) O(n+m),递归深度为两链表长度之和。
空间复杂度 : O ( n + m ) O(n+m) O(n+m),递归栈空间。
Python代码实现
迭代法
python
# Definition for singly-linked list.
# class ListNode:
# def __init__(self, val=0, next=None):
# self.val = val
# self.next = next
class Solution:
def mergeTwoLists(self, l1: Optional[ListNode], l2: Optional[ListNode]) -> Optional[ListNode]:
dummy = ListNode(-1) # 哑节点
tail = dummy
while l1 and l2:
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 else l2
return dummy.next
代码说明:
- 哑节点
dummy简化了头节点的处理,最后返回dummy.next即为新链表头。 tail指针始终指向新链表的尾部,便于追加节点。- 循环结束后,将非空链表的剩余部分直接链接到
tail.next,无需逐个节点追加。
递归法
python
class Solution:
def mergeTwoLists(self, l1: Optional[ListNode], l2: Optional[ListNode]) -> Optional[ListNode]:
if not l1:
return l2
if not l2:
return l1
if l1.val <= l2.val:
l1.next = self.mergeTwoLists(l1.next, l2)
return l1
else:
l2.next = self.mergeTwoLists(l1, l2.next)
return l2
代码说明:
- 递归基直接返回非空链表,简洁明了。
- 每次递归调用都将较小节点的
next指向后续合并的结果,形成递归链。 - 递归深度等于两链表节点总数,需注意栈溢出风险。
优化思路
- 不同链表长度下的性能:迭代法在任何情况下都是最优选择,递归法在链表极长时可能栈溢出。
- 空间优化 :迭代法使用 O ( 1 ) O(1) O(1) 额外空间,递归法为 O ( n + m ) O(n+m) O(n+m)。
- 边界条件 :
- 其中一个链表为空:直接返回另一个链表。
- 两个链表均为空:返回
None。
- 稳定性 :若两节点值相等,优先选择
l1节点可保持稳定性(原顺序)。
面试考点
- 链表操作熟练度:能否熟练使用哨兵节点简化代码?
- 递归思维:能否清晰描述递归合并的过程?
- 时空复杂度分析:能否准确分析迭代与递归的复杂度差异?
- 扩展问题 :如何合并 k k k 个有序链表?如何合并两个有序链表并去重?
总结与扩展
通过以上三道题目的深度解析,我们可归纳出链表问题的核心解题模式:
- 指针操作:链表问题本质是指针的移动与连接。熟练掌握多指针(快慢指针、前后指针)技巧是解题关键。
- 递归思维:许多链表问题(如反转、合并)具有递归结构,利用递归可简化代码,但需注意栈空间开销。
- 边界处理:空链表、单节点链表、环的入口等边界情况需仔细考虑。
力扣链表经典题目延伸练习:
- 19.删除链表的倒数第N个节点:快慢指针定位倒数第N个节点。
- 142.环形链表II:在判断有环的基础上,找到环的入口节点。
- 24.两两交换链表中的节点:递归或迭代实现相邻节点交换。
- 148.排序链表:归并排序在链表上的应用(快慢指针找中点)。
- 160.相交链表:双指针技巧,找到两个链表的相交节点。