写在前面
其实链表的题大多数都可以用双指针(快慢指针)来做, 只不过不像数组中的双指针那样左右移动, 链表(单链表)的双指针方法都是向后移动的.
刷完链表专题, 可以总结一下链表中常用的套路: (当然, 刷题还是要到位)
- 不要怕浪费空间, 头(尾)结点的 dummy 最好用.
- 不要怕浪费指针, 多个指针的移动别搞混.
- 不要怕画图, 很多题画个链表模拟一下指针移动就能解出来了.
链表声明
这里是力扣通用的链表结构体声明.
需要注意不要冲突定义, 比如设计链表这道题如果在全局定义了
ListNode
类就会报重定义错误…
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) {}
};
基本操作
基本增删改查
要照顾到的点很多, 尤其是设计头插尾插时候, 其实可以直接调用现成的AddAtIndex
方法, 前提是这个方法的边界条件都没问题了.
- 索引的有效性;
- 头结点的添加(通过 dummy 实现)
class MyLinkedList {
struct Node {
int val;
Node* next;
Node() : Node(0, nullptr) {}
Node(int _val) : Node(_val, nullptr) {}
Node(int _val, Node* _next) : val(_val), next(_next) {}
};
Node* head;
size_t size;
Node* traverse(int idx) {
auto cur{head};
while (idx--) cur = cur->next;
return cur;
}
public:
MyLinkedList() : head(new Node), size(0) {}
int get(int index) {
if (index >= size || index < 0) return -1;
return traverse(index + 1)->val;
}
void addAtHead(int val) { addAtIndex(0, val); }
void addAtTail(int val) { addAtIndex(size, val); }
void addAtIndex(int index, int val) {
if (index > size) return;
++size;
auto cur = traverse(index);
auto tmp{new Node(val, cur->next)};
cur->next = tmp;
}
void deleteAtIndex(int index) {
if (index >= size || index < 0) return;
--size;
auto cur = traverse(index);
auto tmp{cur->next};
cur->next = cur->next->next;
delete tmp;
}
};
添加指定节点
class Solution {
public:
Node* insert(Node* head, int insertVal) {
if (!head) { // 空节点
head = new Node(insertVal);
head->next = head;
return head;
} else if (head->next == head) { // 单节点
head->next = new Node(insertVal, head);
return head;
}
bool flg{};
auto cur(head);
for (; cur->next != head; cur = cur->next) {
// 三种情况: 顺序插入, 头插入(最小值), 尾插入(最大值)
if ((cur->val <= insertVal && cur->next->val >= insertVal) ||
(cur->val > cur->next->val &&
(cur->next->val > insertVal || cur->val < insertVal))) {
flg = true;
auto node = new Node(insertVal, cur->next);
cur->next = node;
break;
}
}
if (!flg) { // 遍历完后还未找到插入位置(都相同), 插在末尾
auto node = new Node(insertVal, cur->next);
cur->next = node;
}
return head;
}
};
删除指定节点
19. 删除链表的倒数第 N 个结点 - 力扣(LeetCode);
class Solution {
public:
ListNode *removeNthFromEnd(ListNode *head, int n) {
ListNode *dummy = new ListNode(0, head);
auto first = dummy, second = dummy;
while (n--) first = first->next;
while (first && first->next) {
first = first->next;
second = second->next;
}
second->next = second->next->next;
return dummy->next;
}
};
83. 删除排序链表中的重复元素 - 力扣(LeetCode);
双指针的方法: (比较复杂)
class Solution {
public:
ListNode *deleteDuplicates(ListNode *head) {
if (!head) return head;
using lp = ListNode *;
lp cur = head, pre{};
bool flg{};
while (cur) {
while (pre && cur && cur->val == pre->val)
cur = cur->next, flg = true;
if (flg) pre->next = cur, flg = false;
pre = cur;
if (!cur) break;
cur = cur->next;
}
return head;
}
};
单指针其实就够用了:
class Solution {
public:
ListNode *deleteDuplicates(ListNode *head) {
if (!head) return head;
ListNode *cur = head;
while (cur->next) {
if (cur->val == cur->next->val)
cur->next = cur->next->next;
else
cur = cur->next;
}
return head;
}
};
82. 删除排序链表中的重复元素 II - 力扣(LeetCode); 双指针+哑结点
class Solution {
public:
ListNode *deleteDuplicates(ListNode *head) {
if (!head) return head;
ListNode *dummy = new ListNode(-101, head), *cur = head, *pre = dummy;
bool flg{};
while (cur && cur->next) {
while (cur->next && cur->val == cur->next->val)
cur = cur->next, flg = true;
if (flg)
flg = false, pre->next = cur->next;
else
pre = cur; // 若此轮遍历后不是重复值, 更新pre
cur = cur->next;
}
return dummy->next;
}
};
同样, 单指针也可以做, 框架与上一题一样:
class Solution {
public:
ListNode *deleteDuplicates(ListNode *head) {
if (!head) return head;
ListNode *dummy = new ListNode(-101, head), *cur = dummy;
while (cur->next && cur->next->next) {
if (cur->next->val == cur->next->next->val) {
int x = cur->next->val;
while (cur->next && cur->next->val == x)
cur->next = cur->next->next;
} else
cur = cur->next;
}
return dummy->next;
}
};
1171. 从链表中删去总和值为零的连续节点;(这个题的处理过程很像 LRU 缓存, 需要哈希记录前缀和)
一个问题就是, 如果释放了被删除的节点内存, 则会报错
heap-use-after-free
, 很奇怪, 本地测试没问题
class Solution {
public:
ListNode *removeZeroSumSublists(ListNode *head) {
auto dum = new ListNode(0, head), cur = dum;
unordered_map<int, ListNode *> dic;
int sum{};
while (cur) {
sum += cur->val;
dic[sum] = cur;
cur = cur->next;
}
sum = 0;
cur = dum;
while (cur) {
sum += cur->val;
auto tmp = dic[sum]->next, pre{cur->next};
// while (pre != tmp) {
// auto t=pre->next;
// delete pre;
// pre = t;
// }
cur->next = tmp;
cur = cur->next;
}
return dum->next;
}
};
反转
206. 反转链表 - 力扣(LeetCode);剑指 Offer II 024. 反转链表 - 力扣(LeetCode); 迭代法: (常见)
class Solution {
public:
ListNode *reverseList(ListNode *head) {
ListNode *cur = head, *pre = nullptr;
while (cur) {
auto tmp = cur->next;
cur->next = pre;
pre = cur;
cur = tmp;
}
return pre;
}
};
递归写法: (可能会问)
class Solution {
public:
ListNode* reverseList(ListNode* head) {
if (!head || !head->next) return head;
ListNode* ans = reverseList(head->next);
head->next->next = head;
head->next = nullptr;
return ans;
}
};
反转打印
// 没什么技术含量的写法
class Solution {
public:
vector<int> reversePrint(ListNode* head) {
if (!head) return {};
auto cur(head);
vector<int> ans{};
while (cur) {
ans.emplace_back(cur->val);
cur = cur->next;
}
reverse(ans.begin(), ans.end());
return ans;
}
};
// 递归写法
class Solution {
public:
vector<int> reversePrint(ListNode* head) {
if (!head) return {};
auto cur(head);
vector<int> ans{};
function<void(ListNode*)> f = [&](ListNode* node) {
if (!node) return;
f(node->next);
ans.emplace_back(node->val);
};
f(head);
return ans;
}
};
// 栈模拟
class Solution {
public:
vector<int> reversePrint(ListNode* head) {
if (!head) return {};
auto cur(head);
vector<int> ans{};
stack<ListNode*> st;
for (auto cur(head); cur; cur = cur->next) st.emplace(cur);
for (; !st.empty(); st.pop()) ans.emplace_back(st.top()->val);
return ans;
}
};
旋转
// 直接模拟: 速度慢
class Solution {
public:
ListNode* rotateRight(ListNode* head, int k) {
if (k == 0 || !head) return head;
ListNode *mid{head}, *cur{head};
int len{};
while (cur && cur->next) ++len, cur = cur->next;
++len;
k %= len;
if (k == 0) return head;
int m = len - k;
while (--m) mid = mid->next;
ListNode* new_head = mid->next;
mid->next = nullptr;
cur->next = head;
return new_head;
}
};
// 闭合为环: 经典思路, 用到了取余性质, 事实就是运行时间的区别不大
class Solution {
public:
ListNode* rotateRight(ListNode* head, int k) {
if (!head || k == 0) return head;
auto cur{head};
int n{1};
while (cur->next) cur = cur->next, ++n;
k %= n;
if (k == 0) return head;
cur->next = head; // 连接成环
k = n - k;
while (--k) head = head->next; // 找断开的点
auto ans{head->next};
head->next = nullptr;
return ans;
}
};
找中间节点
快慢指针, 根据速度的不同分出来中间节点, 一次遍历即可.
class Solution {
public:
ListNode* middleNode(ListNode* head) {
if (!head || !head->next) return head;
auto fa = head, lo = head;
while (fa && fa->next) lo = lo->next, fa = fa->next->next;
return lo;
}
};
合并
class Solution {
public:
ListNode* mergeInBetween(ListNode* list1, int a, int b, ListNode* list2) {
auto l = list1;
int c = b - a + 2;
while (--a) l = l->next;
auto r = l;
while (c--) r = r->next;
l->next = list2;
auto cur = list2;
while (cur->next) cur = cur->next;
cur->next = r;
return list1;
}
};
21. 合并两个有序链表 - 力扣(LeetCode);剑指 Offer 25. 合并两个排序的链表 - 力扣(LeetCode);
class Solution {
public:
ListNode* mergeTwoLists(ListNode* list1, ListNode* list2) {
auto node = new ListNode, cur = node;
while (list1 && list2) {
if (list1->val < list2->val)
cur->next = list1, list1 = list1->next;
else
cur->next = list2, list2 = list2->next;
cur = cur->next;
}
cur->next = list1 ? list1 : list2;
return node->next;
}
};
分隔
86. 分隔链表 - 力扣(LeetCode);(类似合并排序链表, 可以用来做链表上的快排)
// 不使用额外空间的方法
class Solution {
public:
ListNode* partition(ListNode* head, int x) {
auto large = new ListNode, small = new ListNode;
auto cur1{large}, cur2{small};
while (head) {
if (head->val >= x)
cur1->next = head, cur1 = cur1->next;
else
cur2->next = head, cur2 = cur2->next;
head = head->next;
}
cur1->next = nullptr; // 断大的尾结点
cur2->next = large->next; // 小的尾结点指向大的头的next
auto ans = small->next;
delete small;
delete large;
return ans;
}
};
复制
138. 复制带随机指针的链表;剑指 Offer 35. 复杂链表的复制;
class Solution {
public:
Node* copyRandomList(Node* head) {
if (!head) return head;
// 连接复制出来的节点
for (auto cur(head); cur; cur = cur->next->next) {
auto tmp = new Node(cur->val);
tmp->next = cur->next;
cur->next = tmp;
}
// 添加上 random 节点
for (auto cur(head); cur; cur = cur->next->next)
cur->next->random = cur->random ? cur->random->next : nullptr;
// 拆分节点
auto ans = head->next;
for (auto cur(head); cur; cur = cur->next) {
auto tmp(cur->next);
cur->next = cur->next->next;
tmp->next = tmp->next ? tmp->next->next : nullptr;
}
return ans;
}
};
重排
class Solution {
public:
void reorderList(ListNode* head) {
vector<ListNode*> arr;
for (auto cur(head); cur; cur = cur->next) arr.emplace_back(cur);
int n = arr.size();
auto cur(head);
for (int r{n - 1}; r > n / 2; --r) {
auto tmp = arr[r];
tmp->next = cur->next;
cur->next = tmp;
cur = cur->next->next;
}
if (n & 1)
cur->next = nullptr;
else
cur->next->next = nullptr;
}
};
不要额外空间的方法:
环形链表
class Solution {
public:
bool hasCycle(ListNode *head) {
auto f = head, l = head;
while (1) {
if (!f || !f->next) return false;
f = f->next->next;
l = l->next;
if (f == l) return true;
}
}
};
$\bigstar$142. 环形链表 II;剑指 Offer II 022. 链表中环的入口节点;(经典算法, 需要找一个等价关系)
从相遇点到入环点的距离($c$)+$(n-1)$环周长等于头结点到入环点的长度.
class Solution {
public:
ListNode *detectCycle(ListNode *head) {
if (!head) return head;
auto f = head, s = head;
while (f) {
s = s->next;
if (!f->next) return nullptr; // 无环
f = f->next->next;
if (f == s) {
auto ptr = head;
while (ptr != s) ptr = ptr->next, s = s->next;
return ptr;
}
}
return nullptr;
}
};
综合(进阶)题目
排序
单向链表排序最快的算法是归并, 双向链表排序最快的算法是快速排序.
148. 排序链表; (这个是单链表上排序的最优解法: 归并排序)
相当于两个题的组合:
当然还有快排: (太复杂了, 直接拿来主义, 过这道题会超时)
数字链表相加
class Solution {
public:
ListNode* addTwoNumbers(ListNode* l1, ListNode* l2) {
ListNode *dum = new ListNode(0), *cur = dum;
int carry{}, num{};
while (l1 || l2 || carry) {
int n1{}, n2{};
if (l1)
n1 = l1->val, l1 = l1->next;
if (l2)
n2 = l2->val, l2 = l2->next;
num = n1 + n2 + carry;
cur->next = new ListNode(num % 10);
cur = cur->next;
carry = num / 10;
}
return dum->next;
}
};
class Solution {
public:
ListNode* addTwoNumbers(ListNode* l1, ListNode* l2) {
auto reverse = [](ListNode* l) {
ListNode *cur = l, *pre = nullptr;
while (cur) {
auto tmp = cur->next;
cur->next = pre;
pre = cur;
cur = tmp;
}
return pre;
};
l1 = reverse(l1);
l2 = reverse(l2);
auto dum = new ListNode;
for (int n1, n2, num, carry{}; l1 || l2 || carry;) {
n1 = n2 = 0;
if (l1)
n1 = l1->val, l1 = l1->next;
if (l2)
n2 = l2->val, l2 = l2->next;
num = n1 + n2 + carry;
auto tmp = new ListNode(num % 10, dum->next);
dum->next = tmp;
carry = num / 10;
}
return dum->next;
}
};
更好的方法, 不需要翻转:
class Solution {
public:
ListNode* addTwoNumbers(ListNode* l1, ListNode* l2) {
stack<int, list<int>> s1, s2; // list in container
for (; l1; l1 = l1->next)
s1.emplace(l1->val);
for (; l2; l2 = l2->next)
s2.emplace(l2->val);
auto dum = new ListNode;
for (int n1, n2, num, carry{}; !s1.empty() || !s2.empty() || carry;) {
n1 = n2 = 0;
if (!s1.empty())
n1 = s1.top(), s1.pop();
if (!s2.empty())
n2 = s2.top(), s2.pop();
num = n1 + n2 + carry;
dum->next = new ListNode(num % 10, dum->next);
carry = num / 10;
}
return dum->next;
}
};
回文链表
234. 回文链表 - 力扣(LeetCode);剑指 Offer II 027. 回文链表 - 力扣(LeetCode);面试题 02.06. 回文链表 - 力扣(LeetCode);
直接存数组模拟可以做, $O(1)$空间有一定难度, 下面主要说$O(1)$做法, 综合了上述的很多基本操作.
- 找到中节点(奇数长度是中节点, 偶数长度是中间靠右的结点)
- 翻转中节点开始往后的后部分链表
- 对比两个部分, 判断回文情况
class Solution {
public:
bool isPalindrome(ListNode* head) {
auto mid = find_mid(head), nhead = reverse(mid);
while (nhead) {
if (head->val != nhead->val) return false;
head = head->next;
nhead = nhead->next;
}
return true;
}
ListNode* reverse(ListNode* head) {
ListNode *cur = head, *pre = nullptr;
while (cur) {
auto tmp = cur->next;
cur->next = pre;
pre = cur;
cur = tmp;
}
return pre;
}
ListNode* find_mid(ListNode* head) {
ListNode *fast = head, *low = head, *tmp{};
while (fast && fast->next) {
fast = fast->next->next;
low = low->next;
}
return low;
}
};
合并排序链表
23. 合并K个升序链表 - 力扣(LeetCode);(分治, 或者优先队列, 优先队列方便)
class Solution {
public:
ListNode* mergeKLists(vector<ListNode*>& lists) {
if (lists.empty()) return {};
auto cmp = [](const auto& lhs, const auto& rhs) {
return lhs->val > rhs->val;
};
priority_queue<ListNode*, vector<ListNode*>, decltype(cmp)> pq(cmp);
// int k = lists.size();
auto dum = new ListNode, cur = dum;
for (auto node : lists)
if (node)
pq.emplace(node);
for (; !pq.empty(); pq.pop(), cur = cur->next) {
auto tmp = pq.top();
if (tmp)
cur->next = tmp;
if (tmp->next)
pq.emplace(tmp->next);
}
return dum->next;
}
};
反转链表
指定区间
92. 反转链表 II - 力扣(LeetCode);(用到三个指针的穿针引线法, 画个图就出来了)
class Solution {
public:
ListNode* reverseBetween(ListNode* head, int left, int right) {
auto dummy = new ListNode(0, head), pre = dummy;
for (int i{}; i < left - 1; ++i) pre = pre->next; // 左端指针
auto cur = pre->next, post = cur;
int cnt = right - left;
for (int i{}; i < cnt; ++i) post = post->next;
while (cnt--) {
pre->next = cur->next;
auto tmp = post->next;
post->next = cur;
cur->next = tmp;
cur = pre->next;
}
auto ans = dummy->next;
delete dummy;
return ans;
}
};
指定组
class Solution {
public:
ListNode* reverseKGroup(ListNode* head, int k) {
int n{};
for (auto cur{head}; cur; cur = cur->next) ++n;
auto dum = new ListNode(0, head), pp = dum, cur = head;
ListNode* pre{};
for (; n >= k; n -= k) {
for (int i{}; i < k; ++i) { // reverse
auto tmp = cur->next;
cur->next = pre;
pre = cur;
cur = tmp;
}
// link
auto x = pp->next;
pp->next->next = cur;
pp->next = pre;
pp = x;
}
return dum->next;
}
};