链表 - 代码随想录

链表

链表刷题记录,基于代码随想录整理。

  • 203 移除链表元素(虚拟头结点)
  • 707 设计链表(五大操作)
  • 206 反转链表(双指针 / 递归)
  • 24 两两交换链表中的节点(虚拟头结点 + 模拟)
  • 19 删除链表的倒数第 N 个结点(双指针)
  • 160 链表相交(对齐尾部 + 双指针)
  • 142 环形链表 II(快慢指针 + 数学推导)
  • 虚拟头结点:203、707、24
  • 双指针:19、160
  • 快慢指针:142
  • 反转:206(双指针 / 递归)

203. 移除链表元素

问题描述

给你一个链表的头节点 head 和一个整数 val ,请你删除链表中所有满足 Node.val == val 的节点,并返回 新的头节点

示例 1:

img

1
2
输入:head = [1,2,6,3,4,5,6], val = 6
输出:[1,2,3,4,5]

示例 2:

1
2
输入:head = [], val = 1
输出:[]

示例 3:

1
2
输入:head = [7,7,7,7], val = 7
输出:[]

提示:

  • 列表中的节点数目在范围 [0, 104]
  • 1 <= Node.val <= 50
  • 0 <= val <= 50

思路

这里主要两种方式:

  • 直接使用原来的链表来进行删除操作。

    • 局限:当删除头节点时,需要单独定义一段逻辑,不统一

    203_链表删除元素1

  • 设置一个虚拟头结点在进行删除操作。(推荐!!)

    • return 头结点的时候,别忘了 return dummyNode->next;

203_链表删除元素6

注:使用C++来做leetcode,如果移除一个节点之后,没有手动在内存中删除这个节点,leetcode依然也是可以通过的,只不过,内存使用的空间大一些而已,但建议依然要养成手动清理内存的习惯。

代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
/**
* Definition for singly-linked list.
* struct ListNode {
* int val;
* ListNode *next;
* ListNode() : val(0), next(nullptr) {}
* ListNode(int x) : val(x), next(nullptr) {}
* ListNode(int x, ListNode *next) : val(x), next(next) {}
* };
*/
class Solution {
public:
ListNode* removeElements(ListNode* head, int val) {
ListNode* dummyHead = new ListNode(0);
dummyHead->next = head;
ListNode* cur = dummyHead;
while(cur->next != nullptr){
if(cur->next->val == val){
ListNode* tmp = cur->next;
cur->next = cur->next->next;
delete tmp;
}else{
cur = cur->next;
}
}
return dummyHead->next;
}
};
  • 时间复杂度:O(n)
  • 空间复杂度:O(1)

707. 设计链表

问题描述

你可以选择使用单链表或者双链表,设计并实现自己的链表。

单链表中的节点应该具备两个属性:valnextval 是当前节点的值,next 是指向下一个节点的指针/引用。

如果是双向链表,则还需要属性 prev 以指示链表中的上一个节点。假设链表中的所有节点下标从 0 开始。

实现 MyLinkedList 类:

  • MyLinkedList() 初始化 MyLinkedList 对象。
  • int get(int index) 获取链表中下标为 index 的节点的值。如果下标无效,则返回 -1
  • void addAtHead(int val) 将一个值为 val 的节点插入到链表中第一个元素之前。在插入完成后,新节点会成为链表的第一个节点。
  • void addAtTail(int val) 将一个值为 val 的节点追加到链表中作为链表的最后一个元素。
  • void addAtIndex(int index, int val) 将一个值为 val 的节点插入到链表中下标为 index 的节点之前。如果 index 等于链表的长度,那么该节点会被追加到链表的末尾。如果 index 比长度更大,该节点将 不会插入 到链表中。
  • void deleteAtIndex(int index) 如果下标有效,则删除链表中下标为 index 的节点。

示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
输入
["MyLinkedList", "addAtHead", "addAtTail", "addAtIndex", "get", "deleteAtIndex", "get"]
[[], [1], [3], [1, 2], [1], [1], [1]]
输出
[null, null, null, null, 2, null, 3]

解释
MyLinkedList myLinkedList = new MyLinkedList();
myLinkedList.addAtHead(1);
myLinkedList.addAtTail(3);
myLinkedList.addAtIndex(1, 2); // 链表变为 1->2->3
myLinkedList.get(1); // 返回 2
myLinkedList.deleteAtIndex(1); // 现在,链表变为 1->3
myLinkedList.get(1); // 返回 3

提示:

  • 0 <= index, val <= 1000
  • 请不要使用内置的 LinkedList 库。
  • 调用 getaddAtHeadaddAtTailaddAtIndexdeleteAtIndex 的次数不超过 2000

代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
class MyLinkedList {
public:
struct LinkedNode{
int val;
LinkedNode* next;
LinkedNode(int _val):val(_val),next(nullptr){}
};
MyLinkedList() {
_size=0;
dummyhead = new LinkedNode(0);
}

int get(int index) {
if(index >= _size || index < 0) return -1;
LinkedNode* cur = dummyhead->next;
while(index--){
cur = cur->next;
}
return cur->val;
}

void addAtHead(int val) {
LinkedNode* node = new LinkedNode(val);
LinkedNode* cur = dummyhead;
node->next = cur->next;
cur->next = node;
_size++;
}

void addAtTail(int val) {
LinkedNode* node = new LinkedNode(val);
LinkedNode* cur = dummyhead;
while(cur->next!=nullptr){
cur = cur->next;
}
cur->next = node;
_size++;
}

void addAtIndex(int index, int val) {
if(index>_size || index<0) return;
LinkedNode* node = new LinkedNode(val);
LinkedNode* cur = dummyhead;
while(index--){
cur = cur->next;
}
node->next = cur->next;
cur->next = node;
_size++;
}

void deleteAtIndex(int index) {
if(index>=_size || index<0) return;
LinkedNode* cur = dummyhead;
while(index--){
cur = cur->next;
}
LinkedNode* tmp = cur->next;
cur->next = cur->next->next;
delete tmp;
_size--;
}
private:
int _size;
LinkedNode* dummyhead;
};

/**
* Your MyLinkedList object will be instantiated and called as such:
* MyLinkedList* obj = new MyLinkedList();
* int param_1 = obj->get(index);
* obj->addAtHead(val);
* obj->addAtTail(val);
* obj->addAtIndex(index,val);
* obj->deleteAtIndex(index);
*/
  • 时间复杂度:涉及 index 的相关操作为 O(index),其余为 O(1)
  • 空间复杂度:O(n)

206. 反转链表

问题描述

给你单链表的头节点 head ,请你反转链表,并返回反转后的链表。

示例 1:

img

1
2
输入:head = [1,2,3,4,5]
输出:[5,4,3,2,1]

示例 2:

img

1
2
输入:head = [1,2]
输出:[2,1]

示例 3:

1
2
输入:head = []
输出:[]

提示:

  • 链表中节点的数目范围是 [0, 5000]
  • -5000 <= Node.val <= 5000

进阶: 链表可以选用迭代或递归方式完成反转。你能否用两种方法解决这道题?

思路

双指针法

只需改变链表的next指针的指向,就可将链表反转,而不用重新定义一个新的链表,如图所示:

206_反转链表

之前链表的头节点是元素1, 反转之后头结点就是元素5 ,这里并没有添加或者删除节点,仅仅是改变next指针的方向。

  • 定义一个cur指针,指向头结点;
  • 定义一个pre指针,初始化为null
  • cur->next 节点用tmp 指针保存,然后将cur->next 指向pre ,此时已反转第一个节点;
  • 接下来,继续移动precur指针;
  • 最终,cur 指针指向了null,循环结束,此时pre指向新的头结点,return pre
递归法

其实和双指针法是一样的逻辑,同样是当cur为空的时候循环结束,不断将cur指向pre的过程。

采用递归法时,需关注:递归结束条件、返回类型、输入参数

代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
/**
* Definition for singly-linked list.
* struct ListNode {
* int val;
* ListNode *next;
* ListNode() : val(0), next(nullptr) {}
* ListNode(int x) : val(x), next(nullptr) {}
* ListNode(int x, ListNode *next) : val(x), next(next) {}
* };
*/
class Solution {
public:
ListNode* reverseList(ListNode* head) {
ListNode* pre = nullptr;
ListNode* cur = head;
ListNode* tmp;
while(cur != nullptr){
tmp = cur->next;
cur->next = pre;
pre = cur;
cur = tmp;
}
return pre;
}
};
  • 时间复杂度:O(n)
  • 空间复杂度:O(1)
1
2
3
4
5
6
7
8
9
10
11
12
13
class Solution {
public:
//本质上其实就是两两节点反转
ListNode* reverse(ListNode* cur, ListNode* pre){
if(cur==nullptr) return pre;
ListNode* tmp = cur->next;
cur->next = pre;
return reverse(tmp, cur);
}
ListNode* reverseList(ListNode* head) {
return reverse(head,nullptr);
}
};
  • 时间复杂度:O(n),要递归处理链表的每个节点
  • 空间复杂度:O(n),递归调用了 n 层栈空间

24. 两两交换链表中的节点

问题描述

给你一个链表,两两交换其中相邻的节点,并返回交换后链表的头节点。你必须在不修改节点内部的值的情况下完成本题(即,只能进行节点交换)。

示例 1:

img

1
2
输入:head = [1,2,3,4]
输出:[2,1,4,3]

示例 2:

1
2
输入:head = []
输出:[]

示例 3:

1
2
输入:head = [1]
输出:[1]

提示:

  • 链表中节点的数目在范围 [0, 100]
  • 0 <= Node.val <= 100

思路

一定要动手,不要眼高手低!一定要画图,不画图,操作多个指针容易乱,要了解操作的先后顺序!

两两交换

两两交换操作步骤

pre → A → B → C 为例,目标是交换 A 和 B:

  1. pre->next = B(pre 指向 B)
  2. B->next = A(B 指向 A,即原来 A 的位置)
  3. A->next = C(A 指向 B 原来的下一个节点 C)
  4. 移动 pre = Acur = C,继续下一对交换

代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
/**
* Definition for singly-linked list.
* struct ListNode {
* int val;
* ListNode *next;
* ListNode() : val(0), next(nullptr) {}
* ListNode(int x) : val(x), next(nullptr) {}
* ListNode(int x, ListNode *next) : val(x), next(next) {}
* };
*/
class Solution {
public:
ListNode* swapPairs(ListNode* head) {
ListNode* dummyhead = new ListNode();
dummyhead->next = head;
ListNode* pre = dummyhead;
ListNode* cur = head;
ListNode* tmp;
while(cur!=nullptr && cur->next!=nullptr){
tmp = cur->next->next;
pre->next = cur->next;
pre->next->next = cur;
cur->next = tmp;
pre = cur;
cur = tmp;
}
return dummyhead->next;
}
};
  • 时间复杂度:O(n)
  • 空间复杂度:O(1)

19. 删除链表的倒数第 N 个结点

问题描述

给你一个链表,删除链表的倒数第 n 个结点,并且返回链表的头结点。

示例 1:

img

1
2
输入:head = [1,2,3,4,5], n = 2
输出:[1,2,3,5]

示例 2:

1
2
输入:head = [1], n = 1
输出:[]

示例 3:

1
2
输入:head = [1,2], n = 1
输出:[1]

提示:

  • 链表中结点的数目为 sz
  • 1 <= sz <= 30
  • 0 <= Node.val <= 100
  • 1 <= n <= sz

进阶: 你能尝试使用一趟扫描实现吗?

思路

双指针的经典应用: 若要删除倒数第n个节点,让fast移动n步,然后让fastslow同时移动,直到fast指向链表末尾。删掉slow所指向的节点就可以了。

  • fast = headslow = dummyhead
  • 注: fast相对于dummyhead先走n + 1步,因为只有这样同时移动的时候slow才能指向删除节点的上一个节点;

一定要动手模拟一下!!!!

代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
/**
* Definition for singly-linked list.
* struct ListNode {
* int val;
* ListNode *next;
* ListNode() : val(0), next(nullptr) {}
* ListNode(int x) : val(x), next(nullptr) {}
* ListNode(int x, ListNode *next) : val(x), next(next) {}
* };
*/
class Solution {
public:
ListNode* removeNthFromEnd(ListNode* head, int n) {
ListNode* dummyhead = new ListNode(0);
dummyhead->next = head;
ListNode* slow = dummyhead;
ListNode* fast = head; //fast提前走一步这样使slow最终指向删除节点的前一个
while(n-- && fast!=nullptr){
fast = fast->next;
}
while(fast!=nullptr){
fast = fast->next;
slow = slow->next;
}
ListNode* tmp = slow->next;
slow->next = slow->next->next;
delete tmp;
return dummyhead->next;
}
};
  • 时间复杂度:O(n)
  • 空间复杂度:O(1)

160. 链表相交

问题描述

给你两个单链表的头节点 headAheadB ,请你找出并返回两个单链表相交的起始节点。如果两个链表没有交点,返回 null

图示两个链表在节点 c1 开始相交:

img

题目数据 保证 整个链式结构中不存在环。

注意,函数返回结果后,链表必须 保持其原始结构

示例 1:

img

1
2
3
4
5
输入:intersectVal = 8, listA = [4,1,8,4,5], listB = [5,0,1,8,4,5], skipA = 2, skipB = 3
输出:Intersected at '8'
解释:相交节点的值为 8 (注意,如果两个链表相交则不能为 0)。
从各自的表头开始算起,链表 A 为 [4,1,8,4,5],链表 B 为 [5,0,1,8,4,5]。
在 A 中,相交节点前有 2 个节点;在 B 中,相交节点前有 3 个节点。

示例 2:

img

1
2
3
4
5
输入:intersectVal = 2, listA = [0,9,1,2,4], listB = [3,2,4], skipA = 3, skipB = 1
输出:Intersected at '2'
解释:相交节点的值为 2 (注意,如果两个链表相交则不能为 0)。
从各自的表头开始算起,链表 A 为 [0,9,1,2,4],链表 B 为 [3,2,4]。
在 A 中,相交节点前有 3 个节点;在 B 中,相交节点前有 1 个节点。

示例 3:

img

1
2
3
4
5
输入:intersectVal = 0, listA = [2,6,4], listB = [1,5], skipA = 3, skipB = 2
输出:null
解释:从各自的表头开始算起,链表 A 为 [2,6,4],链表 B 为 [1,5]。
由于这两个链表不相交,所以 intersectVal 必须为 0,而 skipA 和 skipB 可以是任意值。
这两个链表不相交,因此返回 null 。

提示:

  • listA 中节点数目为 m
  • listB 中节点数目为 n
  • 0 <= m, n <= 3 * 104
  • 1 <= Node.val <= 105
  • 0 <= skipA <= m
  • 0 <= skipB <= n
  • 如果 listAlistB 没有交点,intersectVal0
  • 如果 listAlistB 有交点,intersectVal == listA[skipA + 1] == listB[skipB + 1]

进阶: 你能否设计一个时间复杂度 O(n) 、仅用 O(1) 内存的解决方案?

思路

简单来说,就是求两个链表交点节点的指针 是否相等,交点不是数值相等;

两个链表若相交,那么剩余的节点数一定是相同的;

  • 求出两个链表长度的差值,然后让curA移动到,和curB 末尾对齐的位置;
  • 比较curA和curB是否相同,如果不相同,同时向后移动curA和curB;
  • 如果遇到curA == curB,则找到交点;否则循环退出返回空指针。

面试题02.07.链表相交_2

代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
/**
* Definition for singly-linked list.
* struct ListNode {
* int val;
* ListNode *next;
* ListNode(int x) : val(x), next(NULL) {}
* };
*/
class Solution {
public:
ListNode *getIntersectionNode(ListNode *headA, ListNode *headB) {
ListNode* curA = headA;
ListNode* curB = headB;
int lenA=0;
int lenB=0;
while(curA != NULL){
curA = curA->next;
lenA++;
}
while(curB != NULL){
curB = curB->next;
lenB++;
}
if(lenA < lenB){ //curA始终指向较长的链表
curA = headB;
curB = headA;
} else{
curA = headA;
curB = headB;
}
//统一格式,curA始终定义成长链表
int diff = abs(lenA-lenB);
while(diff--){
curA = curA->next;
}
while(curA!=NULL && curB!=NULL){
if(curA == curB){
return curA;
}
curA = curA->next;
curB = curB->next;
}
return NULL;

}
};
  • 时间复杂度:O(n + m)
  • 空间复杂度:O(1)

142. 环形链表 II

问题描述

给定一个链表的头节点 head ,返回链表开始入环的第一个节点。 如果链表无环,则返回 null

如果链表中有某个节点,可以通过连续跟踪 next 指针再次到达,则链表中存在环。 为了表示给定链表中的环,评测系统内部使用整数 pos 来表示链表尾连接到链表中的位置(索引从 0 开始)。如果 pos-1,则在该链表中没有环。注意:pos 不作为参数进行传递,仅仅是为了标识链表的实际情况。

不允许修改 链表。

示例 1:

img

1
2
3
输入:head = [3,2,0,-4], pos = 1
输出:返回索引为 1 的链表节点
解释:链表中有一个环,其尾部连接到第二个节点。

示例 2:

img

1
2
3
输入:head = [1,2], pos = 0
输出:返回索引为 0 的链表节点
解释:链表中有一个环,其尾部连接到第一个节点。

示例 3:

img

1
2
3
输入:head = [1], pos = -1
输出:返回 null
解释:链表中没有环。

提示:

  • 链表中节点的数目范围在范围 [0, 104]
  • -105 <= Node.val <= 105
  • pos 的值为 -1 或者链表中的一个有效索引

进阶: 你是否可以使用 O(1) 空间解决此题?

思路

主要考察两知识点:

  • 判断链表是否环
  • 如果有环,如何找到这个环的入口
判断链表是否有环

快慢指针法,分别定义 fastslow 指针,从头结点出发,fast指针每次移动两个节点,slow指针每次移动一个节点,如果 fastslow指针在途中相遇 ,说明这个链表有环。

  • 相对于slow来说,fast是一个节点一个节点的靠近slow,所以fast一定可以和slow重合。

141.环形链表

如何找到这个环的入口

假设从头结点到环形入口节点 的节点数为x。 环形入口节点到 fast指针与slow指针相遇节点 节点数为y。 从相遇节点 再到环形入口节点节点数为 z。 所以要找环形的入口,就要求解x,因为x表示 头结点到 环形入口节点的的距离。

环入口推导过程
  • (x + y) * 2 = x + y + n (y + z)fast指针走过的节点数 = slow指针走过的节点数 * 2)
  • 化简:x = (n - 1) (y + z) + z
  • 从头结点出发一个指针index1,从相遇节点 也出发一个指针index2,两个指针每次只走一个节点, 那么当这两个指针相遇的时候就是 环形入口的节点index2 指针在环里多转了(n-1)圈。

img

代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
/**
* Definition for singly-linked list.
* struct ListNode {
* int val;
* ListNode *next;
* ListNode(int x) : val(x), next(NULL) {}
* };
*/
class Solution {
public:
ListNode *detectCycle(ListNode *head) {
ListNode* slow = head;
ListNode* fast = head;
while(fast!=NULL && fast->next!=NULL){
slow = slow->next;
fast = fast->next->next;
//判断是否有环
if(slow == fast){
ListNode* pre = slow;
ListNode* cur = head;
//求相交点
while(pre != cur){
pre = pre->next;
cur = cur->next;
}
return pre;
}
}
return NULL;
}
};
  • 时间复杂度:O(n),快慢指针相遇前,指针走的次数小于链表长度,快慢指针相遇后,两个指针走的次数也小于链表长度,总体为走的次数小于 2n
  • 空间复杂度:O(1)

总结

虚拟头结点: 统一头结点和其他节点的处理情况,避免对头结点的单独判断。
设计链表常见的五个操作: 获取第 index 个节点的值、在头部/尾部/指定位置插入节点、删除指定位置的节点。
双指针法: 快慢指针先行 n 步实现倒数定位;长短链表对齐实现相交检测。
快慢指针: 判环 + 数学推导求环入口。
反转链表: 双指针迭代与递归两种写法,核心都是逐节点翻转 next 指向。
一定要动手模拟!!