算法编程题:链表(7 题)
025 原地反转单链表并返回新的头节点。
难度: 基础
方法签名: ListNode? head
输入约束: 链表无环
示例: 1→2→3 → 3→2→1
目标复杂度: 时间 O(n),额外空间 O(1)
查看参考答案
解题思路: 维护 previous、current 和 next,逐个改写 Next。
不变量: previous 指向已经反转的前缀,current 指向未处理后缀首节点。
public static class AlgCode025Solution
{
public sealed class ListNode
{
public int Value;
public ListNode? Next;
public ListNode(int value, ListNode? next = null)
=> (Value, Next) = (value, next);
}
public static ListNode? Solve(ListNode? head)
{
ListNode? previous = null;
ListNode? current = head;
while (current is not null)
{
ListNode? next = current.Next;
current.Next = previous;
previous = current;
current = next;
}
return previous;
}
}复杂度: 时间 O(n),额外空间 O(1)。
边界用例: 空链表;单节点;两个节点。
面试追问:
- 递归反转的调用栈空间是多少?
026 复用原节点合并两个升序单链表。
难度: 进阶
方法签名: ListNode? first, ListNode? second
输入约束: 两条链表无环且按 Value 非降序
示例: 1→3 与 2→4 → 1→2→3→4
目标复杂度: 时间 O(n+m),额外空间 O(1)
查看参考答案
解题思路: 使用虚拟头节点,每次连接较小的当前节点。
不变量: tail 之前是已经合并的有序前缀,两个输入指针指向未处理后缀。
public static class AlgCode026Solution
{
public sealed class ListNode
{
public int Value;
public ListNode? Next;
public ListNode(int value, ListNode? next = null)
=> (Value, Next) = (value, next);
}
public static ListNode? Solve(ListNode? first, ListNode? second)
{
var sentinel = new ListNode(0);
ListNode tail = sentinel;
while (first is not null && second is not null)
{
if (first.Value <= second.Value)
{
tail.Next = first;
first = first.Next;
}
else
{
tail.Next = second;
second = second.Next;
}
tail = tail.Next;
}
tail.Next = first ?? second;
return sentinel.Next;
}
}复杂度: 时间 O(n+m),额外空间 O(1)。
边界用例: 任一链表为空;重复值;长度差异很大。
面试追问:
- 合并 K 条链表时如何利用优先队列?
027 使用 O(1) 额外空间检测单链表是否存在环。
难度: 进阶
方法签名: ListNode? head
输入约束: 节点数有限,Next 可能形成环
示例: 3→2→0→-4,尾节点指向值为 2 的节点 → true
目标复杂度: 时间 O(n),额外空间 O(1)
查看参考答案
解题思路: 快指针每次两步,慢指针每次一步,有环时必在环内相遇。
不变量: 无环时 fast 最终到达 null;有环时二者距离按模环长变化。
public static class AlgCode027Solution
{
public sealed class ListNode
{
public int Value;
public ListNode? Next;
public ListNode(int value) => Value = value;
}
public static bool Solve(ListNode? head)
{
ListNode? slow = head;
ListNode? fast = head;
while (fast is not null && fast.Next is not null)
{
slow = slow!.Next;
fast = fast.Next.Next;
if (ReferenceEquals(slow, fast)) return true;
}
return false;
}
}复杂度: 时间 O(n),额外空间 O(1)。
边界用例: 空链表;单节点自环;无环长链。
面试追问:
- 如何在检测到环后找到入环节点?
028 返回单链表的中间节点,偶数长度时返回后一个中点。
难度: 基础
方法签名: ListNode? head
输入约束: 链表无环
示例: 1→2→3→4 → 节点 3
目标复杂度: 时间 O(n),额外空间 O(1)
查看参考答案
解题思路: 慢指针一步、快指针两步,快指针结束时慢指针位于中点。
不变量: slow 走过的节点数约为 fast 的一半。
public static class AlgCode028Solution
{
public sealed class ListNode
{
public int Value;
public ListNode? Next;
public ListNode(int value, ListNode? next = null)
=> (Value, Next) = (value, next);
}
public static ListNode? Solve(ListNode? head)
{
ListNode? slow = head;
ListNode? fast = head;
while (fast is not null && fast.Next is not null)
{
slow = slow!.Next;
fast = fast.Next.Next;
}
return slow;
}
}复杂度: 时间 O(n),额外空间 O(1)。
边界用例: 空链表;单节点;奇数与偶数长度。
面试追问:
- 如何调整初始化以返回偶数长度时的前一个中点?
029 一次扫描删除单链表倒数第 N 个节点。
难度: 进阶
方法签名: ListNode? head, int n
输入约束: 1 <= n <= 链表长度
示例: 1→2→3→4→5, n=2 → 1→2→3→5
目标复杂度: 时间 O(n),额外空间 O(1)
查看参考答案
解题思路: 让 fast 先走 n 步,再同步移动 fast 与 slow 到目标前驱。
不变量: fast 与 slow 之间始终保持 n 个节点的距离。
public static class AlgCode029Solution
{
public sealed class ListNode
{
public int Value;
public ListNode? Next;
public ListNode(int value, ListNode? next = null)
=> (Value, Next) = (value, next);
}
public static ListNode? Solve(ListNode? head, int n)
{
var sentinel = new ListNode(0, head);
ListNode? fast = sentinel;
for (int step = 0; step < n; step++) fast = fast!.Next;
ListNode slow = sentinel;
while (fast!.Next is not null)
{
fast = fast.Next;
slow = slow.Next!;
}
slow.Next = slow.Next!.Next;
return sentinel.Next;
}
}复杂度: 时间 O(n),额外空间 O(1)。
边界用例: 删除头节点;删除尾节点;单节点链表。
面试追问:
- 如果 n 可能非法,如何在不额外扫描的前提下返回失败?
030 返回两条可能相交的无环单链表的第一个公共节点。
难度: 进阶
方法签名: ListNode? first, ListNode? second
输入约束: 两条链表无环,相交后共享同一节点后缀
示例: A:1→8→9,B:2→3→8→9 → 共享节点 8
目标复杂度: 时间 O(n+m),额外空间 O(1)
查看参考答案
解题思路: 两个指针到达末尾后切换到另一条链表头,抵消长度差。
不变量: 两个指针走过相同总距离后会同时到达交点或 null。
public static class AlgCode030Solution
{
public sealed class ListNode
{
public int Value;
public ListNode? Next;
public ListNode(int value) => Value = value;
}
public static ListNode? Solve(ListNode? first, ListNode? second)
{
ListNode? left = first;
ListNode? right = second;
while (!ReferenceEquals(left, right))
{
left = left is null ? second : left.Next;
right = right is null ? first : right.Next;
}
return left;
}
}复杂度: 时间 O(n+m),额外空间 O(1)。
边界用例: 任一为空;头节点即相交;值相同但节点不同;不相交。
面试追问:
- 如果链表可能有环,问题需要先区分哪些情况?
031 深拷贝带随机指针的单链表。
难度: 综合
方法签名: RandomNode? head
输入约束: Next 无环,Random 可指向任意节点或 null
示例: 复制后结构和值相同,但所有节点引用独立
目标复杂度: 平均时间 O(n),额外空间 O(n)
查看参考答案
解题思路: 第一遍创建原节点到副本的映射,第二遍连接 Next 与 Random。
不变量: 映射中的每个原节点都对应唯一的新节点。
public static class AlgCode031Solution
{
public sealed class RandomNode
{
public int Value;
public RandomNode? Next;
public RandomNode? Random;
public RandomNode(int value) => Value = value;
}
public static RandomNode? Solve(RandomNode? head)
{
if (head is null) return null;
var copies = new Dictionary<RandomNode, RandomNode>();
for (RandomNode? node = head; node is not null; node = node.Next)
copies[node] = new RandomNode(node.Value);
for (RandomNode? node = head; node is not null; node = node.Next)
{
RandomNode copy = copies[node];
copy.Next = node.Next is null ? null : copies[node.Next];
copy.Random = node.Random is null ? null : copies[node.Random];
}
return copies[head];
}
}复杂度: 平均时间 O(n),额外空间 O(n)。
边界用例: 空链表;Random 指向自身;多个节点指向同一节点。
面试追问:
- 如何通过把副本节点穿插到原链表中把额外空间降为 O(1)?