【优化算法:双指针算法刷题宝典】—— 三数之和

⚡ CYBER_PROFILE ⚡
/// SYSTEM READY ///


WARNING : DETECTING HIGH ENERGY

🌊 🌉 🌊 心手合一 · 水到渠成

|------------------------------------------------------------------------------------|--------------------------------------------------------------------------------------------------------------|
| >>> ACCESS TERMINAL <<< ||
| 🦾 作者主页 | 🔥 C++初阶 |
| 💾C++进阶 | 📡 代码仓库 |


Running Process: 100% | Latency: 0ms


索引与导读

前言

本专栏深度聚焦双指针算法 ,精准击破高频面试真题。不仅还原算法原理解题思维 的推演过程,更倾囊相授笔者的实战复盘笔记。旨在通过典型案例拆解,助你构建由点及面的算法知识体系,高效攻克面试难关

一、leetcode原题

🔗Lucy的空间骇客裂缝:


二、题目分析

2.1 核心目标

  • 在给定的整数数组 nums 中,找出所有满足条件的 三元组 n u m s \[ i , n u m s j , n u m s k ] nums\[i, numsj, numsk] nums\[i,numsj,numsk]
  • 数学条件:这三个数的和必须等于 0,即 n u m s i + n u m s j + n u m s k = = 0 numsi + numsj + numsk == 0 numsi+numsj+numsk==0

2.2 限制条件

  • 索引互异: 选出的三个数在原数组中的下标必须互不相同( i ≠ j i \neq j i=j, i ≠ k i \neq k i=k 且 j ≠ k j \neq k j=k)

  • 去重要求: 答案中 不可以包含重复 的三元组。这意味着如果结果中已经有了 [-1, 0, 1],就不能再放入 [0, -1, 1]

2.3 数据规模

  • 数组长度: 3 ≤ n u m s . l e n g t h ≤ 3000 3 \le nums.length \le 3000 3≤nums.length≤3000。

    • 解读:由于长度达到了 3000,使用 O ( N 3 ) O(N^3) O(N3) 的纯暴力三层循环
  • 数值范围: − 10 5 ≤ n u m s i ≤ 10 5 -10^5 \le numsi \le 10^5 −105≤numsi≤105

    • 解读:三个数相加可能达到 3 × 10 5 3 \times 10^5 3×105 或 − 3 × 10 5 -3 \times 10^5 −3×105,在标准 int 范围内,不需要担心溢出问题

总结提取:

这道题的难点不在于"找和为 0",而在于如何在不超时的情况下 彻底去重排序 是处理去重最有效的预处理手


三、算法设计思路

3.1 排序 + 暴力枚举 + 利用 set 容器去重

  • 排序
    先执行 sort(nums.begin(), nums.end()) 有两个巨大好处:
    • 方便去重: 排序后,相同的三元组(如 [1, -1, 0])都会变成统一的顺序(如 [-1, 0, 1])。这样存入 std::set 时,重复项会被自然过滤
    • 剪枝优化: 如果排序后第一个数就大于 0,那么后面的数都大于 0,和绝对不可能为 0,可以直接停止

  • 三层循环
    通过三层 for 循环遍历数组,尝试所有索引 i , j , k i, j, k i,j,k 的组合:

  • 利用set去重
    • 为了满足题目"不包含重复三元组"的要求,我们将找到的每一个满足条件的三元组存入一个 std::set<vector<int>>
    • 由于 set 会自动去重,最后将 set 中的内容转回 vector 返回即可

代码实现
cpp 复制代码
#include <vector>
#include <algorithm>
#include <set>

using namespace std;

class Solution {
public:
    vector<vector<int>> threeSum(vector<int>& nums) {
        int n = nums.size();
        // 1. 排序:为了让重复的三元组具有相同的排列顺序
        sort(nums.begin(), nums.end());
        
        set<vector<int>> resultSet; // 存储结果并自动去重
        
        // 2. 暴力三层嵌套循环
        for (int i = 0; i < n; ++i) {
            // 优化:如果当前数字大于0,后面三个数之和必然大于0
            if (nums[i] > 0) break; 
            
            for (int j = i + 1; j < n; ++j) {
                for (int k = j + 1; k < n; ++k) {
                    if (nums[i] + nums[j] + nums[k] == 0) {
                        // 3. 存入 set,set 会根据内容自动判断是否重复
                        resultSet.insert({nums[i], nums[j], nums[k]});
                    }
                }
            }
        }
        
        // 将 set 转换为题目要求的 vector 格式
        return vector<vector<int>>(resultSet.begin(), resultSet.end());
    }
};

3.2 排序 + 双指针

1)基本思路
  1. 排序;

  2. 定义一个指针 i 固定一个数 a
    a<=0的时候才考虑后面双指针区域

  3. 在该数后面的区间内,利用 "双指针算法"

    快速找到两个的和等于 -a 即可

cpp 复制代码
class Solution {
public:
    vector<vector<int>>
    threeSum(vector<int>& nums) { // 存储符合三叔之和的数组函数

        //这里我们定义一个数组来存储,不能用函数去存储
        vector<vector<int>> ret;

        // 1.排序
        sort(nums.begin(), nums.end());

        int n = nums.size();
        // 2.固定一个数a
        for (int i = 0; i < n; i++) {
            if (a > 0)
                break;
            int left = i + 1, right = n - 1,target = -nums[i];
            // 3.双指针操作
            while (left < right) {
                int sum = nums[left] + nums[right];
                if (sum < target) {
                    left++;
                } else if (sum > target) {
                    right--;
                } else {
                    ret.push_back({nums[i], nums[left], nums[right]});
                }
            }
        }
    }
};

2)处理细节问题

在上一种容器去重 的解法中,如果我们在笔试中运用这种的解法是可以通过的;但是如果在面试中面试官问你有没有另外一种解法,这时候你就会被难住了

所以这个细节问题,我们主要处理去重和不漏,以及防止越界


  • 去重

    • 找到一种结果之后,leftright 指针要跳过重复元素
    • 当使用完一次双指针算法之后,i 也需要跳过重复元素
  • 不漏

    找到一种结果之后,不要"停",缩小区间,继续寻找

  • 防止越界

    这个我们通过后面的代码讲解

cpp 复制代码
class Solution {
public:
    vector<vector<int>>
    threeSum(vector<int>& nums) { // 存储符合三叔之和的数组函数

        // 这里我们定义一个数组来存储,不能用函数去存储
        vector<vector<int>> ret;

        // 1.排序
        sort(nums.begin(), nums.end());

        int n = nums.size();
        // 2.固定一个数a
        for (int i = 0; i < n; ) {
            if (nums[i] > 0)
                break;
            int left = i + 1, right = n - 1, target = -nums[i];
            // 3.双指针操作
            while (left < right) {
                int sum = nums[left] + nums[right];
                if (sum < target) {
                    left++;
                } else if (sum > target) {
                    right--;
                } else {
                    ret.push_back({nums[i], nums[left], nums[right]});
                    // 不漏
                    left++, right--;
                    // 去重
                    while (left < right && nums[left] == nums[left - 1]) {
                        left++;
                    }
                    while (left < right && nums[right] == nums[right + 1]) {
                        right--;
                    }
                }
            }
            i++;
            while (i < n && nums[i] == nums[i - 1]) {
                i++;
            }
        }
    return ret;
    }
};

四、代码关键点解析(防止越界)

cpp 复制代码
for (int i = 0; i < n; )

假设数组为 [-2, -2, -2, 0, 2]

  1. 第一轮循环:i = 0,固定数字 -2。双指针在后面找到了 [-2, 0, 2]

  2. 循环末尾:

    • 执行 i++,此时 i = 1
    • 进入 while 循环,发现 nums[1] 等于 nums[0](都是 -2),执行 i++,此时 i = 2
    • 继续 while 循环,发现 nums[2] 等于 nums[1](都是 -2),执行 i++,此时 i = 3
    • 此时 nums[3] = 0,不等于 nums[2]while 停止。
  3. 第二轮循环:i 直接从 3 开始,固定数字 0

结果: 我们成功跳过了中间那两个多余的 -2,避免了重复计算。


如果我们在for循环加一个i++,就会跳过一些元素,会导致漏掉情况和越界


五、分析时间与空间复杂度

5.1 时间复杂度分析

该算法的时间消耗主要分为两个部分:排序双指针遍历

A. 排序阶段

  • 使用的是 std::sort,在 C++ 中通常是快速排序、堆排序和插入排序的混合
  • 复杂度: O ( N log ⁡ N ) O(N \log N) O(NlogN),其中 N N N 是数组 nums 的长度。

B. 双指针遍历阶段

  • 外层循环: 遍历变量 ( i ),最多执行 ( N ) 次。

  • 内层双指针 : 对于每一个固定的 ( i ),leftright 分别从两端向中间靠拢。由于 left 只增不减,right 只减不增,内层 while 循环的总移动次数也是 ( O(N) )。

  • 去重操作 : 虽然代码中有多个 while 用于跳过重复元素,但这些操作本质上只是让指针走得更快,并没有增加总的遍历次数。每个元素在每一轮外层循环中最多被访问一次。

  • 总计: 外层 ( N ) 次 × 内层 ( N ) 次 = ( O(N^2) )。

结论

T(n) = O(N \\log N) + O(N\^2) = O(N\^2)

在 ( N ) 较大时,( O(N^2) ) 占据主导地位。


5.2 空间复杂度分析

空间复杂度需要根据是否考虑结果数组 以及 排序产生的栈空间来划分

A. 辅助空间

  • 指针与变量 :
    i, left, right, target, sum 等变量仅占用常数空间 ( O(1) )。

  • 排序开销 :
    std::sortC++ 中的实现通常需要 ( O(log N) ) 的递归栈空间。

  • 结论 :

    ( O(log N) )


B. 结果空间

  • 返回数组 :
    ret 存储所有符合条件的三元组。在最坏情况下(例如数组全为 0),三元组的数量可能达到 ( O(N^2) ) 级别。

  • 注意 :

    在算法竞赛和面试中,通常不计入返回结果所需的空间,除非题目特别要求。

结论

  • 如果不计结果数组: ( O(\log N) ) (取决于排序算法)。

  • 如果计入结果数组: 最坏情况下可达 ( O(N^2) )。


💻结尾--- 核心连接协议

警告: 🌠🌠正在接入底层技术矩阵。如果你已成功破解学习中的逻辑断层,请执行以下指令序列以同步数据:🌠🌠


【📡】 建立深度链接: 关注本终端。在赛博丛林中深耕底层架构,从原始代码到进阶协议,同步见证每一次系统升级。

【⚡】 能量过载分发: 执行点赞操作。通过高带宽分发,让优质模组在信息流中高亮显示,赋予知识跨维度的传播力。

【💾】 离线缓存核心: 将本页加入收藏。把这些高频实战逻辑存入你的离线存储器,在遭遇系统崩溃或需要离线检索时,实现瞬时读取。

【💬】 协议加密解密:评论区留下你的散列码。分享你曾遭遇的代码冲突或系统漏洞(那些年踩过的坑),通过交互式编译共同绕过技术陷阱。

【🛰️】 信号频率投票: 通过投票发射你的选择。你的每一次点击都在重新定义矩阵的进化方向,决定下一个被全量拆解的技术节点。



相关推荐
c2385616 小时前
vector(下)
数据结构·算法
z落落16 小时前
C# 冒泡排序+选择排序 + Array.Sort 自定义排序
数据结构·算法
wyy1851007372816 小时前
双路并行:一套匹配算法如何解决中文制单的两大核心难题
算法·ai·crm·crm系统
s_w.h16 小时前
【 linux 】文件系统
linux·运维·服务器·算法·bash
无限进步_16 小时前
【C++】weak_ptr、循环引用与线程安全
开发语言·数据结构·c++·算法·安全
罗超驿17 小时前
9.LeetCode 209. 长度最小的子数组 | 滑动窗口专题详解
java·算法·leetcode·面试
水蓝烟雨17 小时前
0135. 分发糖果
算法·leetcode
IronMurphy17 小时前
【算法五十二】5. 最长回文子串
算法
Lewiis17 小时前
白话选择排序
数据结构·算法·排序算法
计算机安禾17 小时前
【算法分析与设计】第19篇:二分图匹配与指派问题
大数据·人工智能·算法