LeetCode Hot 100 | 链表(下)· 分组翻转与设计(C++ 题解)
本篇补全 Hot100 链表专题剩余 5 道题:
25 / 138 / 148 / 23 / 146。
目录
- [LeetCode Hot 100 | 链表(下)· 分组翻转与设计(C++ 题解)](#LeetCode Hot 100 | 链表(下)· 分组翻转与设计(C++ 题解))
-
- [一、25. Reverse Nodes in k-Group(K 个一组翻转链表)🔴 困难](#一、25. Reverse Nodes in k-Group(K 个一组翻转链表)🔴 困难)
-
- 题目描述
- 图解
- 解题思路(严格对应下面这份代码)
- [C++ 代码](#C++ 代码)
- [二、138. Copy List with Random Pointer(随机链表的复制)🟡 中等](#二、138. Copy List with Random Pointer(随机链表的复制)🟡 中等)
-
- 题目描述
- 图解
- 解题思路(对应你的两遍哈希)
- [C++ 代码](#C++ 代码)
- [三、148. Sort List(排序链表)🟡 中等](#三、148. Sort List(排序链表)🟡 中等)
- [四、23. Merge k Sorted Lists(合并 K 个升序链表)🔴 困难](#四、23. Merge k Sorted Lists(合并 K 个升序链表)🔴 困难)
-
- 题目描述
- 图解
- 解题思路(对应你的两两合并,不是堆)
- [C++ 代码](#C++ 代码)
- [五、146. LRU Cache(LRU 缓存)🟡 中等](#五、146. LRU Cache(LRU 缓存)🟡 中等)
- 总结
一、25. Reverse Nodes in k-Group(K 个一组翻转链表)🔴 困难
题目描述
给你链表的头节点 head,每 k 个节点一组进行翻转,返回修改后的链表。
k 是正整数,它的值小于或等于链表的长度。如果节点总数不是 k 的整数倍,那么请将最后剩余的节点保持原有顺序。
你不能只是单纯改变节点内部的值,而是需要实际进行节点交换。
示例 1:
输入:head = [1,2,3,4,5], k = 2
输出:[2,1,4,3,5]
示例 2:
输入:head = [1,2,3,4,5], k = 3
输出:[3,2,1,4,5]
图解

解题思路(严格对应下面这份代码)
你的代码核心是三件套:dummy、node0、一组内的头插翻转。
1. 先量长度
cpp
/**
* 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* reverseKGroup(ListNode* head, int k) {
ListNode dummy(0, head);
ListNode *node0 = &dummy;
ListNode *cur = head;
ListNode *prev = nullptr;
ListNode *tmp = nullptr;
int len = 0;
//get len
while(cur) {
len++;
cur = cur->next;
}
cur = head;
while (len >= k) {
len -= k;
for (int i = 0; i < k; i++) {
tmp = cur->next;
cur->next = prev;
prev = cur;
cur = tmp;
}
tmp = node0->next;
tmp->next = cur;
node0->next = prev;
node0 = tmp;
}
return dummy.next;
}
};
先知道总共能完整翻转几组。后面用 while (len >= k),每翻一组就 len -= k。
对示例 [1,2,3,4,5], k=2:len=5,能翻 2 组,最后单独的 5 不动------这正好对应代码逻辑。
2. 一组内:经典 4 行头插
cpp
for (int i = 0; i < k; i++) {
tmp = cur->next;
cur->next = prev;
prev = cur;
cur = tmp;
}
这一段和「反转链表」一模一样。循环结束时:
prev:本段新头cur:下一段起点(还没翻)- 本段旧头(翻转前
node0->next)现在变成段尾
3. 用 node0 把段接回主链
cpp
tmp = node0->next; // 旧段头 = 新段尾
tmp->next = cur; // 段尾接到剩余链表
node0->next = prev; // 前驱接到新段头
node0 = tmp; // 前驱挪到段尾,准备下一组
dummy 挂在最前面,最后 return dummy.next。整段过程原地完成,没有额外数组。
为什么正确?
每组恰好翻 k 个,组与组之间靠 node0 串起来;不足 k 的尾巴因为进不了 while (len >= k),保持原序。
复杂度: 时间 O(n)(每个节点最多进翻转循环一次),空间 O(1)
C++ 代码
cpp
/**
* 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* reverseKGroup(ListNode* head, int k) {
ListNode dummy(0, head);
ListNode *node0 = &dummy;
ListNode *cur = head;
ListNode *prev = nullptr;
ListNode *tmp = nullptr;
int len = 0;
//get len
while(cur) {
len++;
cur = cur->next;
}
cur = head;
while (len >= k) {
len -= k;
for (int i = 0; i < k; i++) {
tmp = cur->next;
cur->next = prev;
prev = cur;
cur = tmp;
}
tmp = node0->next;
tmp->next = cur;
node0->next = prev;
node0 = tmp;
}
return dummy.next;
}
};
二、138. Copy List with Random Pointer(随机链表的复制)🟡 中等
题目描述
给你一个长度为 n 的链表,每个节点包含一个额外的随机指针 random,它可指向链表中的任何节点或空。
请构造这个链表的 深拷贝,并返回拷贝后的头节点。
示例:
输入:head = [[7,null],[13,0],[11,4],[10,2],[1,0]]
输出:[[7,null],[13,0],[11,4],[10,2],[1,0]]
图解

解题思路(对应你的两遍哈希)
你没有用「交错插入」那套 O(1) 空间技巧,而是更直观的 unordered_map<Node*, Node*>:
第一遍:只造新节点
cpp
/*
// Definition for a Node.
class Node {
public:
int val;
Node* next;
Node* random;
Node(int _val) {
val = _val;
next = NULL;
random = NULL;
}
};
*/
class Solution {
public:
Node* copyRandomList(Node* head) {
unordered_map<Node*, Node*> umap;
Node *cur = head;
while (cur) {
umap[cur] = new Node(cur->val);
cur = cur->next;
}
cur = head;
while (cur) {
Node *newNode = umap[cur];
newNode->next = umap[cur->next];
newNode->random = umap[cur->random];
cur = cur->next;
}
return umap[head];
}
};
此时新节点的 next/random 都还是空的,但「旧地址 → 新地址」映射齐了。
第二遍:接线
cpp
Node *newNode = umap[cur];
newNode->next = umap[cur->next];
newNode->random = umap[cur->random];
关键点:umap[nullptr] 在 C++ 里对 operator[] 会插入默认值------你这里依赖的是:当 cur->next/random 为空时,映射后也应为空。若判空更稳妥可以写成三元,但以你 AC 的版本为准,讲解按这版走。
最后 return umap[head]。
复杂度: 时间 O(n),空间 O(n)
C++ 代码
cpp
/*
// Definition for a Node.
class Node {
public:
int val;
Node* next;
Node* random;
Node(int _val) {
val = _val;
next = NULL;
random = NULL;
}
};
*/
class Solution {
public:
Node* copyRandomList(Node* head) {
unordered_map<Node*, Node*> umap;
Node *cur = head;
while (cur) {
umap[cur] = new Node(cur->val);
cur = cur->next;
}
cur = head;
while (cur) {
Node *newNode = umap[cur];
newNode->next = umap[cur->next];
newNode->random = umap[cur->random];
cur = cur->next;
}
return umap[head];
}
};
三、148. Sort List(排序链表)🟡 中等
题目描述
给你链表的头节点 head,请将其按 升序 排列并返回排序后的链表。
示例 1:
输入:head = [4,2,1,3]
输出:[1,2,3,4]
图解

解题思路(对应你的 middle + meger + 递归)
这就是链表归并排序,拆成你代码里的三个函数:
1. middle(head):断开左右两半
快慢指针前进,同时用 pre 记 slow 前驱:
cpp
/**
* 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* middle(ListNode *head) {
ListNode *slow = head;
ListNode *fast = head;
ListNode *pre;
while(fast && fast->next) {
pre = slow;
fast = fast->next->next;
slow = slow->next;
}
pre->next = nullptr;
return slow;
}
ListNode * meger(ListNode *left, ListNode *right) {
ListNode dummy;
ListNode *node = &dummy;
while (left && right) {
if (left->val < right->val) {
node->next = left;
left = left->next;
} else {
node->next = right;
right = right->next;
}
node = node->next;
}
if (left || right)
node->next = left == nullptr ? right : left;
return dummy.next;
}
ListNode* sortList(ListNode* head) {
if (!head || !head->next)
return head;
ListNode *head2 = middle(head);
ListNode *left = sortList(head);
ListNode *right = sortList(head2);
return meger(left, right);
}
};
对 [4,2,1,3]:断开后左 4→2,右 1→3。
注意:返回的是右半 head ,左半仍从原 head 开始------所以后面才能 sortList(head) / sortList(head2)。
2. 递归
cpp
if (!head || !head->next) return head;
ListNode *head2 = middle(head);
ListNode *left = sortList(head);
ListNode *right = sortList(head2);
return meger(left, right);
3. meger(你提交里的原函数名)
哑节点双指针,谁小接谁,最后接剩余。和「合并两个有序链表」同一套路。
复杂度: 时间 O(n log n),空间 O(log n)(递归栈)
说明:函数名
meger是你 AC 提交里的原拼写,文章保留,不擅自改你的代码。
C++ 代码
cpp
/**
* 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* middle(ListNode *head) {
ListNode *slow = head;
ListNode *fast = head;
ListNode *pre;
while(fast && fast->next) {
pre = slow;
fast = fast->next->next;
slow = slow->next;
}
pre->next = nullptr;
return slow;
}
ListNode * meger(ListNode *left, ListNode *right) {
ListNode dummy;
ListNode *node = &dummy;
while (left && right) {
if (left->val < right->val) {
node->next = left;
left = left->next;
} else {
node->next = right;
right = right->next;
}
node = node->next;
}
if (left || right)
node->next = left == nullptr ? right : left;
return dummy.next;
}
ListNode* sortList(ListNode* head) {
if (!head || !head->next)
return head;
ListNode *head2 = middle(head);
ListNode *left = sortList(head);
ListNode *right = sortList(head2);
return meger(left, right);
}
};
四、23. Merge k Sorted Lists(合并 K 个升序链表)🔴 困难
题目描述
给你一个链表数组,每个链表都已经按升序排列。请把所有链表合并成一个升序链表并返回。
示例:
输入:lists = [[1,4,5],[1,3,4],[2,6]]
输出:[1,1,2,3,4,4,5,6]
图解

解题思路(对应你的两两合并,不是堆)
你的主循环非常直接:
cpp
/**
* 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* merge(ListNode *left, ListNode *right) {
ListNode dummy;
ListNode *cur = &dummy;
while (left && right) {
if (left->val < right->val) {
cur->next = left;
left = left->next;
} else {
cur->next = right;
right = right->next;
}
cur = cur->next;
}
if (left || right)
cur->next = (left == nullptr) ? right : left;
return dummy.next;
}
ListNode* mergeKLists(vector<ListNode*>& lists) {
if (lists.empty())
return nullptr;
while (lists.size() > 1) {
vector<ListNode*> tmp;
for (int i = 0; i < lists.size(); i+=2) {
ListNode *head1 = lists[i];
ListNode *head2 = nullptr;
if (i + 1 < lists.size())
head2 = lists[i + 1];
tmp.push_back(merge(head1, head2));
}
lists = tmp;
}
return lists[0];
}
};
- 每轮把链表条数大约减半
- 奇数条时
head2=nullptr,merge等于原样保留那一条 merge仍是哑节点双指针
对示例三轮思想:先合并前两条,第三条单独留下;再把结果和第三条合并。
复杂度: 设总节点 N、链表数 k,大约 O(N log k)
C++ 代码
cpp
/**
* 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* merge(ListNode *left, ListNode *right) {
ListNode dummy;
ListNode *cur = &dummy;
while (left && right) {
if (left->val < right->val) {
cur->next = left;
left = left->next;
} else {
cur->next = right;
right = right->next;
}
cur = cur->next;
}
if (left || right)
cur->next = (left == nullptr) ? right : left;
return dummy.next;
}
ListNode* mergeKLists(vector<ListNode*>& lists) {
if (lists.empty())
return nullptr;
while (lists.size() > 1) {
vector<ListNode*> tmp;
for (int i = 0; i < lists.size(); i+=2) {
ListNode *head1 = lists[i];
ListNode *head2 = nullptr;
if (i + 1 < lists.size())
head2 = lists[i + 1];
tmp.push_back(merge(head1, head2));
}
lists = tmp;
}
return lists[0];
}
};
五、146. LRU Cache(LRU 缓存)🟡 中等
题目描述
请你设计并实现一个满足 LRU(最近最少使用)缓存约束的数据结构。
实现 LRUCache 类:
LRUCache(int capacity)以正整数作为容量 capacity 初始化int get(int key)如果关键字 key 存在于缓存,返回值,否则返回 -1void put(int key, int value)插入或更新;若容量满了,先淘汰最久未使用的
get 和 put 必须在 O(1) 平均时间复杂度完成。
图解

解题思路(对应你的:哈希 + 环形双向链表)
你自己实现了双向链表节点,并用 root 做成 环形哨兵:
cpp
class LRUCache {
struct ListNode {
ListNode *prev;
ListNode *next;
int value_;
int key_;
ListNode(int key = 0, int value = 0) {
prev = NULL;
next = NULL;
value_ = value;
key_ = key;
}
};
public:
int capacity_;
ListNode *root;
unordered_map<int, ListNode *> umap;
LRUCache(int capacity) {
capacity_ = capacity;
root = new ListNode();
root->next = root;
root->prev = root;
}
int get(int key) {
if (umap.count(key)) {
remove(umap[key]);
headInsert(umap[key]);
return umap[key]->value_;
} else {
return -1;
}
}
void put(int key, int value) {
if (umap.count(key)) {
umap[key]->value_ = value;
remove(umap[key]);
} else {
umap[key] = new ListNode(key, value);
}
headInsert(umap[key]);
if (umap.size() > capacity_) {
ListNode *tmp = umap[root->prev->key_];
umap.erase(root->prev->key_);
remove(tmp);
}
}
void remove(ListNode *list) {
ListNode* prev = list->prev;
ListNode* next = list->next;
prev->next = next;
next->prev = prev;
}
void headInsert(ListNode *list) {
ListNode *tmp;
tmp = root->next;
root->next = list;
list->prev = root;
list->next = tmp;
tmp->prev = list;
}
};
/**
* Your LRUCache object will be instantiated and called as such:
* LRUCache* obj = new LRUCache(capacity);
* int param_1 = obj->get(key);
* obj->put(key,value);
*/
约定:
root->next一侧是 最新root->prev一侧是 最旧(淘汰就删它)umap[key] -> ListNode*保证 O(1) 定位
两个原语
remove(node):左右邻居互连,摘出去headInsert(node):插到root后面(变成最新)
get
命中:remove 再 headInsert,返回 value_;否则 -1。
put
- 已存在:改值,先
remove - 不存在:
new ListNode放进umap - 然后
headInsert - 若
umap.size() > capacity_:删root->prev,并umap.erase
这就是标准 LRU:读写都把节点提到最前,满了丢最旧。
复杂度: get/put 均摊 O(1)
C++ 代码
cpp
class LRUCache {
struct ListNode {
ListNode *prev;
ListNode *next;
int value_;
int key_;
ListNode(int key = 0, int value = 0) {
prev = NULL;
next = NULL;
value_ = value;
key_ = key;
}
};
public:
int capacity_;
ListNode *root;
unordered_map<int, ListNode *> umap;
LRUCache(int capacity) {
capacity_ = capacity;
root = new ListNode();
root->next = root;
root->prev = root;
}
int get(int key) {
if (umap.count(key)) {
remove(umap[key]);
headInsert(umap[key]);
return umap[key]->value_;
} else {
return -1;
}
}
void put(int key, int value) {
if (umap.count(key)) {
umap[key]->value_ = value;
remove(umap[key]);
} else {
umap[key] = new ListNode(key, value);
}
headInsert(umap[key]);
if (umap.size() > capacity_) {
ListNode *tmp = umap[root->prev->key_];
umap.erase(root->prev->key_);
remove(tmp);
}
}
void remove(ListNode *list) {
ListNode* prev = list->prev;
ListNode* next = list->next;
prev->next = next;
next->prev = prev;
}
void headInsert(ListNode *list) {
ListNode *tmp;
tmp = root->next;
root->next = list;
list->prev = root;
list->next = tmp;
tmp->prev = list;
}
};
/**
* Your LRUCache object will be instantiated and called as such:
* LRUCache* obj = new LRUCache(capacity);
* int param_1 = obj->get(key);
* obj->put(key,value);
*/
总结
| 题号 | 题目 | 难度 | 你的写法核心 | 时间复杂度 |
|---|---|---|---|---|
| 25 | K 个一组翻转链表 | 🔴困难 | dummy + 分组头插 | O(n) |
| 138 | 随机链表的复制 | 🟡中等 | 两遍哈希映射 | O(n) |
| 148 | 排序链表 | 🟡中等 | 归并(middle/meger) | O(n log n) |
| 23 | 合并 K 个升序链表 | 🔴困难 | 两两 merge | O(N log k) |
| 146 | LRU 缓存 | 🟡中等 | 哈希 + 环形双向链表 | O(1) |
链表下篇的本质: 指针关系能画清楚,代码就只是把图翻译成 next/prev。