算法<C++>——双指针操作链表
21. 合并两个有序链表 | 力扣 | LeetCode | 【easy】
将两个升序链表合并为一个新的 升序 链表并返回。新链表是通过拼接给定的两个链表的所有节点组成的。
示例 1:
输入:l1 = [1,2,4], l2 = [1,3,4] 输出:[1,1,2,3,4,4] 示例 2:输入:l1 = [], l2 = [] 输出:[] 示例 3:
输入:l1 = [], l2 = [0] 输出:[0] 提示:
两个链表的节点数目范围是 [0, 50]
-100 <= Node.val <= 100 l1 和 l2 均按 非递减顺序 排列 题目来源:力扣 21. 合并两个有序链表。

我们的 while 循环每次比较 p1 和 p2 的大小,把较小的节点接到结果链表上

迭代
class Solution {
public:ListNode* mergeTwoLists(ListNode* l1, ListNode* l2) {//虚拟头结点,更加便利的返回合并后的链表ListNode* preHead = new ListNode(-1);//维护prev指针ListNode* prev = preHead;while (l1 != nullptr && l2 != nullptr) {if (l1->val < l2->val) {prev->next = l1;l1 = l1->next;} else {prev->next = l2;l2 = l2->next;}prev = prev->next;}// 合并后 l1 和 l2 最多只有一个还未被合并完,我们直接将链表末尾指向未合并完的链表即可if(l1==nullptr) prev->next=l2;else prev->next=l1;return preHead->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;}else{list2->next = mergeTwoLists(list1,list2->next);return list2;}}
};
86. 分隔链表 | 力扣 | LeetCode | 【medium】
给你一个链表的头节点 head 和一个特定值 x ,请你对链表进行分隔,使得所有 小于 x 的节点都出现在 大于或等于 x 的节点之前。
你应当 保留 两个分区中每个节点的初始相对位置。
示例 1:
输入:head = [1,4,3,2,5,2], x = 3 输出:[1,2,2,4,3,5] 示例 2:
输入:head = [2,1], x = 2 输出:[1,2] 提示:
链表中节点的数目在范围 [0, 200] 内
-100 <= Node.val <= 100
-200 <= x <= 200 题目来源:力扣 86. 分隔链表
尝试一下
class Solution {
public:ListNode* partition(ListNode* head, int x) {//初始化两个虚拟头结点分别代表大于val和小于valListNode s(-1),l(-1);ListNode* sL = &s;ListNode* lL = &l;while(head){if(head->val<x){sL->next=head;sL=sL->next;}else{lL->next=head;lL=lL->next;}head=head->next;}//防止回环lL->next=nullptr;访问头结点sL->next=l.next;return s.next;}
};//写法二
class Solution {
public:ListNode* partition(ListNode* head, int x) {// 存放小于 x 的链表的虚拟头结点ListNode* dummy1 = new ListNode(-1);// 存放大于等于 x 的链表的虚拟头结点ListNode* dummy2 = new ListNode(-1);// p1, p2 指针负责生成结果链表ListNode* p1 = dummy1, *p2 = dummy2;// p 负责遍历原链表,类似合并两个有序链表的逻辑// 这里是将一个链表分解成两个链表ListNode* p = head;while (p != nullptr) {if (p->val >= x) {p2->next = p;p2 = p2->next;} else {p1->next = p;p1 = p1->next;}// 不能直接让 p 指针前进,// p = p->next// 断开原链表中的每个节点的 next 指针ListNode* temp = p->next;p->next = nullptr;p = temp;}// 连接两个链表p1->next = dummy2->next;return dummy1->next;}
};
很好思路一样,但是在.和->使用出错
什么时候使用->或者什么时候使用.?
你问得好,关键在于:什么时候用 .,什么时候用 ->。
✅ 一句话总结:
.(点):用于访问「对象本身」的成员。->(箭头):用于访问「指针所指对象」的成员。
📘 举个例子:
ListNode node; // 这是一个对象
ListNode* p = &node; // 这是一个指针
-
如果是对象:
node.val; // ✅ 用 . 访问成员 node.next; // ✅ 用 . 访问成员 -
如果是指针:
p->val; // ✅ 用 -> 访问成员 p->next; // ✅ 用 -> 访问成员 (*p).val; // ✅ 这是等价写法,但繁琐
💡 原理解释:
p->val 等价于 (*p).val。
也就是说:
箭头
->是语法糖(语法简化),表示“先解引用,再访问成员”。
⚠️ 在你的代码里:
head 是一个指针(ListNode*),
所以必须用:
head->val
head->next
如果写成 head.val,编译器会报错:
“request for member ‘val’ in pointer type …”
✅ 小总结:
| 变量类型 | 访问成员写法 | 示例 |
|---|---|---|
| 对象(非指针) | . | node.val |
| 指针(指向对象) | -> | head->val |
所以在链表题中(比如 LeetCode 的大多数链表题),
你几乎总是操作指针,自然就得用 ->。
23. 合并 K 个升序链表 | 力扣 | LeetCode | (hard)
给你一个链表数组,每个链表都已经按升序排列。
请你将所有链表合并到一个升序链表中,返回合并后的链表。
示例 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
示例 2:输入:lists = [] 输出:[] 示例 3:
输入:lists = [[]] 输出:[] 提示:
k == lists.length 0 <= k <= 10^4 0 <= lists[i].length <= 500
-10^4 <= lists[i][j] <= 10^4 lists[i] 按 升序 排列 lists[i].length 的总和不超过 10^4 题目来源:力扣 23. 合并 K 个升序链表。
合并两个链表的扩展
尝试使用递归:
class Solution {
public:ListNode* mergeKLists(vector<ListNode*>& lists) {//初始化为空ListNode *head=nullptr;for(int i=0;i<lists.size();i++){head=mergeTwoLists(head,lists[i]);}return head;}ListNode* mergeTwoLists(ListNode *a,ListNode *b){if(a==nullptr) return b;if(b==nullptr) return a;ListNode res;ListNode *prev = &res;ListNode *aL=a,*bL=b;while(aL&&bL){if(aL->val<bL->val){prev->next=aL;aL=aL->next;}else{prev->next=bL;bL=bL->next;}prev = prev->next;}prev->next=(aL?aL:bL);return res.next;}
};
尝试成功,看了题解……优化的方法暂时看不懂
141. 环形链表 | 力扣 | LeetCode | (easy)
给你一个链表的头节点 head ,判断链表中是否有环。
如果链表中有某个节点,可以通过连续跟踪 next 指针再次到达,则链表中存在环。 为了表示给定链表中的环,评测系统内部使用整数 pos 来表示链表尾连接到链表中的位置(索引从 0 开始)。注意:pos 不作为参数进行传递 。仅仅是为了标识链表的实际情况。
如果链表中存在环 ,则返回 true 。 否则,返回 false 。
输
输入:head = [1,2], pos = 0 输出:true 解释:链表中有一个环,其尾部连接到第一个节点。
判断链表是否包含环属于经典问题了,解决方案也是用快慢指针:
每当慢指针 slow 前进一步,快指针 fast 就前进两步。
如果 fast 最终能正常走到链表末尾,说明链表中没有环;如果 fast 走着走着竟然和 slow 相遇了,那肯定是 fast 在链表中转圈了,说明链表中含有环。
class Solution {
public:bool hasCycle(ListNode* head) {if (head == NULL || head->next == NULL)return false;ListNode* slow = head;ListNode* fast = head->next;while (slow != fast) {if (fast == NULL || fast->next == NULL) {return false;}slow = slow->next;fast = fast->next->next;}return true;}
};
142. 环形链表(||) | 力扣 | LeetCode | (medium)
给定一个链表的头节点 head ,返回链表开始入环的第一个节点。 如果链表无环,则返回 null。
如果链表中有某个节点,可以通过连续跟踪 next 指针再次到达,则链表中存在环。 为了表示给定链表中的环,评测系统内部使用整数 pos
来表示链表尾连接到链表中的位置(索引从 0 开始)。如果 pos 是 -1,则在该链表中没有环。注意:pos
不作为参数进行传递,仅仅是为了标识链表的实际情况。不允许修改 链表。

输入:head = [3,2,0,-4], pos = 1
输出:返回索引为 1 的链表节点
解释:链表中有一个环,其尾部连接到第二个节点。
尝试用哈希集合统计已经遍历过的结点,一旦重复遇到,则即刻成环,输出索引
class Solution {
public:ListNode *detectCycle(ListNode *head) {unordered_set<ListNode *>L;while(head){if(L.count(head)){return head;}else{L.insert(head);head=head->next;}}return nullptr;}
};
双(快慢)指针优化后:
class Solution {
public:ListNode* detectCycle(ListNode* head) {ListNode *s = head, *f = head, *res = head;while (f && f->next) {s = s->next;f = f->next->next;if (s == f) {while (res != s) {res = res->next;s = s->next;}return res;}}return nullptr;}
};
根据题意,任意时刻,fast 指针走过的距离都为 slow 指针的 2 倍。因此,我们有
a+(n+1)b+nc=2(a+b)⟹a=c+(n−1)(b+c) 有了 a=c+(n−1)(b+c)
的等量关系,我们会发现:从相遇点到入环点的距离加上 n−1 圈的环长,恰好等于从链表头部到入环点的距离。因此,当发现 slow 与 fast 相遇时,我们再额外使用一个指针 ptr。起始,它指向链表头部;随后,它和 slow
每次向后移动一个位置。最终,它们会在入环点相遇。
876. 单链表的中点 | 力扣 | LeetCode | (easy)
- 给你单链表的头结点 head ,请你找出并返回链表的中间结点。
如果有两个中间结点,则返回第二个中间结点。

输入:head = [1,2,3,4,5] 输出:[3,4,5] 解释:链表只有一个中间结点,值为 3 。
快慢指针(慢走一步,快走两步),快指针到末尾,慢指针到中间,考虑到两个中间节点的话,偶数个结点。
因为快指针和快指针的下一个节点不为空,所以仍然需要遍历一遍while循环,s指针往后移动到第二个中间节点。
class Solution {
public:ListNode* middleNode(ListNode* head) {ListNode *s=head,*f=head;while(f&&f->next){s=s->next;f=f->next->next;}return s;}
};
160. 相交链表 | 力扣 | LeetCode | (easy)
给你两个单链表的头节点 headA 和 headB ,请你找出并返回两个单链表相交的起始节点。如果两个链表不存在相交节点,返回 null 。
图示两个链表在节点 c1 开始相交:

题目数据 保证 整个链式结构中不存在环。
注意,函数返回结果后,链表必须 保持其原始结构 。
自定义评测:
评测系统 的输入如下(你设计的程序 不适用 此输入):
intersectVal - 相交的起始节点的值。如果不存在相交节点,这一值为 0 listA - 第一个链表 listB - 第二个链表
skipA - 在 listA 中(从头节点开始)跳到交叉节点的节点数 skipB - 在 listB
中(从头节点开始)跳到交叉节点的节点数 评测系统将根据这些输入创建链式数据结构,并将两个头节点 headA 和 headB
传递给你的程序。如果程序能够正确返回相交节点,那么你的解决方案将被 视作正确答案 。
示例 1:

输入:intersectVal = 8, listA = [4,1,8,4,5], listB = [5,6,1,8,4,5], skipA
= 2, skipB = 3 输出:Intersected at ‘8’ 解释:相交节点的值为 8 (注意,如果两个链表相交则不能为 0)。 从各自的表头开始算起,链表 A 为 [4,1,8,4,5],链表 B 为 [5,6,1,8,4,5]。 在 A 中,相交节点前有 2
个节点;在 B 中,相交节点前有 3 个节点。 — 请注意相交节点的值不为 1,因为在链表 A 和链表 B 之中值为 1 的节点 (A
中第二个节点和 B 中第三个节点) 是不同的节点。换句话说,它们在内存中指向两个不同的位置,而链表 A 和链表 B 中值为 8 的节点 (A
中第三个节点,B 中第四个节点) 在内存中指向相同的位置。
尝试失败,看灵神题解后只能说NB:
具体算法如下:
初始化两个指针 p=headA, q=headB。
不断循环,直到 p=q。
每次循环,p 和 q 各向后走一步。具体来说,如果 p 不是空节点,那么更新 p 为 p.next,否则更新 p 为 headB;如果 q 不是空节点,那么更新 q 为 q.next,否则更新 q 为 headA。
循环结束时,如果两条链表相交,那么此时 p 和 q 都在相交的起始节点处,返回 p;如果两条链表不相交,那么 p 和 q 都走到空节点,所以也可以返回 p,即空节点。
class Solution {
public:ListNode* getIntersectionNode(ListNode* headA, ListNode* headB) {ListNode *a = headA, *b = headB;while (a != b) {a = a ? a->next : headB;b = b ? b->next : headA;}return a;}
};
19. 删除链表的倒数第N个结点 | 力扣 | LeetCode | (medium)
给你一个链表,删除链表的倒数第 n 个结点,并且返回链表的头结点。
示例 1:
输入:head = [1,2,3,4,5], n = 2 输出:[1,2,3,5] 示例 2:
输入:head = [1], n = 1 输出:[] 示例 3:
输入:head = [1,2], n = 1 输出:[1]
快慢指针解决
想象有一把长度固定的尺子,左端点在链表头部,右端点在正数第 n 个节点。向右移动尺子,当尺子右端点到达链表末尾时,左端点就在倒数第 n
个节点。
class Solution {
public:ListNode* removeNthFromEnd(ListNode* head, int n) {//虚拟头结点ListNode dummy(0,head);ListNode *s=&dummy,*f=&dummy,*del;while(n--){//快指针先走n步f=f->next;}while(f->next){s=s->next;f=f->next;}del=s->next;s->next=s->next->next;delete del;return dummy.next;}
};
