========快慢指针=========
Leetcode 876. 链表的中间结点
结论:
代码:
class Solution {
public:
ListNode* middleNode(ListNode* head) {
ListNode* fast =head, *slow = head;
while (fast != nullptr && fast -> next != nullptr){
slow = slow -> next;
fast = fast -> next -> next;
}
return slow;
}
};
Leetcode 141. 环形链表
在上面一题的基础上,加上一个slow和fast的判断即可。
class Solution {
public:
bool hasCycle(ListNode *head) {
ListNode* fast = head, *slow = head;
while (fast != nullptr && fast -> next != nullptr){
slow = slow -> next;
fast = fast -> next -> next;
if(slow == fast) return true;
}
return false;
}
};
Leetcode 142. 环形链表 II
本题需要返回进入环形链表的第一个节点:(且要求O(1)的空间复杂度)
class Solution {
public:
ListNode *detectCycle(ListNode *head) {
ListNode* fast = head, *slow = head;
while (fast && fast -> next){
slow = slow -> next;
fast = fast -> next -> next;
if(slow == fast){
while (slow != head){
slow = slow -> next;
head = head -> next;
}
return slow;
}
}
return nullptr;
}
};
上面在进行数学的推导的时候,假设了在慢指针与快指针相遇时,慢指针还没有完成环形一圈的路程,下面给出证明:
Leetcode 143. 重排链表
组合题(快慢指针+翻转链表):只要先用876题确定出中间结点,然后对中间结点后面的所有节点进行206题的翻转链表操作,然后迭代构建出答案链表即可。
实现代码:
class Solution {
public:
void reorderList(ListNode* head) {
// step1: 快慢指针找到中间结点
ListNode* fast = head, *slow = head;
while(fast && fast -> next){
slow = slow -> next;
fast = fast -> next -> next;
} // 退出循环的slow指针就是中间节点的指向
// step2: 完成后半部分链表的翻转
ListNode* pre = nullptr, * cur = slow, * nxt = cur;
while (cur != nullptr){
nxt = nxt -> next;
cur -> next = pre;
pre = cur, cur = nxt;
} // 后半部分链表翻转后, 头结点为pre, 而cur == nullptr
// step3: 完成链表的重排
ListNode* headnext = head -> next, * h2 = pre, * h2next = pre -> next;
while (h2next != nullptr){ // 注意是h2的next为空的时候,退出循环(模拟一下就可以知道,)
head -> next = h2;
h2 -> next = headnext;
// 移动指针, 迭代子问题
head = headnext;
h2 = h2next;
headnext = head -> next;
h2next = h2next -> next;
}
}
};
这里在组合重排答案链表的时候,注意终止条件是h2next == nullptr。模拟一下即可,奇数结点headnxt==nullptr 或h2next == nullptr都可以,但是偶数链表当完成组合后,只能h2next == nullptr退出循环。
Leetcode 160. 相交链表
解题思路:
代码实现:时间复杂度O(n+m) 空间复杂度O(1)
我的实现方式
class Solution {
public:
ListNode *getIntersectionNode(ListNode *headA, ListNode *headB) {
ListNode* pa = headA, *pb = headB;
bool f1 = false, f2 = false;
while(pa != nullptr && pb != nullptr){
if(pa == pb) return pa; // 必须放在前面,否则当两个链表第一个节点就是相交结点的话,则会被跳过
pa = pa -> next;
pb = pb -> next;
if(pa == nullptr && !f1) pa = headB, f1 = true;
if(pb == nullptr && !f2) pb = headA, f2 = true;
}
return nullptr;
}
};
更加优雅的方式:(为什么不会死循环,因为A和B在交换链表遍历之后,一定会在同一时刻(不相交的话)变成nullptr)(即同时走了a+b长度的路)
class Solution {
public:
ListNode *getIntersectionNode(ListNode *headA, ListNode *headB) {
ListNode *A = headA, *B = headB;
while (A != B) {
A = A != nullptr ? A->next : headB;
B = B != nullptr ? B->next : headA;
}
return A;
}
};
========反转链表=========
Leetcode 206. 反转链表(板子)
class Solution {
public:
ListNode* reverseList(ListNode* head) {
ListNode* pre = nullptr, * cur = head, *nxt = head;
while (nxt != nullptr){
nxt = nxt -> next;
cur -> next = pre;
pre = cur, cur = nxt;
}
return pre;
}
};
Leetcode 92. 反转链表 II
利用规律:在反转结束后,从原链表上看,pre指向这一段的末尾(即反转后链表的头),cur指向这一段的下一个节点。
思路:
蓝色线是先按照206的解法翻转链表,粉线是接下来与整个链表对接的操作。
为了统一操作(left = 1的情况),设置一个头结点。
class Solution {
public:
ListNode* reverseBetween(ListNode* head, int left, int right) {
ListNode* dummy = new ListNode(0, head); // 初始化虚拟头结点(统一left=1的情况)
ListNode* p0 = dummy, *pre = nullptr, * cur, * nxt;
// 找出p0
for(int i = 0; i < left - 1; i ++) p0 = p0 -> next;
cur = p0 -> next, nxt = cur;
// 开始翻转(那一部分(子链表),最后再进行与整体链表的对接)
for(int i = 0; i < right - left + 1; i ++){ // 子链表花费right - left + 1次翻转
nxt = nxt -> next;
cur -> next = pre;
pre = cur, cur = nxt;
}
// 翻转完毕, 此时pre是当前子链表的'head', cur指向大链表中子链表的后一个结点(拼接)
p0 -> next -> next = cur;
p0 -> next = pre;
return dummy -> next; // dummy的next结点才是真正的头结点
}
};
Leetcode 25. K 个一组翻转链表
思路与leetcode 92相同,92题是本问题的子问题,组合一下就好。
class Solution {
public:
ListNode* reverseKGroup(ListNode* head, int k) {
int n = 0; // 先统计一下链表中结点的个数
for(ListNode* p = head; p != nullptr; p = p -> next) n ++;
int cnt = n / k; // 整除-->代表翻转的次数
ListNode* dummy = new ListNode(0, head);
ListNode* p0 = dummy;
while (cnt --){
ListNode* pre = nullptr, *cur = p0 -> next, * nxt = cur;
for(int i = 0; i < k; i ++){ // 一共k结点,操作k次
nxt = nxt -> next;
cur -> next = pre;
pre = cur, cur = nxt;
}
cout << p0 -> val << endl;
p0 -> next -> next = cur; // cur此时是原翻转链表后面的一个结点
p0 -> next = pre; // pre是翻转的那部分子链表的头部
// 更新p0值
for(int i = 0; i < k; i ++) p0 = p0 -> next;
}
return dummy -> next;
}
};
Leetcode 234. 回文链表
方法一:利用O(n)的空间,额外存储一下链表的值,利用数组的随机访问特性即可。(略)
方法二:递归(空间仍然是O(n))
使用本方法前,可以先看下这个递归实现的“逆序打印链表的值”的伪代码:
function print_values_in_reverse(ListNode head) if head is NOT null print_values_in_reverse(head.next) print head.val
利用递归函数一直拿到链表的最后一个元素,与front指针指向元素比较。然后front往前走,递归出栈相当于右指针往前走。
class Solution {
public:
bool isPalindrome(ListNode* head) {
ListNode* front = head;
auto f = [&](auto&& f, ListNode* cur) -> bool{
if(cur != nullptr){
if(!f(f, cur -> next)) return false;
if(cur -> val != front -> val) return false;
front = front -> next;
}
return true; // 递归终止的base case
};
return f(f, head);
}
};
方法三:快慢指针/翻转后半部分链表
实现代码:
class Solution {
public:
bool isPalindrome(ListNode* head) {
int n = 0;
ListNode* p = head;
while (p != nullptr) n ++, p = p -> next;
p = head;
for(int i = 0; i < (n / 2); i ++) p = p -> next;
if (n % 2) p = p -> next; // 找到下半段链表的第一个结点
// 翻转后半部分链表
ListNode* pre = nullptr, * cur = p, * nxt = cur;
while (cur != nullptr){
nxt = nxt -> next;
cur -> next = pre;
pre = cur, cur = nxt;
}
// 开始比较
ListNode* i = head, * j = pre;
while (i != nullptr && j != nullptr){
if(i -> val != j -> val) return false;
i = i -> next, j = j -> next;
}
// 恢复成原来的链表
p = head; // 找到上半段链表的最后一个结点
for(int i = 0; i < (n % 2 == 0? (n / 2 - 1): n / 2); i ++) p = p -> next;
ListNode* p1 = nullptr, * p2 = pre, * p3 = p2;
while (cur != nullptr){
p3 = p3 -> next;
p2 -> next = p1;
p1 = p2, p2 = p3;
}
p -> next = p1;
return true;
}
};
========删除=========
Leetcode 237. 删除链表中的节点
(脑筋急转弯): 注意题目要求:我们只需要让链表在删除前后「看起来」是一样的就行。
比如示例 1,我们可以把第二个节点的值替换成第三个节点的值,然后删除第三个节点,「看起来」删除前后就是一样的。具体来说,先把 node.val 更新成 node.next.val,然后删除 node 的下一个节点,把 node.next 更新成 node.next.next。
class Solution { public: void deleteNode(ListNode* node) { int val = node -> next -> val; ListNode* p = node -> next; node -> val = val; node -> next = node -> next -> next; delete p, p = nullptr; } };
Leetcode 19. 删除链表的倒数第 N 个结点
方法一:遍历链表得到总的结点数目,得到正数的位置删除即可
方法二:巧妙设置l和r指针,r先走n步,然后l和r同时走直到r走到最后一个结点,那么l的位置就是倒数第n + 1个结点。
class Solution {
public:
ListNode* removeNthFromEnd(ListNode* head, int n) {
ListNode* dummy = new ListNode(0, head);
ListNode* left = dummy, * right = dummy, *p;
for(int i = 0; i < n; i ++) right = right -> next;
while(right -> next){
left = left -> next;
right = right -> next;
} // right和left始终差n个位置,right目前在最后一个结点,那么left就在倒数第n+1个结点上
p = left -> next;
left -> next = left -> next -> next;
delete p, p = nullptr;
return dummy -> next;
}
};
========排序链表=========
Leetcode 148. 排序链表
下面是“链表的归并排序”,包含子函数①寻找中间结点 ②合并两个有序链表
class Solution {
public:
// func1: 快慢指针寻找中间结点(当偶数节点的时候,返回中位节点的靠前结点,注意fast的写法区别即可)
ListNode* findmid(ListNode* head){
if(!head) return head;
ListNode* fast = head -> next, *slow = head;
while(fast && fast -> next){
fast = fast -> next -> next;
slow = slow -> next;
}
return slow; // 当偶数节点的时候,返回中位节点的靠前结点
}
// func2: 合并两个有序链表
ListNode* merge(ListNode* list1, ListNode* list2){
ListNode* dummy = new ListNode(0); // 虚节点
ListNode* p1 = list1, * p2 = list2, * q = dummy;
while(p1 && p2){
int v1 = p1 -> val, v2 = p2 -> val;
if(v1 < v2) q -> next = p1, p1 = p1 -> next;
else q -> next = p2, p2 = p2 -> next;
q = q -> next;
}
// 扫尾
while(p1) q -> next = p1, p1 = p1 -> next, q = q -> next;
while(p2) q -> next = p2, p2 = p2 -> next, q = q -> next;
return dummy -> next;
}
// func3:主函数
ListNode* sortList(ListNode* head) {
if(head == nullptr || head -> next == nullptr) return head;
ListNode* midNode = findmid(head), * j = midNode -> next;
midNode -> next = nullptr; // 从中间断开链表
ListNode* left = sortList(head); // 排左边的
ListNode* right = sortList(j); // 排右边的
// 排完序后进行merge
return merge(left, right);
}
};
========其他=========
Leetcode 21. 合并两个有序链表
方法一:迭代实现ez
class Solution {
public:
// 迭代实现: 时间O(n) 空间O(1)
ListNode* mergeTwoLists(ListNode* list1, ListNode* list2) {
if(list1 == nullptr) return list2;
if(list2 == nullptr) return list1;
ListNode* dummy = new ListNode();
ListNode* p1 = list1, *p2 = list2, *p = dummy;
while (p1 && p2){
int v1 = p1 -> val, v2 = p2 -> val;
if(v1 < v2) p -> next = p1, p1 = p1 -> next;
else p -> next = p2, p2 = p2 -> next;
p = p -> next;
}
if(p1 == nullptr) p -> next = p2;
else p -> next = p1;
return dummy -> next;
}
};
方法二:递归实现
class Solution {
public:
ListNode* mergeTwoLists(ListNode* list1, ListNode* list2) {
if(list1 == nullptr) return list2;
else if(list2 == nullptr) return list1;
else if(list1 -> val < list2 -> val){
list1 -> next = mergeTwoLists(list1 -> next, list2);
return list1;
}else{
list2 -> next = mergeTwoLists(list1, list2 -> next);
return list2;
}
}
};
Leetcode 23. 合并 K 个升序链表
纯暴力做法,在21题的思路基础上暴力找下一个可能结点。O(n²)
class Solution {
public:
ListNode* mergeKLists(vector<ListNode*>& lists) {
int n = lists.size();
ListNode* dummy = new ListNode(0), * p = dummy;
while(1){
bool bkfg = true;
int minnum = INT_MIN, minidx = -1;
for(int i = 0; i < n; i ++){
if(lists[i] != nullptr){
bkfg = false;
if(minidx == -1 || minnum > lists[i] -> val)
minidx = i, minnum = lists[i] -> val;
}
}
if(bkfg) break;
p -> next = lists[minidx], lists[minidx] = lists[minidx] -> next, p = p -> next;
}
return dummy -> next;
}
};
优化方法一:最小堆
class Solution {
public:
ListNode* mergeKLists(vector<ListNode*>& lists) {
auto cmp = [](const ListNode* a, const ListNode* b){
return a -> val > b -> val; // 最小堆
};
priority_queue<ListNode*, vector<ListNode*>, decltype(cmp)> pq;
for(auto& head: lists)
if(head) pq.push(head);
ListNode* dummy = new ListNode(0), *cur = dummy; // 虚拟头结点
while (!pq.empty()){ // 循环至heap为空
auto node = pq.top(); // 剩余节点中最小的节点
pq.pop();
if(node -> next) pq.push(node -> next);
cur -> next = node; // 合并到新链表中
cur = cur -> next;
}
return dummy -> next;
}
};
优化方法二:分治
class Solution {
public:
// func: 合并两个有序链表
ListNode* MergeTwo(ListNode* list1, ListNode* list2){
ListNode* dummy = new ListNode(0),* cur = dummy;
ListNode* p1 = list1, * p2 = list2;
while (p1 && p2){
int v1 = p1 -> val, v2 = p2 -> val;
if(v1 < v2) cur -> next = p1, p1 = p1 -> next;
else cur -> next = p2, p2 = p2 -> next;
cur = cur -> next;
}
// 扫尾
if (p1) cur -> next = p1;
if (p2) cur -> next = p2;
return dummy -> next;
}
// func: 合并从 list[i]到lists[j - 1]的链表
ListNode* subMerge(vector<ListNode*>& lists, int i, int j){
int m = j - i; // 计算需要合并的链表数目
if(m == 0) return nullptr; // 输入lists为空
if(m == 1) return lists[i]; // 无需合并, 直接返回
auto left = subMerge(lists, i, i + m / 2); // 合并左半部分
auto right = subMerge(lists, i + m / 2, j); // 合并右半部分
return MergeTwo(left, right);
}
ListNode* mergeKLists(vector<ListNode*>& lists) {
return subMerge(lists, 0, lists.size());
}
};
Leetcode 2. 两数相加
class Solution {
public: // 本题直接变成int/longlong会爆整数范围, 应该直接考虑每一位相加(模拟加法)
ListNode* addTwoNumbers(ListNode* l1, ListNode* l2) {
ListNode* dummy = new ListNode(0), *p1 = l1, *p2 = l2, *p3 = dummy;
int fg = 0; // 进位标识符
while(p1 != nullptr || p2 != nullptr){
// cout << 6 << endl;
int a, b;
if(p1 && p2)
a = p1 -> val, b = p2 -> val;
else if(p1 == nullptr) a = 0, b = p2 -> val;
else if(p2 == nullptr) a = p1 -> val, b = 0; // 别忘记给另一个变量赋值
int c = a + b + fg;
int v = c % 10;
fg = c / 10;
ListNode* tmp = new ListNode(v);
// cout << a << " " << b << " " << fg << endl;
p3 -> next = tmp;
p3 = p3 -> next;
if(p1) p1 = p1 -> next;
if(p2) p2 = p2 -> next;
}
if(fg){ // 处理最后的进位
ListNode* t = new ListNode(1);
p3 -> next = t;
}
return dummy -> next;
}
};
========综合应用=========
Leetcode 146. LRU 缓存
class Node{
public:
int key, value;
Node* prev, * next;
Node(int k = 0, int v = 0): key(k), value(v){}
};
class LRUCache {
private:
int capacity;
Node* dummy; // 虚拟结点
unordered_map<int, Node*> key_to_node;
// 构建双向链表
// func1: 删除一个结点(抽出一本书)
void remove(Node* x){
x -> prev -> next = x -> next;
x -> next -> prev = x -> prev;
}
// func2:在链表头添加一个结点(把一本书放在最上面)
void push_front(Node* x){
x -> prev = dummy;
x -> next = dummy -> next;
x -> prev -> next = x;
x -> next -> prev = x;
}
// func3:
Node* get_node(int key){
auto it = key_to_node.find(key);
if(it == key_to_node.end())
return nullptr; // 没有这本书
auto node = it -> second; // 有
remove(node); push_front(node); // 抽出来并放在最上面
return node;
}
public:
LRUCache(int capacity): capacity(capacity), dummy(new Node()) {
dummy -> prev = dummy;
dummy -> next = dummy;
}
int get(int key) {
auto node = get_node(key);
return node? node -> value: -1;
}
void put(int key, int value) {
auto node = get_node(key);
if(node){ // 有这本书(在getnode里面已经将书本放在最上面了)
node -> value = value; // 更新值
return;
}
key_to_node[key] = node = new Node(key, value); // 新书
push_front(node); // 放在最上面
if (key_to_node.size() > capacity){ // 书太多了,该LRU了
auto back_node = dummy -> prev;
key_to_node.erase(back_node -> key);
remove(back_node); // 去掉最后一本书
delete back_node; // 释放内存
}
}
};