链表中的双指针目前可以分为3类题目:
- 1 两个链表之间的交点
- 1.1 O(n)的空间复杂度,哈希表
- 1.2 O(1)的空间复杂度,快慢指针
- 2 查找链表的某个节点
- 2.1 快慢指针,快指针优先走n部,再快慢指针同步走
- 3 链表的合并和分解
- 3.1 链表的合并,使用两个指针指向待合并的链表
- 3.2 链表的分解,使用两个指针指向分解后的链表
1 两个链表之间的交点
1.1 相交链表
给你两个单链表的头节点 headA
和 headB
,请你找出并返回两个单链表相交的起始节点。如果两个链表不存在相交节点,返回 null
。
图示两个链表在节点 c1
开始相交:
题目数据 保证 整个链式结构中不存在环。
解题思路:
- 1 可以遍历一下链表A和链表B的长度,使遍历A,B链表时做长度对齐,如上图所示让链表A从a1开始,链表B从b2开始。
- 2 即,短链表从开头开始遍历,长链表从倒数第N(短链表的长度)个节点开始遍历。
class Solution {
public:
ListNode *getIntersectionNode(ListNode *headA, ListNode *headB) {
int lenA = 0, lenB = 0;
ListNode* curA = headA, * curB = headB;
// 1 计算不同链表的长度
while(curA){
curA = curA->next;
lenA++;
}
while(curB){
curB = curB->next;
lenB++;
}
// 2 对齐需要对比的长度
curA = headA;
curB = headB;
if (lenA > lenB){
int n = lenA - lenB;
while(n--) curA = curA->next;
}else{
int n = lenB - lenA;
while(n--) curB = curB->next;
}
// 3 找到相交节点
while(curA){
if (curA == curB) return curA;
else{
curA = curA->next;
curB = curB->next;
}
}
return nullptr;
}
};
1.2 环形链表
给定一个链表的头节点 head
,返回链表开始入环的第一个节点。 如果链表无环,则返回 null
。
如果链表中有某个节点,可以通过连续跟踪 next
指针再次到达,则链表中存在环。 为了表示给定链表中的环,评测系统内部使用整数 pos
来表示链表尾连接到链表中的位置(索引从 0 开始)。如果 pos
是 -1
,则在该链表中没有环。注意:pos
不作为参数进行传递,仅仅是为了标识链表的实际情况。
不允许修改 链表。
示例 1:
输入:head = [3,2,0,-4], pos = 1 输出:返回索引为 1 的链表节点 解释:链表中有一个环,其尾部连接到第二个节点。
解题思路:
- 1 慢指针走一步,快指针走两步,如果有环则快慢指针会相遇。
- 2 如果快指针走到空了,则没有环。
2 查找链表的某个节点
2.1 删除链表的倒数第 N 个结点
给你一个链表,删除链表的倒数第 n
个结点,并且返回链表的头结点。
示例 1:
输入:head = [1,2,3,4,5], n = 2 输出:[1,2,3,5]
解题思路 – 快慢指针:
- 1 可以让快指针先走n步,然后快慢指针一起走,当快指针->next变成nullptr时,慢指针的位置为需要删除的节点的前一个节点。
- 2 使用虚拟头结点的话,即快指针先走n+1步
class Solution {
public:
ListNode* removeNthFromEnd(ListNode* head, int n) {
// 0 如果需要增加和删除则使用虚拟头结点
ListNode* dummy = new ListNode(0);
dummy->next = head;
ListNode* slow = dummy, * fast = dummy;
n++;
// 1 快指针先走
while(n--) fast = fast->next;
// 2 快慢指针一起走
while(fast){
fast = fast->next;
slow = slow->next;
}
// 3 删除指定元素
ListNode* temp = slow->next;
slow->next = slow->next->next;
delete temp;
return dummy->next;
}
};
2.2 单链表的中点
给你单链表的头结点 head
,请你找出并返回链表的中间结点。
如果有两个中间结点,则返回第二个中间结点。
示例 1:
输入:head = [1,2,3,4,5] 输出:[3,4,5] 解释:链表只有一个中间结点,值为 3 。
解题思路:
- 1 快指针走两步,慢指针走一步
class Solution {
public:
ListNode* middleNode(ListNode* head) {
ListNode* slow = head, * fast = head;
while(fast && fast->next){
slow = slow->next;
fast = fast->next->next;
}
return slow;
}
};
3 链表的合并和分解
3.1 合并两个有序链表
将两个升序链表合并为一个新的 升序 链表并返回。新链表是通过拼接给定的两个链表的所有节点组成的。
示例 1:
输入:l1 = [1,2,4], l2 = [1,3,4] 输出:[1,1,2,3,4,4]
解题思路:
- 1 使用p1指针指向l1,p2指针指向l2,新链表使用p1和p2中较小的那个。
- 2 一旦某个链表遍历完成,直接将新链表指向还未遍历完成的链表即可。
class Solution {
public:
ListNode* mergeTwoLists(ListNode* list1, ListNode* list2) {
ListNode* dummy = new ListNode(0);
ListNode* cur = dummy;
ListNode* p1 = list1, *p2 = list2;
// 1 两个链表择最小的加入新链表
while(p1!=nullptr && p2!=nullptr){
if (p1->val < p2->val){
cur->next = p1;
// 1.1 p1链表移动
p1 = p1->next;
}
else{
cur->next = p2;
// 1.2 p2链表移动
p2 = p2->next;
}
// 1.3 新链表移动
cur = cur->next;
}
// 2 把剩下的一条链表直接接到新链表之后
if (p1 == nullptr && p2!=nullptr) cur->next = p2;
else if (p1 != nullptr && p2 == nullptr) cur->next = p1;
return dummy->next;
}
};
3.2 单链表的分解
给你一个链表的头节点 head
和一个特定值 x
,请你对链表进行分隔,使得所有 小于 x
的节点都出现在 大于或等于 x
的节点之前。
你应当 保留 两个分区中每个节点的初始相对位置。
示例 1:
输入:head = [1,4,3,2,5,2], x = 3 输出:[1,2,2,4,3,5]
解题思路:
- 1 使用两个虚拟头结点分别表示两个分离链表,同时使用两个遍历指针指向虚拟头结点。
- 2 将原始链表全部拆解完成后,拼接一下分离后的链表
- 3 每次分离操作都需要释放一下原链表的cur->next的指针,防止成环。
class Solution {
public:
ListNode* partition(ListNode* head, int x) {
// 需要新表就需要设置dummy节点和p=dummy的遍历节点
ListNode* dummy1 = new ListNode(0);
ListNode* dummy2 = new ListNode(0);
ListNode* p1 = dummy1, * p2 = dummy2;
// 原链表的遍历节点
ListNode* cur = head;
while(cur){
if (cur->val < x){
p1->next = cur;
p1 = p1->next;
}
else{
p2->next = cur;
p2 = p2->next;
}
// 释放当前cur->next,防止出现环
ListNode* temp = cur->next;
cur->next = nullptr;
cur = temp;
// cur = cur->next;
}
p1->next = dummy2->next;
return dummy1->next;
}
};
3.3 合并k个有序链表
给你一个链表数组,每个链表都已经按升序排列。
请你将所有链表合并到一个升序链表中,返回合并后的链表。
示例 1:
输入: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
解题思路:
- 1 该题相比合并2个有序链表,需要每次对k个有序链表的头结点进行排序,找到最小的头结点。
- 2 该题可以使用优先队列来解决排序的问题,优先队列会维护一个堆,每次加入一个新数据,就会重新调整堆,使堆的top元素为最大或者最小。
- 3 C++中优先队列的使用:
- priority_queue<ListNode*, vector<ListNode*>, function<bool(ListNode*, ListNode*)>> pq([](ListNode* a, ListNode* b){return a->val > b->val;});
详细代码:
/**
* 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(0);
ListNode* cur = dummy;
priority_queue<ListNode*, vector<ListNode*>, function<bool(ListNode*, ListNode*)>> pq([](ListNode* a, ListNode* b){return a->val > b->val;});
// 1 将每一个链表的头结点压入优先队列
for(auto head:lists){
if (head != nullptr) pq.push(head);
}
// 2 每从优先队列中取出一个节点,就从对应节点的链表后一个节点压入优先队列
while(!pq.empty()){
// 2.1 从优先队列中取出一个节点
ListNode* node = pq.top(); pq.pop();
// 2.2 加入新链表
cur->next = node;
// 2.3 从对应节点的链表后一个节点压入优先队列
if (node->next != nullptr) pq.push(node->next);
// 2.4 更新cur
cur = cur->next;
}
return dummy->next;
}
};