2.19~2.22 -链表
206. 反转链表 - 力扣(LeetCode)
经典问题,经典方法:
- 递归法
- 头插法
//递归
class Solution {
public:
ListNode* reverseList(ListNode* head) {
if(!head || !head->next) return head;
ListNode* last = reverseList(head->next);
head->next->next = head;
head->next = NULL;
return last;
}
};
//头插
class Solution {
public:
ListNode* reverseList(ListNode* head) {
ListNode* pre = NULL;
ListNode* cur = head;
while(cur) {
ListNode* nxt = cur->next;
cur->next = pre;
pre = cur;
cur = nxt;
}
return pre;
}
};
//复杂度分析
//时间复杂度:O(n),其中n为链表节点个数。
//空间复杂度:O(1),仅用到若干额外变量。
234. 回文链表 - 力扣(LeetCode)
找链表中间节点+翻转链表
⚠注意:第一张图中的2→3,在反转链表后,并不会断开。第一张图反转链表后,我们得到了两条链表,一条是1→2→3,另一条是5→4→3,所以不存在第一条链表长度比第二条小1的情况。
class Solution {
// 876. 链表的中间结点
ListNode* middleNode(ListNode* head) {
ListNode* slow = head, *fast = head;
while (fast && fast->next) {
slow = slow->next;
fast = fast->next->next;
}
return slow;
}
// 206. 反转链表
ListNode* reverseList(ListNode* head) {
ListNode* pre = nullptr, *cur = head;
while (cur) {
ListNode* nxt = cur->next;
cur->next = pre;
pre = cur;
cur = nxt;
}
return pre;
}
public:
bool isPalindrome(ListNode* head) {
ListNode* mid = middleNode(head);
ListNode* head2 = reverseList(mid);
while (head2) {
if (head->val != head2->val) { // 不是回文链表
return false;
}
head = head->next;
head2 = head2->next;
}
return true;
}
};
142. 环形链表 II - 力扣(LeetCode)
- 检测有无环
- 有,递推公式得到2(x + y) = x + y + z + y --> x = z,让cur节点从头出发,slow节点从当前位置出发,相遇点即环入口。
- 无,返回NULL
class Solution {
public:
ListNode* detectCycle(ListNode* head) {
ListNode* slow = head;
ListNode* fast = head;
while (fast && fast->next) {
slow = slow->next;
fast = fast->next->next;
if (fast == slow) {
while (slow != head) {
slow = slow->next;
head = head->next;
}
return slow;
}
}
return nullptr;
}
};
21. 合并两个有序链表 - 力扣(LeetCode)
- 迭代:
- 递归:
//迭代
class Solution {
public:
ListNode* mergeTwoLists(ListNode* list1, ListNode* list2) {
ListNode dummy{}; // 用哨兵节点简化代码逻辑
auto cur = &dummy; // cur 指向新链表的末尾
while (list1 && list2) {
if (list1->val < list2->val) {
cur->next = list1; // 把 list1 加到新链表中
list1 = list1->next;
} else { // 注:相等的情况加哪个节点都是可以的
cur->next = list2; // 把 list2 加到新链表中
list2 = list2->next;
}
cur = cur->next;
}
cur->next = list1 ? list1 : list2; // 拼接剩余链表
return dummy.next;
}
};
//递归
class Solution {
public:
ListNode* mergeTwoLists(ListNode* list1, ListNode* list2) {
if (list1 == nullptr) return list2; // 注:如果都为空则返回空
if (list2 == nullptr) return list1;
if (list1->val < list2->val) {
list1->next = mergeTwoLists(list1->next, list2);
return list1;
}
list2->next = mergeTwoLists(list1, list2->next);
return list2;
}
};
2. 两数相加 - 力扣(LeetCode)
- 递归法
每次把两个节点值
l1 .val,l2 .val与进位值carry相加,除以10的余数即为当前节点需要保存的数位,除以10的商即为新的进位值。代码实现时,有一个简化代码的小技巧:如果递归中发现l 2 的长度比l 1 更长,那么可以交换l 1 和l 2 ,保证l 1 不是空节点,从而简化代码逻辑。
class Solution {
public:
// l1 和 l2 为当前遍历的节点,carry 为进位
ListNode* addTwoNumbers(ListNode* l1, ListNode* l2, int carry = 0) {
if (l1 == nullptr && l2 == nullptr) { // 递归边界:l1 和 l2 都是空节点
return carry ? new ListNode(carry) : nullptr; // 如果进位了,就额外创建一个节点
}
if (l1 == nullptr) { // 如果 l1 是空的,那么此时 l2 一定不是空节点
swap(l1, l2); // 交换 l1 与 l2,保证 l1 非空,从而简化代码
}
int sum = carry + l1->val + (l2 ? l2->val : 0); // 节点值和进位加在一起
l1->val = sum % 10; // 每个节点保存一个数位(直接修改原链表)
l1->next = addTwoNumbers(l1->next, (l2 ? l2->next : nullptr), sum / 10); // 进位
return l1;
}
};
迭代的思路是,初始化答案为一个「空链表」,每次循环,向该链表末尾添加一个节点(保存一个数位)。循环即遍历链表 l1 和 l2 ,每次把两个节点值
l1 .val, l2 .val与进位值carry相加,除以10的余数即为当前节点需要保存的数位,除以10的商即为新的进位值。需要注意的是,在第一次循环时,我们无法往一个空节点的末尾添加节点。这里的技巧是,创建一个哨兵节点(dummy node),当成初始的「空链表」。循环结束后,哨兵节点的下一个节点就是最终要返回的链表头节点。
class Solution {
public:
ListNode* addTwoNumbers(ListNode* l1, ListNode* l2) {
ListNode dummy; // 哨兵节点
ListNode* cur = &dummy;
int carry = 0; // 进位
while (l1 || l2 || carry) { // 有一个不是空节点,或者还有进位,就继续迭代
if (l1) {
carry += l1->val; // 节点值和进位加在一起
l1 = l1->next; // 下一个节点
}
if (l2) {
carry += l2->val; // 节点值和进位加在一起
l2 = l2->next; // 下一个节点
}
cur = cur->next = new ListNode(carry % 10); // 每个节点保存一个数位
carry /= 10; // 新的进位
}
return dummy.next; // 哨兵节点的下一个节点就是头节点
}
};
445. 两数相加 II - 力扣(LeetCode)
在上一题的基础上改成,链表从高位到低位保存数字
思路:链表翻转+两数相加
class Solution {
ListNode* reverseList(ListNode* head) {//迭代法翻转
ListNode* pre = nullptr, *cur = head;
while (cur) {
ListNode* nxt = cur->next;
cur->next = pre;
pre = cur;
cur = nxt;
}
return pre;
}
ListNode* addTwo(ListNode* l1, ListNode* l2) {//迭代法相加
ListNode dummy; // 哨兵节点
auto cur = &dummy;
int carry = 0; // 进位
while (l1 || l2 || carry) { // 有一个不是空节点,或者还有进位,就继续迭代
if (l1) carry += l1->val; // 节点值和进位加在一起
if (l2) carry += l2->val; // 节点值和进位加在一起
cur = cur->next = new ListNode(carry % 10); // 每个节点保存一个数位
carry /= 10; // 新的进位
if (l1) l1 = l1->next; // 下一个节点
if (l2) l2 = l2->next; // 下一个节点
}
return dummy.next; // 哨兵节点的下一个节点就是头节点
}
public:
ListNode* addTwoNumbers(ListNode* l1, ListNode* l2) {
l1 = reverseList(l1);
l2 = reverseList(l2);
auto l3 = addTwo(l1, l2);
return reverseList(l3);
}
};
19. 删除链表的倒数第 N 个结点 - 力扣(LeetCode)
快慢指针思路,先让fast走n步,再让fast走到底,此时slow下一个就是待删节点,令slow->next = slow->next->next。
注意点:在先让fast走n步时,可能走到空节点(如果n = list.size(),此时删除头结点),因此定义虚拟头结点dummy,最后返回的也是dummy->next
class Solution {
public:
ListNode* removeNthFromEnd(ListNode* head, int n) {
// 由于可能会删除链表头部,用哨兵节点简化代码
ListNode dummy{0, head};
ListNode* left = &dummy;
ListNode* right = &dummy;
while (n--) {
right = right->next; // 右指针先向右走 n 步
}
while (right->next) {
left = left->next;
right = right->next; // 左右指针一起走
}
// 左指针的下一个节点就是倒数第 n 个节点
ListNode* nxt = left->next;
left->next = left->next->next;
delete nxt;//注意回收空间
return dummy.next;
}
};
24. 两两交换链表中的节点 - 力扣(LeetCode)
class Solution {
public:
ListNode* swapPairs(ListNode* head) {
ListNode dummy(0, head); // 用哨兵节点简化代码逻辑
ListNode* node0 = &dummy;
ListNode* node1 = head;
while (node1 && node1->next) { // 至少有两个节点
ListNode* node2 = node1->next;
ListNode* node3 = node2->next;
node0->next = node2; // 0 -> 2
node2->next = node1; // 2 -> 1
node1->next = node3; // 1 -> 3
node0 = node1; // 下一轮交换,0 是 1
node1 = node3; // 下一轮交换,1 是 3
}
return dummy.next; // 返回新链表的头节点
}
};
92. 反转链表 II - 力扣(LeetCode)
由于left可能等于头结点,因此需要定义dummyhead
class Solution {
public:
ListNode* reverseBetween(ListNode* head, int left, int right) {
ListNode* dummyHead = new ListNode(0,head);
ListNode* p0 = dummyHead;
//p0从dummyHead走l - 1步来到待翻转部分的前一个节点
for (int i = 0; i < left - 1; i++) {
p0 = p0->next;
}
ListNode* pre = nullptr;
ListNode* cur = p0->next;
//翻转r - l 次,最后pre停在反转后的头结点上,cur停在下一个节点。
for (int i = 0; i < right - left + 1; i++) {
ListNode* nxt = cur->next;
cur->next = pre;
pre = cur;
cur = nxt;
}
//此时已经l->r翻转完毕,l->next = nullptr但是p0->next并没有断
p0->next->next = cur;
p0->next = pre;
return dummyHead->next;
}
};
25. K 个一组翻转链表 - 力扣(LeetCode)
class Solution {
public:
ListNode* reverseKGroup(ListNode* head, int k) {
// 统计节点个数
int n = 0;
for (ListNode* cur = head; cur; cur = cur->next) {
n++;
}
ListNode dummy(0, head);
ListNode* p0 = &dummy;
ListNode* pre = nullptr;
ListNode* cur = head;
// k 个一组处理
for (; n >= k; n -= k) {
for (int i = 0; i < k; i++) { // 同 92 题
ListNode* nxt = cur->next;
cur->next = pre; // 每次循环只修改一个 next,方便大家理解
pre = cur;
cur = nxt;
}
// 见视频
ListNode* nxt = p0->next;//翻转后的末尾
p0->next->next = cur;
p0->next = pre;//p0接到翻转后的头
p0 = nxt;//翻转后的末尾是下一个循环的前驱结点
}
return dummy.next;
}
};
前置题:328. 奇偶链表 - 力扣(LeetCode)
- 如果是空链表,evenList的初始节点就不存在,因此特殊处理,直接返回;
- 初始化奇数索引节点链表oddList初始化为头节点,偶数索引节点链表evenList初始化为次节点;
- 记录偶数索引节点链表头节点evenList,用于之后的拼接;【奇数索引节点的头节点就是head,不需要在额外记录了】
- 更新奇偶索引链表的节点;
- 最后一个奇数索引节点和首个偶数索引节点拼接起来;
返回头节点head;
class Solution {
public:
ListNode* oddEvenList(ListNode* head) {
if(!head)return head; // 空链表,直接返回
ListNode* odd = head, *even = head->next; // 奇数索引节点链表odd初始化为头节点,偶数索引节点链表even初始化为次节点
ListNode* evenhead = even; // 记录偶数索引节点链表头节点
// 当前偶数节点不为空且当前偶数节点之后还有节点
while(even && even->next){
odd->next = even->next; // 下一个奇数索引节点是当前偶数索引节点的下一个节点
odd = odd->next; // 更新当前奇数索引节点
even->next = odd->next; // 下一个偶数索引节点是新的奇数索引节点的下一个节点
even = even->next; // 更新偶数索引节点
}
odd->next = evenhead; // 最后一个奇数索引节点和首个偶数索引节点拼接起来
return head;
}
};
138. 随机链表的复制 - 力扣(LeetCode)
题意
深拷贝一个链表,要求新链表中的每个节点都是新创建的,并且这些节点的random指针都指向新链表中的相应节点。
思路
如果没有
random指针,只需在遍历链表的同时,依次复制每个节点(创建新节点并复制val),添加在新链表的末尾。有
random指针,问题就变得复杂了,我们需要知道random指向的那个节点,在新链表中是哪个节点。所以必须记录原链表节点到新链表节点的映射(map)。这样可以通过原链表
random指向的节点,知道新链表的random应该指向哪个节点。难道要用哈希表吗?不需要,我们可以把新链表和旧链表「混在一起」。
例如链表1→2→3,依次复制每个节点(创建新节点并复制val和next),把新节点直接插到原节点的后面,形成一个交错链表:1→1 ′ →2→2 ′ →3→3 ′
如此一来,原链表节点的下一个节点,就是其对应的新链表节点了!
然后遍历这个交错链表,假如节点1的
random指向节点3,那么就把节点1 ′ 的random指向节点3的下一个节点3 ′ ,这样就完成了对random指针的复制。cur->next->random = cur->random->next;最后,从交错链表链表中分离出1 ′ →2 ′ →3 ′ ,即为深拷贝后的链表。做法类似328. 奇偶链表。
⚠注意:不能只删除节点1,2,3,因为题目要求原链表的next不能修改。(但 Python 可以这么做)
class Solution {
public:
Node* copyRandomList(Node* head) {
if (head == nullptr) {
return nullptr;
}
// 复制每个节点,把新节点直接插到原节点的后面
for (Node* cur = head; cur; cur = cur->next->next) {
cur->next = new Node(cur->val, cur->next, nullptr);
}
// 遍历交错链表中的原链表节点
for (Node* cur = head; cur; cur = cur->next->next) {
if (cur->random) {
// 要复制的 random 是 cur->random 的下一个节点
cur->next->random = cur->random->next;
}
}
// 把交错链表分离成两个链表
Node* new_head = head->next;
Node* cur = head;
for (; cur->next->next; cur = cur->next) {
Node* copy = cur->next;
cur->next = copy->next; // 恢复原节点的 next
copy->next = copy->next->next; // 设置新节点的 next
}
cur->next = nullptr; // 恢复原节点的 next
return new_head;
}
};
148. 排序链表 - 力扣(LeetCode)
前置题目
- 876. 链表的中间结点(快慢指针找中间节点)
- 21. 合并两个有序链表
迭代法:
class Solution { public: ListNode* mergeTwoLists(ListNode* list1 , ListNode* list2){ ListNode dummy; ListNode* cur = &dummy; 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 dummy.next; } };递归法:
class Solution { public: ListNode* mergeTwoLists(ListNode* list1, ListNode* list2) { if(!list1) return list2; if(!list2) return list1; if(list1->val < list2->val){ list1->next = mergeTwoLists(list1->next , list2); return list1; } list2->next = mergeTwoLists(list1 , list2->next); return list2; } };
回归本题:
方法一:归并排序(分治)
- 找到链表的中间结点
head 2的前一个节点,并断开head 2与其前一个节点的连接。这样我们就把原链表均分成了两段更短的链表。- 分治,递归调用
sortList,分别排序head(只有前一半)和head 2。- 排序后,我们得到了两个有序链表,那么合并两个有序链表,得到排序后的链表,返回链表头节点。
class Solution { // 876. 链表的中间结点(快慢指针) ListNode* middleNode(ListNode* head) { ListNode* pre = head; ListNode* slow = head; ListNode* fast = head; while (fast && fast->next) { pre = slow; // 记录 slow 的前一个节点 slow = slow->next; fast = fast->next->next; } pre->next = nullptr; // 断开 slow 的前一个节点和 slow 的连接 return slow; } // 21. 合并两个有序链表(双指针) ListNode* mergeTwoLists(ListNode* list1, ListNode* list2) { ListNode dummy; // 用哨兵节点简化代码逻辑 ListNode* cur = &dummy; // cur 指向新链表的末尾 while (list1 && list2) { if (list1->val < list2->val) { cur->next = list1; // 把 list1 加到新链表中 list1 = list1->next; } else { // 注:相等的情况加哪个节点都是可以的 cur->next = list2; // 把 list2 加到新链表中 list2 = list2->next; } cur = cur->next; } cur->next = list1 ? list1 : list2; // 拼接剩余链表 return dummy.next; } public: ListNode* sortList(ListNode* head) { // 如果链表为空或者只有一个节点,无需排序 if (head == nullptr || head->next == nullptr) { return head; } // 找到中间节点,并断开 head2 与其前一个节点的连接 // 比如 head=[4,2,1,3],那么 middleNode 调用结束后 head=[4,2] head2=[1,3] ListNode* head2 = middleNode(head); // 分治 head = sortList(head); head2 = sortList(head2); // 合并 return mergeTwoLists(head, head2); } };复杂度分析
- 时间复杂度:
O(nlogn),其中n是链表长度。递归式\(T(n)=2T(n/2)+O(n)\),由主定理可得时间复杂度为O(nlogn)。- 空间复杂度:
O(logn)。递归需要O(logn)的栈开销。
方法二:归并排序(迭代)
方法一的归并是自顶向下计算,需要O(logn)的递归栈开销。
方法二将其改成自底向上计算,空间复杂度优化成O(1)。
自底向上的意思是:
首先,归并长度为1的子链表。例如[4,2,1,3],把第一个节点和第二个节点归并,第三个节点和第四个节点归并,得到[2,4,1,3]。
然后,归并长度为2的子链表。例如[2,4,1,3],把前两个节点和后两个节点归并,得到[1,2,3,4]。
然后,归并长度为4的子链表。
依此类推,直到归并的长度大于等于链表长度为止,此时链表已经是有序的了。
具体算法:
- 遍历链表,获取链表长度length。
- 初始化步长step=1。
- 循环直到step≥length。
- 每轮循环,从链表头节点开始。
- 分割出两段长为step的链表,合并,把合并后的链表插到新链表的末尾。重复该步骤,直到链表遍历完毕。
- 把step扩大一倍。回到第4步。
class Solution { // 获取链表长度 int getListLength(ListNode* head) { int length = 0; while (head) { length++; head = head->next; } return length; } // 分割链表 // 如果链表长度 <= size,不做任何操作,返回空节点 // 如果链表长度 > size,把链表的前 size 个节点分割出来(断开连接),并返回剩余链表的头节点 ListNode* splitList(ListNode* head, int size) { // 先找到 next_head 的前一个节点 ListNode* cur = head; for (int i = 0; i < size - 1 && cur; i++) { cur = cur->next; } // 如果链表长度 <= size if (cur == nullptr || cur->next == nullptr) { return nullptr; // 不做任何操作,返回空节点 } ListNode* next_head = cur->next; cur->next = nullptr; // 断开 next_head 的前一个节点和 next_head 的连接 return next_head; } // 21. 合并两个有序链表(双指针) // 返回合并后的链表的头节点和尾节点 pair<ListNode*, ListNode*> mergeTwoLists(ListNode* list1, ListNode* list2) { ListNode dummy; // 用哨兵节点简化代码逻辑 ListNode* cur = &dummy; // cur 指向新链表的末尾 while (list1 && list2) { if (list1->val < list2->val) { cur->next = list1; // 把 list1 加到新链表中 list1 = list1->next; } else { // 注:相等的情况加哪个节点都是可以的 cur->next = list2; // 把 list2 加到新链表中 list2 = list2->next; } cur = cur->next; } cur->next = list1 ? list1 : list2; // 拼接剩余链表 while (cur->next) { cur = cur->next; } // 循环结束后,cur 是合并后的链表的尾节点 return {dummy.next, cur}; } public: ListNode* sortList(ListNode* head) { int length = getListLength(head); // 获取链表长度 ListNode dummy(0, head); // 用哨兵节点简化代码逻辑 // step 为步长,即参与合并的链表长度 for (int step = 1; step < length; step *= 2) { ListNode* new_list_tail = &dummy; // 新链表的末尾 ListNode* cur = dummy.next; // 每轮循环的起始节点 while (cur) { // 从 cur 开始,分割出两段长为 step 的链表,头节点分别为 head1 和 head2 ListNode* head1 = cur; ListNode* head2 = splitList(head1, step); cur = splitList(head2, step); // 下一轮循环的起始节点 // 合并两段长为 step 的链表 auto [head, tail] = mergeTwoLists(head1, head2); // 合并后的头节点 head,插到 new_list_tail 的后面 new_list_tail->next = head; new_list_tail = tail; // tail 现在是新链表的末尾 } } return dummy.next; } };复杂度分析
- 时间复杂度:O(nlogn),其中n是链表长度。
- 空间复杂度:O(1)。
23. 合并 K 个升序链表 - 力扣(LeetCode)
方法一:最小堆
合并后的第一个节点
first,一定是某个链表的头节点(因为链表已按升序排列)。合并后的第二个节点,可能是某个链表的头节点,也可能是first的下一个节点。
例如有三个链表
1->2->5, 3->4->6, 4->5->6,找到第一个节点1之后,第二个节点不是另一个链表的头节点,而是节点1的下一个节点2。按照这个过程继续思考,每当我们找到一个节点值最小的节点x,就把节点
x.next加入「可能是最小节点」的集合中。因此,我们需要一个数据结构,它支持:
从数据结构中找到并移除最小节点。插入节点。这可以用最小堆实现。
初始把所有链表的头节点入堆,然后不断弹出堆中最小节点x,如果x.next不为空就加入堆中。循环直到堆为空。把弹出的节点按顺序拼接起来,就得到了答案。
实现时,可以用哨兵节点简化代码。
函数
priority_queue中关键点解释1. 为什么需要
decltype(cmp)?
priority_queue的模板参数需要明确三个信息:
- 元素类型:
ListNode*(堆中存储的是链表节点指针)。- 底层容器类型:
vector<ListNode*>(默认用 vector 实现堆)。- 比较器类型:
decltype(cmp)(用来定义堆中元素的比较规则)。- 由于
cmp是一个 lambda 表达式,它的类型是编译器自动生成的、唯一的匿名类型。直接用decltype(cmp)可以避免手动写出这个复杂的类型名。这种写法是 C++11 后对 lambda 表达式类型的标准处理方式,避免了手动编写复杂的比较器类型。2. 为什么不用
std::greater或std::less?
- 标准库的比较器(如
std::greater)是按节点指针的地址大小比较的,而我们需要按节点的val值比较。- 因此必须自定义一个比较规则(lambda 表达式
cmp),让堆按照节点的val值构造最小堆。
class Solution {
public:
ListNode* mergeKLists(vector<ListNode* > &lists) {
auto cmp = [](const ListNode* a, const ListNode* b) {
return a->val > b->val; // 最小堆:a 的 val 比 b 的 val 大时,a 下沉
};
priority_queue<ListNode*, vector<ListNode*>, decltype(cmp)> pq;// 元素类型 ^^^^^^^^^ 底层容器类型 ^^^^^^^^^^ 比较器类型 ^^^^^^
for (auto head: lists) {
if (head) {
pq.push(head);
}
}
ListNode dummy{}; // 哨兵节点,作为合并后链表头节点的前一个节点
auto cur = &dummy;
while (!pq.empty()) { // 循环直到堆为空
auto node = pq.top(); // 剩余节点中的最小节点
pq.pop();
if (node->next) { // 下一个节点不为空
pq.push(node->next); // 下一个节点有可能是最小节点,入堆
}
cur->next = node; // 合并到新链表中
cur = cur->next; // 准备合并下一个节点
}
return dummy.next; // 哨兵节点的下一个节点就是新链表的头节点
}
};
/*复杂度分析
时间复杂度:O(nlogk),其中k为lists的长度,n为所有链表的节点数之和。
空间复杂度:O(k)。堆中至多有k个元素。
*/
方法二:分治
暴力做法是,按照
21. 合并两个有序链表的题解思路,先合并前两个链表,再把得到的新链表和第三个链表合并,再和第四个链表合并,依此类推。但是这种做法,平均每个节点会参与到O(k)次合并中(用
(1+2+⋯+k)/k粗略估计),所以总的时间复杂度为O(nk)。一个巧妙的思路是,把lists一分为二(尽量均分),先合并前一半的链表,再合并后一半的链表,然后把这两个链表合并成最终的链表。如何合并前一半的链表呢?我们可以继续一分为二。如此分下去直到只有一个链表,此时无需合并。
我们可以写一个递归来完成上述逻辑,按照一分为二再合并的逻辑,递归像是在后序遍历一棵平衡二叉树。由于平衡树的高度是
O(logk),所以每个链表节点只会出现在O(logk)次合并中!这样就做到了更快的O(nlogk)时间。假设输入链表为:这个例子中,合并三个链表的分治过程如下:
- List1: 1 → 4 → 5
- List2: 1 → 3 → 4
- List3: 2 → 6
分治步骤:
- 分割:将三个链表分成左半部分(List1)和右半部分(List2、List3)。
- 合并右半部分:
- 进一步分割为 List2 和 List3。
- 合并 List2 和 List3 → 1 → 2 → 3 → 4 → 6。
- 合并左半部分与右半结果:
- 合并 List1(1→4→5)和上一步的结果(1→2→3→4→6)→ 最终合并结果为 1→1→2→3→4→4→5→6。
最终结果:
1 → 1 → 2 → 3 → 4 → 4 → 5 → 6代码执行流程:
- 递归分割链表数组,逐层合并相邻链表,最终将所有链表合并为一个有序链表。
class Solution {
// 21. 合并两个有序链表
ListNode* mergeTwoLists(ListNode* list1, ListNode* list2) {
ListNode dummy{}; // 用哨兵节点简化代码逻辑
auto cur = &dummy; // cur 指向新链表的末尾
while (list1 && list2) {
if (list1->val < list2->val) {
cur->next = list1; // 把 list1 加到新链表中
list1 = list1->next;
} else { // 注:相等的情况加哪个节点都是可以的
cur->next = list2; // 把 list2 加到新链表中
list2 = list2->next;
}
cur = cur->next;
}
cur->next = list1 ? list1 : list2; // 拼接剩余链表
return dummy.next;
}
// 合并从 lists[i] 到 lists[j-1] 的链表
ListNode* mergeKLists(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 = mergeKLists(lists, i, i + m / 2); // 合并左半部分
auto right = mergeKLists(lists, i + m / 2, j); // 合并右半部分
return mergeTwoLists(left, right); // 最后把左半和右半合并
}
public:
ListNode* mergeKLists(vector<ListNode* > &lists) {
return mergeKLists(lists, 0, lists.size());
}
};
/*复杂度分析
时间复杂度:O(nlogk),其中k为lists的长度,n为所有链表的节点数之和。每个节点参与链表合并的次数为O(logk)次,一共有n个节点,所以总的时间复杂度为O(nlogk)。
空间复杂度:O(logk)。递归深度为O(logk),需要用到O(logk)的栈空间。Python 忽略切片产生的额外空间。
146. LRU 缓存 - 力扣(LeetCode)
问:需要几个哨兵节点?
答:一个就够了。一开始哨兵节点
dummy的prev和next都指向dummy。随着节点的插入,dummy的next指向链表的第一个节点(最上面的书),prev指向链表的最后一个节点(最下面的书)。问:为什么节点要把key也存下来?
答:在删除链表末尾节点时,也要删除哈希表中的记录,这需要知道末尾节点的key。
class Node {
public:
int key;
int value;
Node* prev;
Node* 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;
// 删除一个节点(抽出一本书)
void remove(Node* x) {
x->prev->next = x->next;
x->next->prev = x->prev;
}
// 在链表头添加一个节点(把一本书放在最上面)
void push_front(Node* x) {
x->prev = dummy;
x->next = dummy->next;
x->prev->next = x;
x->next->prev = x;
}
// 获取 key 对应的节点,同时把该节点移到链表头部
Node* get_node(int key) {
auto it = key_to_node.find(key);
if (it == key_to_node.end()) { // 没有这本书
return nullptr;
}
Node* 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) {
Node* node = get_node(key);
return node ? node->value : -1;
}
void put(int key, int value) {
Node* node = get_node(key);
if (node) { // 有这本书
node->value = value; // 更新 value
return;
}
key_to_node[key] = node = new Node(key, value); // 新书
push_front(node); // 放在最上面
if (key_to_node.size() > capacity) { // 书太多了
Node* back_node = dummy->prev;
key_to_node.erase(back_node->key);
remove(back_node); // 去掉最后一本书
delete back_node; // 释放内存
}
}
};









浙公网安备 33010602011771号