LeetCode 链表入门:从反转链表到快慢指针(C++面试刷题笔记)
最近重新准备 C/C++ 面试,发现自己虽然做了很多业务开发,但基础数据结构已经生疏了不少。因此决定从 LeetCode 最经典的链表题开始系统复习。
本文记录了我完成的四道高频链表题,以及过程中总结出的核心套路。
一、链表基础知识
链表节点定义
LeetCode 中最常见的定义:
struct ListNode
{
int val;
ListNode* next;
ListNode(int x)
{
val = x;
next = NULL;
}
};
每个节点包含:
val -> 当前节点数据
next -> 指向下一个节点
例如:
1 -> 2 -> 3 -> NULL
内存关系:
±----±----+ ±----±----+ ±----±-----+
| 1 | *-------> | 2 | *-------> | 3 | NULL |
±----±----+ ±----±----+ ±----±-----+
二、必须掌握的指针知识
对象访问成员
ListNode node(1);
node.val = 1;
node.next = NULL;
使用:
.
指针访问成员
ListNode* p = &node;
p->val = 1;
p->next = NULL;
使用:
->
两种写法区别
栈对象
ListNode dummy(0);
访问:
dummy.next
堆对象
ListNode* dummy = new ListNode(0);
访问:
dummy->next
三、LeetCode 206:反转链表
题目
1 -> 2 -> 3 -> 4 -> 5
变为
5 -> 4 -> 3 -> 2 -> 1
核心思想
反转过程实际上是在不断修改:
curr->next
原来:
1 -> 2
变成:
2 -> 1
三指针法
需要保存:
prev
curr
next
否则链表会断掉。
动态过程
初始:
prev = NULL
curr = 1
第一轮:
next = curr->next;
curr->next = prev;
结果:
1 -> NULL
curr = 2
prev = 1
第二轮:
2 -> 1 -> NULL
最终:
5 -> 4 -> 3 -> 2 -> 1 -> NULL
代码
ListNode* reverseList(ListNode* head)
{
ListNode* prev = NULL;
ListNode* curr = head;
while(curr)
{
ListNode* next = curr->next;
curr->next = prev;
prev = curr;
curr = next;
}
return prev;
}
复杂度
时间复杂度:
O(n)
每个节点访问一次。
空间复杂度:
O(1)
只使用了三个辅助指针。
四、LeetCode 21:合并两个有序链表
题目
1 -> 2 -> 4
1 -> 3 -> 4
合并后:
1 -> 1 -> 2 -> 3 -> 4 -> 4
核心思想
类似归并排序中的 merge。
每次比较:
list1->val
list2->val
谁小接谁。
Dummy Node(虚拟头节点)
经典写法:
ListNode dummy(0);
ListNode* curr = &dummy;
作用:
避免头节点特殊处理
统一插入逻辑
代码
ListNode* mergeTwoLists(ListNode* list1, ListNode* list2)
{
ListNode dummy(0);
ListNode* curr = &dummy;
while(list1 && list2)
{
if(list1->val < list2->val)
{
curr->next = list1;
list1 = list1->next;
}
else
{
curr->next = list2;
list2 = list2->next;
}
curr = curr->next;
}
curr->next = list1 ? list1 : list2;
return dummy.next;
}
为什么没有创建新节点?
因为直接复用了原链表节点:
curr->next = list1;
只是修改了:
next指针指向
而没有:
new ListNode(…)
复杂度
时间:
O(m+n)
空间:
O(1)
五、LeetCode 141:环形链表
题目
判断链表是否有环。
例如:
1 -> 2 -> 3
^ |
|____|
快慢指针
定义:
slow
fast
速度:
slow = slow->next;
fast = fast->next->next;
为什么能相遇?
假设链表有环:
1 -> 2 -> 3 -> 4
^ |
|_______|
快指针每次比慢指针多走一步。
进入环后:
距离不断缩小
最终必然相遇。
代码
bool hasCycle(ListNode* head)
{
ListNode* slow = head;
ListNode* fast = head;
while(fast && fast->next)
{
slow = slow->next;
fast = fast->next->next;
if(slow == fast)
{
return true;
}
}
return false;
}
复杂度
时间:
O(n)
空间:
O(1)
六、LeetCode 19:删除链表倒数第 N 个节点
题目
1 -> 2 -> 3 -> 4 -> 5
n = 2
删除后:
1 -> 2 -> 3 -> 5
核心难点
删除节点时需要找到:
目标节点前一个节点
因为链表删除本质是:
prev->next = prev->next->next;
Dummy + 快慢指针
构造:
ListNode dummy(0);
dummy.next = head;
定义:
slow = &dummy
fast = &dummy
先让 fast 走:
n + 1
步。
然后:
slow
fast
同步前进。
当:
fast == NULL
时:
slow
刚好停在待删除节点前一个位置。
代码
ListNode* removeNthFromEnd(ListNode* head, int n)
{
ListNode dummy(0);
dummy.next = head;
ListNode* slow = &dummy;
ListNode* fast = &dummy;
for(int i = 0; i < n + 1; i++)
{
fast = fast->next;
}
while(fast)
{
slow = slow->next;
fast = fast->next;
}
slow->next = slow->next->next;
return dummy.next;
}
复杂度
时间:
O(n)
空间:
O(1)
七、链表面试核心套路总结
经过这几题,可以总结出链表面试最重要的四个套路:
题目 核心套路
206 反转链表 prev + curr + next
21 合并有序链表 dummy + tail
141 环形链表 fast + slow
19 删除倒数第N个 dummy + fast/slow
八、复杂度分析经验
很多面试者容易混淆时间复杂度和空间复杂度。
时间复杂度
关注:
循环执行次数
例如:
while(curr)
遍历整个链表:
O(n)
空间复杂度
关注:
是否额外申请空间
例如:
ListNode* prev;
ListNode* curr;
ListNode* next;
只有固定数量变量:
O(1)
如果:
new ListNode(…)
随着 n 增长不断创建节点:
O(n)
结语
链表看起来简单,但几乎覆盖了 C/C++ 面试中的核心基础:
指针
内存模型
结构体与类
Dummy Node
快慢指针
时间复杂度分析
空间复杂度分析
如果能够熟练掌握 206、21、141、19 这四题,已经具备了应对大部分链表面试题的基础能力。
下一阶段我准备继续学习:
LeetCode 20 有效括号(栈)
LeetCode 232 用栈实现队列
LeetCode 104 二叉树最大深度
LeetCode 226 翻转二叉树
逐步补齐数据结构与算法基础。
更多推荐

所有评论(0)