- 第 189 篇 -
Date: 2026 - 03- 15 | 周日
Author: 郑龙浩(仟墨)
今日算法or技巧:双指针 & 链表 & 回溯算法
2026-03-15-算法打卡day23
文章目录
- 2026-03-15-算法打卡day23
-
- 1-力扣19-删除链表的倒数第N个结点
- 2-面试题02.07.-链表相交
-
- 【题目】
- [【思路1 & 代码】哈希表](#【思路1 & 代码】哈希表)
- [【思路2 & 代码】双指针](#【思路2 & 代码】双指针)
- 3-力扣142-环形链表II
-
- 【题目】
- [【思路1 & 代码】哈希表](#【思路1 & 代码】哈希表)
- [【思路2 & 代码】双指针](#【思路2 & 代码】双指针)
- 4-力扣15-三数之和
- 5-力扣18-四数之和
- 6-力扣77-组合
1-力扣19-删除链表的倒数第N个结点
难度:中等
算法/技巧:链表 & 栈 & 双指针
【题目】
给你一个链表,删除链表的倒数第 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]
提示:
- 链表中结点的数目为
sz 1 <= sz <= 300 <= Node.val <= 1001 <= n <= sz
【注意】
题目说了链表长度>=1,无需判断链表为空的情况出现
【思路1 & 代码】 计算链表长度
- 创建一个虚拟头结点 dummy,dummy->next 永远存储头结点
- 主要目的是:不需要对头结点进行特判了
- 如果要删除的节点恰好是头节点,那么"待删除节点的前驱节点"是不存在的的。这会增加额外的判断逻辑,代码会显得冗长
- 通过在头节点之前添加一个固定的虚拟节点 ,可以保证"待删除节点的前驱节点"总是存在 。这样,无论是的逻辑就可以统一处理所有情况(包括删除头节点),不需要对"是否删除头节点"进行特判。
- 从头节点开始对链表进行一次遍历,得到链表的长度 len
- 再从头结点遍历第二遍,当遍历到第 len−n 个节点时,它就是我们需要删除的节点的前面的一个节点,定义为pre
- 然后让该节点pre的next指向要删除节点的下一个节点,也就是pre的next指向下一个节点的下一个节点
- 最后返回dummy->next
cpp
// 方法1:计算链表长度
ListNode* removeNthFromEnd(ListNode* head, int n) {
ListNode* dummy = new ListNode(0, head); // 虚拟头结点
int len = 0;
ListNode* cur = head;
while (cur != nullptr) { // 计算链表长度(只有当前节点不是nullptr才会进行计数)
len++;
cur = cur->next;
}
ListNode* pre = dummy; // 找到要删除节点的前驱结点
for (int i = 0; i < len - n; i++) {
pre = pre->next;
}
pre->next = pre->next->next;
return dummy->next;
}
【思路2 & 代码】 利用栈「后进先出」的特性
- 创建一个虚拟头结点 dummy,dummy->next 永远存储头结点
- 主要目的是:不需要对头结点进行特判了
- 如果要删除的节点恰好是头节点,那么"待删除节点的前驱节点"是不存在的的。这会增加额外的判断逻辑,代码会显得冗长
- 通过在头节点之前添加一个固定的虚拟节点 ,可以保证"待删除节点的前驱节点"总是存在。这样,无论是的逻辑就
该方法使用栈「后进先出」的特性
- 将所有的节点压入栈中
- 然后弹出最后的n个节点,此时第n+1个结点还在栈中的top没有弹出
- 此时只需要取出第n+1个节点pre,让
pre->next = pre->next->next即可- 这个第n+1个节点,就是弹出n个节点后,栈中的top节点
cpp
// 方法2:利用栈「后进先出」的特性
ListNode* removeNthFromEnd(ListNode* head, int n) {
ListNode* dummy = new ListNode(0, head); // 虚拟头结点
stack <ListNode*> sta;
ListNode* cur = dummy; // 将dummy也加入栈中,如果长度只有1的话,要让pre可以指向虚拟节点
while (cur != nullptr) { // 将dummy在内的所有节点加入栈中
sta.push(cur);
cur = cur->next;
}
// 删除最后n个节点
for (int i = 0; i < n; i++) {
sta.pop();
}
// 取出倒数第n + 1个节点
ListNode* pre = sta.top();
pre->next = pre->next->next;
return dummy->next;
}
【思路3 & 代码】双指针,让两个指针始终保持n个距离
- 创建一个虚拟头结点 dummy,dummy->next 永远存储头结点
- 主要目的是:不需要对头结点进行特判了
- 如果要删除的节点恰好是头节点,那么"待删除节点的前驱节点"是不存在的的。这会增加额外的判断逻辑,代码会显得冗长
- 通过在头节点之前添加一个固定的虚拟节点 ,可以保证"待删除节点的前驱节点"总是存在。这样,无论是的逻辑就
该方法刚开始确实没想到,我觉得也是最好的一个方法
- 使用两个指针,让这两个指针始终保持n个距离(first 和 second,first后面第二个就是second),first 在 second的前面
- 换句话说,first最后停留的位置就是要删除结点的前驱
- 而second停留的位置是最后一个节点
- 写一个循环,让first 和 second同时向后移动,当second移动到最后一个节点时,first就是要删除节点的前驱结点(前面一个节点),此时只需要执行
first->next = first->next->next
cpp
// 方法3:双指针,让两个指针始终保持n个距离
ListNode* removeNthFromEnd(ListNode* head, int n) {
ListNode* dummy = new ListNode(0, head); // 虚拟头结点
ListNode* first = dummy;
ListNode* second = first; // second是first后的第n个节点
for (int i = 0; i < n; i++) second = second->next;
while (second->next != nullptr) { // 让second停留在最后一个节点,而不是最后一个结点的后面(第一次写不小心写成了second != nullptr)此时循环结束时,second停留的是最后一个节点后面,那么frist停留的就是要删除的节点了,那么要删除节点的前驱就会丢失了,所以不能这么写
first = first->next;
second = second->next;
}
first->next = first->next->next; // 此时first的位置是删除节点的前驱,所以将前驱指向删除节点的后面节点就可以了
return dummy->next;
}
【代码】
cpp
/* 2026-03-15-算法打卡day23
1-力扣19-删除链表的倒数第N个结点
Author:郑龙浩
Date:2026-03-15
算法/技巧:链表 & 双指针
用时:
*/
#include "bits/stdc++.h"
using namespace std;
/**
* 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) {}
* };
*/
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:
// 方法1:计算链表长度
/*
ListNode* removeNthFromEnd(ListNode* head, int n) {
ListNode* dummy = new ListNode(0, head); // 虚拟头结点
int len = 0;
ListNode* cur = head;
while (cur != nullptr) { // 计算链表长度(只有当前节点不是nullptr才会进行计数)
len++;
cur = cur->next;
}
ListNode* pre = dummy; // 找到要删除节点的前驱结点
for (int i = 0; i < len - n; i++) {
pre = pre->next;
}
pre->next = pre->next->next;
return dummy->next;
}
*/
// 方法2:利用栈「后进先出」的特性
/*
ListNode* removeNthFromEnd(ListNode* head, int n) {
ListNode* dummy = new ListNode(0, head); // 虚拟头结点
stack <ListNode*> sta;
ListNode* cur = dummy; // 将dummy也加入栈中,如果长度只有1的话,要让pre可以指向虚拟节点
while (cur != nullptr) { // 将dummy在内的所有节点加入栈中
sta.push(cur);
cur = cur->next;
}
// 删除最后n个节点
for (int i = 0; i < n; i++) {
sta.pop();
}
// 取出倒数第n + 1个节点
ListNode* pre = sta.top();
pre->next = pre->next->next;
return dummy->next;
}
*/
// 方法3:双指针,让两个指针始终保持n个距离
ListNode* removeNthFromEnd(ListNode* head, int n) {
ListNode* dummy = new ListNode(0, head); // 虚拟头结点
ListNode* first = dummy;
ListNode* second = first; // second是first后的第n个节点
for (int i = 0; i < n; i++) second = second->next;
while (second->next != nullptr) { // 让second停留在最后一个节点,而不是最后一个结点的后面(第一次写不小心写成了second != nullptr)此时循环结束时,second停留的是最后一个节点后面,那么frist停留的就是要删除的节点了,那么要删除节点的前驱就会丢失了,所以不能这么写
first = first->next;
second = second->next;
}
first->next = first->next->next; // 此时first的位置是删除节点的前驱,所以将前驱指向删除节点的后面节点就可以了
return dummy->next;
}
};
int main() {
ios::sync_with_stdio(0);
cin.tie(0); cout.tie(0);
return 0;
}
2-面试题02.07.-链表相交
难度:方法1简单,方法2中等
方法2主要是想不到,其实明白了后也挺简单的,主要是想不到这个点上,做题还是太少了
算法/技巧:链表 & 哈希表 & 双指针
【题目】
给你两个单链表的头节点 headA 和 headB ,请你找出并返回两个单链表相交的起始节点。如果两个链表没有交点,返回 null 。
图示两个链表在节点 c1 开始相交**:**

题目数据 保证 整个链式结构中不存在环。
注意 ,函数返回结果后,链表必须 保持其原始结构 。
示例 1:

输入: intersectVal = 8, listA = [4,1,8,4,5], listB = [5,0,1,8,4,5], skipA = 2, skipB = 3
输出: Intersected at '8'
解释: 相交节点的值为 8 (注意,如果两个链表相交则不能为 0)。
从各自的表头开始算起,链表 A 为 [4,1,8,4,5],链表 B 为 [5,0,1,8,4,5]。
在 A 中,相交节点前有 2 个节点;在 B 中,相交节点前有 3 个节点。
示例 2:

输入: intersectVal = 2, listA = [0,9,1,2,4], listB = [3,2,4], skipA = 3, skipB = 1
输出: Intersected at '2'
解释: 相交节点的值为 2 (注意,如果两个链表相交则不能为 0)。
从各自的表头开始算起,链表 A 为 [0,9,1,2,4],链表 B 为 [3,2,4]。
在 A 中,相交节点前有 3 个节点;在 B 中,相交节点前有 1 个节点。
示例 3:

输入: intersectVal = 0, listA = [2,6,4], listB = [1,5], skipA = 3, skipB = 2
输出: null
**解释:**从各自的表头开始算起,链表 A 为 [2,6,4],链表 B 为 [1,5]。
由于这两个链表不相交,所以 intersectVal 必须为 0,而 skipA 和 skipB 可以是任意值。
这两个链表不相交,因此返回 null 。
提示:
listA中节点数目为mlistB中节点数目为n0 <= m, n <= 3 * 1041 <= Node.val <= 1050 <= skipA <= m0 <= skipB <= n- 如果
listA和listB没有交点,intersectVal为0 - 如果
listA和listB有交点,intersectVal == listA[skipA + 1] == listB[skipB + 1]
【思路1 & 代码】哈希表
时间复杂度 O(m + n),分别遍历了链表1和链表2
- 使用哈希表存储链表1
- 遍历链表2的节点,只要在哈希表中找到了相同的节点就返回该节点
- 如果没有找到相同的节点,就返回null
cpp
class Solution {
public:
// 方法1:哈希表 时间复杂度 O(m + n)
ListNode *getIntersectionNode(ListNode *headA, ListNode *headB) {
if (headA == nullptr || headB == nullptr) return NULL; // 若有一方链表为空,不可能有相交的节点,直接return NULL
unordered_set <ListNode*> set; // 存储链表1中的所有节点,方便find
ListNode* cur = headA;
while (cur != nullptr) { // 将链表1的节点全部存入set哈希表
set.insert(cur);
cur = cur->next;
}
cur = headB;
while (cur != nullptr) { // 遍历链表2节点,同时查找是否在链表1中也有相同的节点
if (set.find(cur) != set.end()) return cur;
cur = cur->next;
}
return NULL;
}
};
【思路2 & 代码】双指针
这个思路我属实没想到
看了力扣的官方题解后我才知道有这个思路
力扣题解:
时间复杂度:O(1)
使用双指针的方法,可以将空间复杂度降至 O(1)。
只有当链表 headA 和 headB 都不为空时,两个链表才可能相交。因此首先判断链表 headA 和 headB 是否为空,如果其中至少有一个链表为空,则两个链表一定不相交,返回 null。
当链表 headA 和 headB 都不为空时,创建两个指针 pA 和 pB,初始时分别指向两个链表的头节点 headA 和 headB,然后将两个指针依次遍历两个链表的每个节点。具体做法如下:
每步操作需要同时更新指针 pA 和 pB。
如果指针 pA 不为空,则将指针 pA 移到下一个节点;如果指针 pB 不为空,则将指针 pB 移到下一个节点。
如果指针 pA 为空,则将指针 pA 移到链表 headB 的头节点;如果指针 pB 为空,则将指针 pB 移到链表 headA 的头节点。
当指针 pA 和 pB 指向同一个节点或者都为空时,返回它们指向的节点或者 null。
证明
下面提供双指针方法的正确性证明。考虑两种情况,第一种情况是两个链表相交,第二种情况是两个链表不相交。
情况一:两个链表相交
链表 headA 和 headB 的长度分别是 m 和 n。假设链表 headA 的不相交部分有 a 个节点,链表 headB 的不相交部分有 b 个节点,两个链表相交的部分有 c 个节点,则有 a+c=m,b+c=n。
如果 a=b,则两个指针会同时到达两个链表相交的节点,此时返回相交的节点;
如果 a
=b,则指针 pA 会遍历完链表 headA,指针 pB 会遍历完链表 headB,两个指针不会同时到达链表的尾节点,然后指针 pA 移到链表 headB 的头节点,指针 pB 移到链表 headA 的头节点,然后两个指针继续移动,在指针 pA 移动了 a+c+b 次、指针 pB 移动了 b+c+a 次之后,两个指针会同时到达两个链表相交的节点,该节点也是两个指针第一次同时指向的节点,此时返回相交的节点。
情况二:两个链表不相交
链表 headA 和 headB 的长度分别是 m 和 n。考虑当 m=n 和 m
=n 时,两个指针分别会如何移动:
如果 m=n,则两个指针会同时到达两个链表的尾节点,然后同时变成空值 null,此时返回 null;
如果 m
=n,则由于两个链表没有公共节点,两个指针也不会同时到达两个链表的尾节点,因此两个指针都会遍历完两个链表,在指针 pA 移动了 m+n 次、指针 pB 移动了 n+m 次之后,两个指针会同时变成空值 null,此时返回 null。
cpp
class Solution {
public:
// 方法1:哈希表 时间复杂度 O(m + n)
/*
ListNode *getIntersectionNode(ListNode *headA, ListNode *headB) {
if (headA == nullptr || headB == nullptr) return NULL; // 若有一方链表为空,不可能有相交的节点,直接return NULL
unordered_set <ListNode*> set; // 存储链表1中的所有节点,方便find
ListNode* cur = headA;
while (cur != nullptr) { // 将链表1的节点全部存入set哈希表
set.insert(cur);
cur = cur->next;
}
cur = headB;
while (cur != nullptr) { // 遍历链表2节点,同时查找是否在链表1中也有相同的节点
if (set.find(cur) != set.end()) return cur;
cur = cur->next;
}
return NULL;
}
*/
// 方法2:双指针 时间复杂度 O(1)
ListNode *getIntersectionNode(ListNode *headA, ListNode *headB) {
if (headA == nullptr || headB == nullptr) return NULL; // 若有一方链表为空,不可能有相交的节点,直接return NULL
ListNode* pA = headA;
ListNode* pB = headB;
// 第一次遍历两个链表
// 链表 headA 和 headB 的长度分别是 m 和 n。假设链表 headA 的不相交部分有 a 个节点,链表 headB 的不相交部分有 b 个节点,两个链表相交的部分有 c 个节点,则有 a+c=m,b+c=n
while (pA != pB) { // 两种情况,情况1:同时到了null,也就是同时到了尾结点,pA 和 pB 将两个节点全都遍历了,此时返回NULL(pA遍历了n + m个节点,pB遍历了m + n个节点)
// 情况2:同时遍历到了某个节点,此时pA遍历了a + c个节点,pB遍历了b + c个节点,此时返回的是相交节点
// 如果pA节点到了null,就从B链表的头节点headB开始遍历,如果没有到,就继续下一个节点
pA = pA == nullptr ? headB : pA->next;
// 同上
pB = pB == nullptr ? headA : pB->next;
}
return pA; // or: return pB;
}
};
3-力扣142-环形链表II
难度:中等
算法/技巧:哈希表 & 双指针
【题目】
给定一个链表的头节点 head ,返回链表开始入环的第一个节点。 如果链表无环,则返回 null。
如果链表中有某个节点,可以通过连续跟踪 next 指针再次到达,则链表中存在环。 为了表示给定链表中的环,评测系统内部使用整数 pos 来表示链表尾连接到链表中的位置(索引从 0 开始 )。如果 pos 是 -1,则在该链表中没有环。注意:pos 不作为参数进行传递,仅仅是为了标识链表的实际情况。
不允许修改 链表。
示例 1:

输入: head = [3,2,0,-4], pos = 1
**输出:**返回索引为 1 的链表节点
**解释:**链表中有一个环,其尾部连接到第二个节点。
示例 2:

输入: head = [1,2], pos = 0
**输出:**返回索引为 0 的链表节点
**解释:**链表中有一个环,其尾部连接到第一个节点。
示例 3:

输入: ead = [1], pos = -1
**输出:**返回 null
**解释:**链表中没有环。
提示:
- 链表中节点的数目范围在范围
[0, 104]内 -105 <= Node.val <= 105pos的值为-1或者链表中的一个有效索引
【思路1 & 代码】哈希表
该思路与上面一个题类似
- 从头节点开始遍历,只要遇到了曾经遇到过的节点(set有的节点),就直接return
- 只要是没有遇到过的节点,就直接将当前节点放入set,且继续遍历
cpp
class Solution {
public:
// 方法1:哈希表
// 用时6min
ListNode *detectCycle(ListNode *head) {
if (head == nullptr) return NULL; // 如果链表为空,则直接返回NULL
unordered_set <ListNode*> set;
ListNode* cur = head;
while (cur != nullptr) {
if (set.find(cur) != set.end()) return cur;
set.insert(cur);
cur = cur->next;
}
return NULL;
}
};
【思路2 & 代码】双指针
又不会,我只能想到用哈希表去做,但是时间复杂度稍高,力扣题解中给出了第二种做法,快慢指针,但是我并不能想到这个做法
我感觉这个题要想想到双指针的做法,要么是曾经遇到过类似的,要么就是数学思维比较好
包括上面的面试题02...02-链表相交也是
但是我放弃去搞懂这个双指针了,使用双指针的话,要保证数学思维和推理能力还不错,我目前还没有这个能力,目前先放弃这个题吧,等比完赛后有时间可以试着再做做这个双指针
4-力扣15-三数之和
难度:中等
算法/技巧:双指针法
在2026-03-13,两天前做过这个题,今天算是回顾吧
【题目】
给你一个整数数组 nums ,判断是否存在三元组 [nums[i], nums[j], nums[k]] 满足 i != j、i != k 且 j != k ,同时还满足 nums[i] + nums[j] + nums[k] == 0 。请你返回所有和为 0 且不重复的三元组。
**注意:**答案中不可以包含重复的三元组。
示例 1:
输入: nums = [-1,0,1,2,-1,-4]
输出: [[-1,-1,2],[-1,0,1]]
解释:
nums[0] + nums[1] + nums[2] = (-1) + 0 + 1 = 0
nums[1] + nums[2] + nums[4] = 0 + 1 + (-1) = 0
nums[0] + nums[3] + nums[4] = (-1) + 2 + (-1) = 0
不同的三元组是 [-1,0,1] 和 [-1,-1,2] 。
注意,输出的顺序和三元组的顺序并不重要。
示例 2:
输入: nums = [0,1,1]
输出: []
解释: 唯一可能的三元组和不为 0 。
示例 3:
输入: nums = [0,0,0]
输出: [[0,0,0]]
解释: 唯一可能的三元组和为 0 。
提示:
3 <= nums.length <= 3000-105 <= nums[i] <= 105
【思路】
大概思路如下:
- 排序预处理:先对数组排序,这是双指针能工作的前提,也方便去重。
- 固定第一个数 :遍历数组,固定第一个数
nums[i]。 - 双指针找剩下两个数 :在
i后面的区间[i+1, len-1]中,用左右指针left和right寻找和为和nums[i]加起来为0的两个数。 - 指针移动规则 :
- 三数之和
sum = 0:找到解,记录,然后跳过重复元素,两指针同时向中间移动- 这一步要进行去重,一定要找到解后再去重,避免出现第二个相同的解
- 这一步目的不是为了让同一个元组的数字不重复,而是为了避免出现两个相同的元素,也就是之前出现过的元组不可以再出现第二次了
- 三数之和
sum < 0:和太小,左指针右移(增大和) - 三数之和
sum > 0:和太大,右指针左移(减小和)
- 三数之和
- 去重处理 :
- 外层循环跳过相同的
nums[i],避免重复的三元组开头 - 找到解后,跳过相同的
nums[left]和nums[right],避免相同的三元组
- 外层循环跳过相同的
【代码】
cpp
/* 2026-03-15-算法打卡day23
* 4-力扣15-三数之和
* Author:郑龙浩
* Date:2026-03-15
* 算法/技巧:链表 & 双指针 & 二分查找
* 用时:20min
*/
#include "bits/stdc++.h"
using namespace std;
class Solution {
public:
vector<vector<int>> threeSum(vector<int>& nums) {
int len = nums.size();
long long sum = 0; // 存储三数之和
vector<vector<int>> result; // 存储所有结果三元组
sort(nums.begin(), nums.end()); // 排序才能使用双指针 & 二分思路
for (int first = 0; first < len - 2; first++) { // 枚举第一个数
// 跳过重复的第一个数,避免重复的三元组
// 如果当前元素等于上一个元素,就不能统计为三元组其中
// 因为上一个元素已经处理过了,当前元素与上一个元素相同,如果再将当前元素当做三元组之一去处理,可能就会出现「两个相同的三元组」,所以为了避免出现两个相同的三元组,就不要处理已经处理过的元素,将处理过的元素跳过
// 其实就是相当于跳过已经处理过的元素
if (first > 0 && nums[first] == nums[first - 1]) continue;
int second = first + 1; // 第二个指针,从当前遍历位置的下一个开始
int third = len - 1; // 第三个指针,从数组末尾开始
while (second < third) { // second 始终在 third 左
sum = (long long)nums[first] + nums[second] + nums[third];
if (sum == 0) { // 如果和为0,找到一组解,将解存储
result.push_back({nums[first], nums[second], nums[third]});
// 找到一组解后,second和third都要变动,因为要找下一组
// 我之前的疑惑:为什么不可以只变动一个?因为
second++;
third--;
// 跳过第二个数和第三个数中重复的值,避免重复三元组
// 换句话说,处理过的元素不能处理了
while (second < third && nums[second] == nums[second - 1]) second++;
while (second < third && nums[third] == nums[third + 1]) third;
}
else if (sum < 0) second++; // 如果和小于0,说明需要更大的数,左指针右移
else if (sum > 0) third--; // 如果和大于0,说明需要更小的数,右指针左移
}
}
return result;
}
};
int main() {
ios::sync_with_stdio(0);
cin.tie(0); cout.tie(0);
return 0;
}
5-力扣18-四数之和
题目难度:中等
算法:双指针
前天做过这个题,今天再做一遍
【题目】
给你一个由 n 个整数组成的数组 nums ,和一个目标值 target 。请你找出并返回满足下述全部条件且不重复 的四元组 [nums[a], nums[b], nums[c], nums[d]] (若两个四元组元素一一对应,则认为两个四元组重复):
0 <= a, b, c, d < na、b、c和d互不相同nums[a] + nums[b] + nums[c] + nums[d] == target
你可以按 任意顺序 返回答案 。
示例 1:
输入: nums = [1,0,-1,0,-2,2], target = 0
输出: [[-2,-1,1,2],[-2,0,0,2],[-1,0,0,1]]
示例 2:
输入: nums = [2,2,2,2,2], target = 8
输出: [[2,2,2,2]]
提示:
1 <= nums.length <= 200-109 <= nums[i] <= 109-109 <= target <= 109
【思路】
和力扣15-三数之和是一样的思路
唯一的不同就是「多加了一个for循环」
主要的思路就是写两层循环,i 和 j 代表元组第一个和第二个,然后用双指针去寻找第三个和第四个
思路和前面是相同的
而且这两个题都要注意:去重,必须要去重,也就是要避免之前出现过的元组不可以再出现了
【代码】
我写的代码,因为懒得加注释了,所以注释是AI加上的
cpp
/* 2026-03-15-算法打卡day23
* 5-力扣18-四数之和
* Author:郑龙浩
* Date:2026-03-15
* 算法/技巧:双指针
* 用时:16min
*/
#include "bits/stdc++.h"
using namespace std;
class Solution {
public:
vector<vector<int>> fourSum(vector<int>& nums, int target) {
int n = nums.size();
vector<vector<int>> results;
if (n < 4) return results; // 长度小于4,直接返回
sort(nums.begin(), nums.end());
// 外层循环:固定第一个数
for (int first = 0; first < n - 3; first++) {
// 跳过重复的第一个数(跳过已经处理过的元素)
// 注意:不要判断0位置的元素,因为这个元素是第二个数遍历的起始位置,前面没有将当前数字作为第1个数字处理过,所以必须参与运算
// 所以判断是否是重复数字的时候是从,遍历的第二个元素开始判断的
if (first > 0 && nums[first] == nums[first - 1]) continue;
// 第二层循环:固定第二个数
for (int second = first + 1; second < n - 2; second++) {
// 跳过重复的第二个数(跳过已经处理过的元素)
// 注意:不要判断first + 1位置的元素,因为这个元素是第二个数遍历的起始位置,前面没有将当前数字作为第2个数字处理过,所以必须参与运算
// 所以判断是否是重复数字的时候是从,遍历的第二个元素开始判断的
if (second > first + 1 && nums[second] == nums[second - 1]) continue;
int third = second + 1;
int fourth = n - 1;
while (third < fourth) {
// 关键:防止整数溢出,计算时就要转换为long long
long long sum = (long long)nums[first] + nums[second] + nums[third] + nums[fourth];
if (sum == target) { // 如果找到符合条件的四元组,直接插入,使用{}即可
results.push_back({nums[first], nums[second], nums[third], nums[fourth]});
// 移动指针,寻找下组
third++;
fourth--;
// 已经作为第3个数尝试的元素,就要跳过
while (third < fourth && nums[third] == nums[third - 1]) third++;// 跳过重复的第三个数
// 同上
while (third < fourth && nums[fourth] == nums[fourth + 1]) fourth--;// 跳过重复的第四个数
}
else if (sum < target) third++;// 和太小,左指针右移
else fourth--; // 和太大,右指针左移
}
}
}
return results;
}
};
int main() {
ios::sync_with_stdio(0);
cin.tie(0);
cout.tie(0);
return 0;
}
6-力扣77-组合
难度:中等
算法:回溯算法
【题目】
给定两个整数 n 和 k,返回范围 [1, n] 中所有可能的 k 个数的组合。
你可以按 任何顺序 返回答案。
示例 1:
**输入:**n = 4, k = 2
输出:
cpp
[
[2,4],
[3,4],
[2,3],
[1,2],
[1,3],
[1,4],
]
示例 2:
**输入:**n = 1, k = 1
输出: [[1]]
提示:
1 <= n <= 201 <= k <= n
【思路】
使用了回溯算法,没有太明白,到点了该回宿舍了,明天再好好把回溯算法搞明白,感觉真的抽象
【代码】
cpp
/* 2026-03-15-算法打卡day23
* 6-力扣77-组合
* Author:郑龙浩
* Date:2026-03-15
* 算法/技巧:回溯算法 & 递归
*/
#include "bits/stdc++.h"
using namespace std;
class Solution {
public:
vector<vector<int>> results; // 存储所有组合结果
vector<int> path; // 存储当前路径/组合
vector<vector<int>> combine(int n, int k) {
backtracking(n, k, 1); // 从数字1开始回溯
return results;
}
void backtracking(int n, int k, int startIndex) {
// 终止条件:当前路径长度等于k
if (path.size() == k) {
results.push_back(path); // 保存当前组合
return;
}
// 遍历选择:从startIndex到n
for (int i = startIndex; i <= n; i++) {
path.push_back(i); // 做出选择:将数字i加入路径
backtracking(n, k, i + 1); // 递归:处理下一个数字
path.pop_back(); // 撤销选择:回溯,移除最后一个数字
}
}
};
int main() {
ios::sync_with_stdio(0);
cin.tie(0);
cout.tie(0);
return 0;
}