链表

链表

LoftyDust Lv1

七道链表题

合并两个有序链表

将两个升序链表合并为一个新的 升序 链表并返回。新链表是通过拼接给定的两个链表的所有节点组成的。

示例 1:

img

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

示例 2:

1
2
输入:l1 = [], l2 = []
输出:[]

示例 3:

1
2
输入:l1 = [], l2 = [0]
输出:[0]

提示:

  • 两个链表的节点数目范围是 [0, 50]
  • -100 <= Node.val <= 100
  • l1l2 均按 非递减顺序 排列

代码中用到一个链表的算法题中是很常见的「虚拟头结点」技巧

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
/**
* 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* mergeTwoLists(ListNode* list1, ListNode* list2) {
ListNode* dummy = new ListNode(-1);
ListNode* p = dummy;
ListNode* p1 = list1, *p2 = list2;
while (p1 != NULL && p2 != NULL) {
if (p1 -> val < p2 -> val) {
p -> next = p1;
p1 = p1 -> next;
} else {
p -> next = p2;
p2 = p2 -> next;
}
p = p -> next;
}
if (p1 != NULL) {
p -> next = p1;
} else {
p -> next = p2;
}
return dummy -> next;
}
};

单链表的分解

给你一个链表的头节点 head 和一个特定值 x ,请你对链表进行分隔,使得所有 小于 x 的节点都出现在 大于或等于 x 的节点之前。

你应当 保留 两个分区中每个节点的初始相对位置。

示例 1:

img

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

示例 2:

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

提示:

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

整体逻辑和合并有序链表非常相似,细节直接看代码吧,注意虚拟头结点的运用:

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
/**
* 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* partition(ListNode* head, int x) {
// 存放小于x的节点
ListNode* dummy1 = new ListNode(-1);
// 存放大于等于x的节点
ListNode* dummy2 = new ListNode(-1);
ListNode* p = head;
ListNode* l1 = dummy1, *l2 = dummy2;
while (p != NULL) {
if (p->val < x) {
l1->next = p;
l1 = l1->next;
} else {
l2->next = p;
l2 = l2->next;
}
// 不能直接让 p 指针前进,
// p = p.next
// 断开原链表中的每个节点的 next 指针
ListNode* temp = p->next;
p->next = NULL;
p = temp;
}
l1->next = dummy2->next;
return dummy1->next;
}
};

细节:如果你不断开原链表中的每个节点的 next 指针,那么就会出错,因为结果链表中会包含一个环

总的来说,如果我们需要把原链表的节点接到新链表上,而不是 new 新节点来组成新链表的话,那么断开节点和原链表之间的链接可能是必要的。那其实我们可以养成一个好习惯,但凡遇到这种情况,就把原链表的节点断开,这样就不会出错了。

合并 k 个有序链表

给你一个链表数组,每个链表都已经按升序排列。

请你将所有链表合并到一个升序链表中,返回合并后的链表。

示例 1:

1
2
3
4
5
6
7
8
9
10
输入:lists = [[1,4,5],[1,3,4],[2,6]]
输出:[1,1,2,3,4,4,5,6]
解释:链表数组如下:
[
1->4->5,
1->3->4,
2->6
]
将它们合并到一个有序链表中得到。
1->1->2->3->4->4->5->6

示例 2:

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

示例 3:

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

提示:

  • k == lists.length
  • 0 <= k <= 10^4
  • 0 <= lists[i].length <= 500
  • -10^4 <= lists[i][j] <= 10^4
  • lists[i]升序 排列
  • lists[i].length 的总和不超过 10^4

难点:如何快速得到 k 个节点中的最小节点,接到结果链表上?

用到 优先级队列(二叉堆) 这种数据结构,把链表节点放入一个最小堆,就可以每次获得 k 个节点中的最小节点:

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
/**
* 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* mergeKLists(vector<ListNode*>& lists) {
ListNode* dummy = new ListNode(-1);
ListNode* p = dummy;
priority_queue<ListNode*,vector<ListNode*>,function<bool(ListNode*, ListNode*)>> pq([] (ListNode* l1, ListNode* l2) {return l1 -> val > l2 -> val; });
for (auto head : lists) {
if (head != NULL) {
pq.push(head);
}
}
while (!pq.empty()) {
ListNode* node = pq.top();
pq.pop();
p -> next = node;
if(node -> next != NULL) {
pq.push(node -> next);
}
p = p -> next;
}
return dummy -> next;

}
};

单链表的倒数第 k 个节点

(深圳软牛笔试编程题)滑动窗口思想:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// 返回链表的倒数第 k 个节点
ListNode* findFromEnd(ListNode* head, int k) {
ListNode* p1 = head;
// p1 先走 k 步
for (int i = 0; i < k; i++) {
p1 = p1 -> next;
}
ListNode* p2 = head;
// p1 和 p2 同时走 n - k 步
while (p1 != nullptr) {
p2 = p2 -> next;
p1 = p1 -> next;
}
// p2 现在指向第 n - k + 1 个节点,即倒数第 k 个节点
return p2;
}

给你一个链表,删除链表的倒数第 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

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

原理同上,找倒数第 k+1 个节点

单链表的中点

依然快慢指针,一个走两步,一个走一步

1
2
3
4
5
6
7
8
9
10
11
12
13
ListNode* middleNode(ListNode* head) {
// 快慢指针初始化指向 head
ListNode* slow = head;
ListNode* fast = head;
// 快指针走到末尾时停止
while (fast != nullptr && fast->next != nullptr) {
// 慢指针走一步,快指针走两步
slow = slow->next;
fast = fast->next->next;
}
// 慢指针指向中点
return slow;
}

判断链表是否包含环

快慢指针,如果相遇则有环

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
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;
}

当然,这个问题还有进阶版,也是力扣第 142 题「环形链表 IIopen in new window 」:如果链表中含有环,如何计算这个环的起点?

证明来自labuladong:

我们假设快慢指针相遇时,慢指针 slow 走了 k 步,那么快指针 fast 一定走了 2k 步:img

fast 一定比 slow 多走了 k 步,这多走的 k 步其实就是 fast 指针在环里转圈圈,所以 k 的值就是环长度的「整数倍」。

假设相遇点距环的起点的距离为 m,那么结合上图的 slow 指针,环的起点距头结点 head 的距离为 k - m,也就是说如果从 head 前进 k - m 步就能到达环起点。

巧的是,如果从相遇点继续前进 k - m 步,也恰好到达环起点。因为结合上图的 fast 指针,从相遇点开始走k步可以转回到相遇点,那走 k - m 步肯定就走到环起点了:

img

所以,只要我们把快慢指针中的任一个重新指向 head,然后两个指针同速前进,k - m 步后一定会相遇,相遇之处就是环的起点了。

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* fast = head;
ListNode* slow = head;
while (fast && fast -> next) {
slow = slow -> next;
fast = fast -> next -> next;
if (slow == fast) {
break;
}
}
if (fast == NULL || fast -> next == NULL) {
return NULL;
}
slow = head;
while (slow != fast) {
slow = slow -> next;
fast = fast -> next;
}
return slow;
}
};

两个链表是否相交

**解决这个问题的关键是,通过某些方式,让 p1p2 能够同时到达相交节点 c1**。

所以,我们可以让 p1 遍历完链表 A 之后开始遍历链表 B,让 p2 遍历完链表 B 之后开始遍历链表 A,这样相当于「逻辑上」两条链表接在了一起。

如果这样进行拼接,就可以让 p1p2 同时进入公共部分,也就是同时到达相交节点 c1

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
/**
* 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* p1 = headA, *p2 = headB;
while (p1 != p2) {
if (p1 == NULL) {
p1 = headB;
} else {
p1 = p1 -> next;
}
if (p2 == NULL) {
p2 = headA;
} else {
p2 = p2 -> next;
}
}
return p1;
}
};

递归反转单链表

递归反转整个单链表

Code:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
/**
* 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) {
if (head == NULL || head -> next == NULL) {
return head;
}
ListNode* last = reverseList(head -> next);
head -> next -> next = head;
head -> next = NULL;
return last;
}
};

反转单链表前N个节点

具体思路差不多:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
/**
* 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* successor = nullptr;
ListNode* reverseN(ListNode* head, int n) {
if (n == 1) {
successor = head -> next;
return head;
}
ListNode* last = reverseList(head -> next, n - 1);
head -> next -> next = head;
head -> next = successor;
return last;
}
};

具体的区别:

1、base case 变为 n == 1,反转一个元素,就是它本身,同时要记录后驱节点

2、刚才我们直接把 head.next 设置为 null,因为整个链表反转后原来的 head 变成了整个链表的最后一个节点。但现在 head 节点在递归反转之后不一定是最后一个节点了,所以要记录后驱 successor(第 n + 1 个节点),反转之后将 head 连接上。

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
32
/**
* 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* successor = NULL;
//反转前N个
ListNode* reverseN(ListNode* head, int n) {
if (n == 1) {
successor = head -> next;
return head;
}
ListNode* last = reverseN(head -> next, n - 1);
head -> next -> next = head;
head -> next = successor;
return last;
}
ListNode* reverseBetween(ListNode* head, int left, int right) {
if (left == 1) {
return reverseN(head, right);
}
head -> next = reverseBetween(head -> next, left - 1, right - 1);
return head;
}
};

如何 K 个一组反转链表

给你链表的头节点 head ,每 k 个节点一组进行翻转,请你返回修改后的链表。

k 是一个正整数,它的值小于或等于链表的长度。如果节点总数不是 k 的整数倍,那么请将最后剩余的节点保持原有顺序。

你不能只是单纯的改变节点内部的值,而是需要实际进行节点交换。

示例 1:

img

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

示例 2:

img

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

提示:

  • 链表中的节点数目为 n
  • 1 <= k <= n <= 5000
  • 0 <= Node.val <= 1000

进阶:你可以设计一个只用 O(1) 额外内存空间的算法解决此问题吗?

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
/**
* 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* reverse(ListNode* a, ListNode* b) {
ListNode* pre = NULL, *curr = a, *nxt = a;
while (curr != b) {
nxt = curr -> next;
curr -> next = pre;
pre = curr;
curr = nxt;
}
return pre;

}
ListNode* reverseKGroup(ListNode* head, int k) {
if (head == NULL) {
return NULL;
}
ListNode *a, *b;
a = b = head;
for (int i = 0; i < k; i++) {
if (b == NULL) {
return head;
}
b = b -> next;
}
ListNode* newHead = reverse(a, b);
a -> next = reverseKGroup(b, k);
return newHead;
}
};

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
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
/**
* 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:
bool isPalindrome(ListNode* head) {
if (head == nullptr) {
return true;
}

ListNode *firstHalfEnd = endOfFirstHalf(head);
ListNode *secondHalfStart = reverseList(firstHalfEnd -> next);

ListNode *p1 = head;
ListNode *p2 = secondHalfStart;
bool result = true;
while (result && p2 != nullptr) {
if (p1 -> val != p2 -> val) {
result = false;
}
p1 = p1 -> next;
p2 = p2 -> next;
}
firstHalfEnd -> next = reverseList(secondHalfStart);
return result;
}
ListNode *reverseList(ListNode *head) {
ListNode *prev = nullptr;
ListNode *curr = head;
while(curr != nullptr) {
ListNode *next = curr -> next;
curr -> next = prev;
prev = curr;
curr = next;
}
return prev;
}
ListNode *endOfFirstHalf(ListNode *head) {
ListNode *fast = head;
ListNode *slow = head;
while (fast -> next != nullptr && fast -> next -> next != nullptr) {
fast = fast -> next -> next;
slow = slow -> next;
}
return slow;
}
};
  • Title: 链表
  • Author: LoftyDust
  • Created at : 2024-03-04 17:15:58
  • Updated at : 2024-05-18 12:03:31
  • Link: https://loftydust.top/e3601277190a.html
  • License: This work is licensed under CC BY-NC-SA 4.0.
Comments