前言
前言:刷链表面试高频题。
文章目录
- 前言
- 一. 基础回顾
- 二. 高频考题
- 1、例题
- 例题1:LeetCode 206 反转链表
- 1)题目链接
- 2) 算法思路
- 3)源码剖析
- 4)时间复杂度
- 例题2:LeetCode 92 反转链表II
- 1)题目链接
- 2) 算法思路
- 3)源码剖析
- 4)时间复杂度
- 例题3:LeetCode 203 移除链表元素
- 1)题目链接
- 2)遍历做法
- 3)递归做法
- 3)时间复杂度
- 2、习题
- 习题1:LeetCode 19 删除链表的第N个节点
- 1)题目链接
- 2) 算法思路
- 3)源码剖析
- 3)时间复杂度
- 习题2:LeetCode 876 链表的中间节点
- 1)题目链接
- 2) 算法思路
- 3)源码剖析
- 3)时间复杂度
- 习题3:LeetCode 160 相交链表
- 1)题目链接
- 2) 算法思路
- 3)源码剖析
- 3)时间复杂度
- 习题4:LeetCode 141 环型链表
- 1)题目链接
- 2) 算法思路
- 3)源码剖析
- 4)时间复杂度
- 习题5:LeetCode 142. 环形链表 II
- 1)题目链接
一. 基础回顾
参考上一讲: 04 |「链表」简析
结构:
1
1
1->
2
2
2->
3
3
3->NULL,链表是 指向型
结构 。
查找:随机访问的时间复杂度是
O
(
n
)
O(n)
O(n)。
增删:删除和插入元素的时间复杂度都是
O
(
1
)
O(1)
O(1) 。
头结点(head
):对于链表,给你一个链表时,我们拿到的是头节点(head
) 。如果没有头结点证明整个链表为空 NULL
,如果已经有头结点证明链表不为空。
虚拟头结点(dummy
) :针对链表,需要经常判断头结点 (head
)头结点为空和不为空对应不同的操作,增加哨兵节点统一操作。如果链表为空(head = null
),那么 访问 null.val 与 null.next 出错。为了避免这种情况,增加一个虚拟头结点(dummy
),其中 dummy
的值 (val
)常用 -1
表示,这样 dummy.next = null,避免直接访问空指针。
// 增加虚拟头节点的链表遍历
dummy; dumm->next = head; p = dummy;
while (p)
{
}
// 没有虚拟头结点的链表遍历
head;
while (head)
{
head = head->next;
}
二. 高频考题
1、例题
例题1:LeetCode 206 反转链表
1)题目链接
原题链接:反转链表(点击链接直达)
2) 算法思路
- 明确:修改几条边,修改哪几条边,注意是修改
n
条边; - 操作:将当前节点的
next
指针改为指向前一个节点(last
); - 维护:双链表可以通过
pre
指针访问前一个节点。针对单链表,没有pre
指针无法访问前一个节点(last
),需要新开一个变量维护前一个节点(last
); - 边界:针对头结点(
head
)没有前一个节点,创建last
并赋为NULL
;
3)源码剖析
class Solution {
public:
ListNode* reverseList(ListNode* head) {
if (!head || !head->next) return head;
ListNode* last = nullptr; //(1)
ListNode* cur = head; //(2)
while (cur) //(3)
{
ListNode* next = cur->next; //(4)
cur->next = last; //(5)
last = cur; //(6)
cur = next; //(7)
}
return last; //(8)
}
};
- (1)/(2):初始化变量
last
和cur
,last
指向上一个节点,cur
指向当前节点; - (3):修改每条边,需要循环遍历访问每个节点;
- (4):修改一条边时,先保存当前节点(
cur
)的下一个节点(next
),防止丢失; - (5):修改一条边;
- (6)/(7):
last
和cur
分别向后移动一位; - (8):返回反转后链表的头结点。当
cur
停下时指向原链表的NULL
,此时last
指向反转后链表的头结点;
4)时间复杂度
O ( n ) O(n) O(n)
例题2:LeetCode 92 反转链表II
1)题目链接
原题链接:反转链表(点击链接直达)
2) 算法思路
- 将
tmp
节点移动到left-1
的位置处; - 反转
[left, right]
部分的节点。从left
位置开始反转,反转right-left
次; - 调整剩余部分节点的指向;
- 返回头结点;
3)源码剖析
class Solution {
public:
ListNode* reverseBetween(ListNode* head, int left, int right) {
if (left == right) return head; //(1)
ListNode* dummy = new ListNode(-1); //(2)
dummy->next = head;
ListNode* tmp = dummy;
for (int i = 0; i < left - 1; i ++) tmp = tmp->next; //(3)
//(4)
ListNode* pre = tmp->next;
ListNode* cur = pre->next;
for (int i = 0; i < right - left; i ++)
{
ListNode* next = cur->next;
cur->next = pre;
pre = cur;
cur = next;
}
//(5)
tmp->next->next = cur;
tmp->next = pre;
return dummy->next; //(6)
}
};
- (1):
left=right
证明只有一个头结点; - (2):
dummy
为哨兵节点。因为left
可能在head
位置,故添加哨兵节点; - (3):将
tmp
节点移动到left-1
的位置; - (4):
(4)- (5)
之间的代码为反转[left, right]
部分的节点,逻辑同上题; - (5):
(5)-(6)
之间的代码为调整其它节点的指向。如示例1,2
的next
指向5
,1
的next
指向4
; - (6):返回链表头节点;
4)时间复杂度
O ( n ) O(n) O(n)
例题3:LeetCode 203 移除链表元素
1)题目链接
原题链接:移除链表元素(点击链接直达)
2)遍历做法
- 增加
dummy
哨兵节点的目的是统一操作,少写特判断头结点(head
)是否为空。// 不增加哨兵节点dummy if (!head) { return head; } else { }
// 增加哨兵节点dummy class Solution { public: ListNode* removeElements(ListNode* head, int val) { ListNode* dummy = new ListNode(-1); dummy->next = head; ListNode* p = dummy; while (p->next) { if (p->next->val == val) p->next = p->next->next; else p = p->next; } return dummy->next; } };
3)递归做法
if (!head) return head;
head->next = removeElements(head->next, val);
return head->val == val? head->next : head;
3)时间复杂度
O ( n ) O(n) O(n)
2、习题
习题1:LeetCode 19 删除链表的第N个节点
1)题目链接
原题链接: 删除链表的第N个节点(点击链接直达)
2) 算法思路
纸上画图实际模拟一遍即可。
3)源码剖析
class Solution {
public:
ListNode* removeNthFromEnd(ListNode* head, int n) {
ListNode* dummy = new ListNode(-1); //(1)
dummy->next = head;
ListNode* p = dummy, *q = dummy;
for (int i = 0; i < n; i ++) p = p->next; //(2)
while (p->next != nullptr) //(3)
{
p = p->next;
q = q->next;
}
q->next = q->next->next; //(4)
return dummy->next;
}
};
- (1):定义虚拟头结点
dummy
,不用考虑头结点的特殊情况; - (2):
p
指针先走 n 步; - (3):
p
指针和q
指针同时走,直到p
指针走到最后一个节点,两指针都停下; - (4):此时
q
指向的就是要删除节点的前一个节点(n-1
处),删除第n
个节点;
3)时间复杂度
双指针遍历时间复杂度为 O ( n ) O(n) O(n)
习题2:LeetCode 876 链表的中间节点
1)题目链接
原题链接: 链表的中间节点(点击链接直达)
2) 算法思路
- 模拟枚举。奇数个节点,
q
走到中点时,p->next
为NULL
。偶数个节点,q
走到中点时,fast
为空NULL
。
3)源码剖析
class Solution {
public:
ListNode* middleNode(ListNode* head) {
auto p = head, q = head;
while (p && p->next) { // 只要p和p->next都不为空时,两指针就一种往后走
p = p->next->next;
q = q->next;
}
return q;
}
};
3)时间复杂度
双指针遍历时间复杂度为 O ( n ) O(n) O(n)
习题3:LeetCode 160 相交链表
1)题目链接
原题链接: 相交链表(点击链接直达)
2) 算法思路
- 判断相交:两指针是否相等;
- 难点:两个链表相同节点前面的长度不同,无法控制遍历的长度。
例如,链表a
: 1=>2=>3=>4,链表b
:5=>3=>4,相同节点为3
,3
前面的链表部分长度两个不相等; - 解决:将两个链表逻辑上拼接在一起。先遍历链表
a
,遍历完后再遍历链表b
。同理,先先遍历链表b
,遍历完后再遍历链表a
。这样,相同节点前面的长度就保持一致了,可以通过遍历相同的次数走到相同的节点;
例如,链表a
逻辑上变为:1=>2=>3=>4=>5=>3=>4,链表b
逻辑上变为:5=>3=>4=>1=>2=>3=>4;
3)源码剖析
class Solution {
public:
ListNode *getIntersectionNode(ListNode *headA, ListNode *headB) {
auto p = headA, q = headB;
while (p != q) {
// p没走到A链表终点就一直往后走,走到终点就开始走B链表
p = p != NULL? p->next : headB;
// q没走到B链表终点就一直往后走,走到终点就开始走A链表
q = q != NULL? q->next : headA;
}
return p;
}
};
3)时间复杂度
O ( n ) O(n) O(n)
习题4:LeetCode 141 环型链表
1)题目链接
原题链接: 环型链表(点击链接直达)
2) 算法思路
- 明确什么叫有环;
- 明确有环和无环的区别:
- 定义:
fast
是跑得快的指针,slow
是跑的慢的指针。快指针每次走两步,慢指针每次走一步; - 有环:有环相当于
fast
和slow
两指针在环形操场跑,如果fast
和slow
相遇,那一定是fast
超过了slow
一圈; - 无环:无环相当于
fast
和slow
两指针在直道操场跑,因为快指针跑的快会先达到终点,则两指针一定不会遇到;
- 定义:
3)源码剖析
class Solution {
public:
bool hasCycle(ListNode *head) {
ListNode* slow = head, *fast = head;
while (fast && fast->next) //(1)
{
fast = fast->next->next; //(2)
slow = slow->next; //(3)
if (fast == slow) return true; //(4)
}
return false; //(5)
}
};
- (1):判断快指针是否到达终点;
- (2):快指针每次走两步;
- (3):慢指针每次走一步;
- (4):两指针相遇,证明两指针套圈了,则一定有环;
- (5):快指针先达到终点,证明无环;
4)时间复杂度
O ( n ) O(n) O(n)
习题5:LeetCode 142. 环形链表 II
1)题目链接
原题链接: 环形链表 II(点击链接直达)
LeetCode 234 回文链表 原题链接