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

⚡ CYBER_PROFILE ⚡
/// SYSTEM READY ///


WARNING \]: DETECTING HIGH ENERGY **🌊 🌉 🌊 心手合一 · 水到渠成** ![分隔符](https://i-blog.csdnimg.cn/direct/60a3de2294e9439abad47378e657b337.gif) |------------------------------------------------------------------------------------|--------------------------------------------------------------------------------------------------------------| | **\>\>\> ACCESS TERMINAL \<\<\<** || | [**\[ 🦾 作者主页 \]**](https://blog.csdn.net/fengtinghuqu520?spm=1000.2115.3001.5343) | [**\[ 🔥 C++初阶 \]**](https://blog.csdn.net/fengtinghuqu520/category_13085789.html) | | [**\[ 💾C++进阶 \]**](https://blog.csdn.net/fengtinghuqu520/category_13085793.html) | [**\[ 📡 代码仓库 \]**](https://blog.csdn.net/fengtinghuqu520/article/details/147275999?spm=1001.2014.3001.5502) | --------------------------------------- Running Process: 100% \| Latency: 0ms *** ** * ** *** #### 索引与导读 * [前言](#前言) * * [一、leetcode原题](#一、leetcode原题) * [二、题目分析](#二、题目分析) * * [2.1 核心目标](#2.1 核心目标) * [2.2 限制条件](#2.2 限制条件) * [2.3 数据规模](#2.3 数据规模) * [总结提取:](#总结提取:) * [三、算法设计思路](#三、算法设计思路) * * [3.1 排序 + 暴力枚举 + 利用 set 容器去重](#3.1 排序 + 暴力枚举 + 利用 set 容器去重) * * [代码实现](#代码实现) * [3.2 排序 + 双指针](#3.2 排序 + 双指针) * * [1)基本思路](#1)基本思路) * [2)处理细节问题](#2)处理细节问题) * [四、代码关键点解析(防止越界)](#四、代码关键点解析(防止越界)) * [五、分析时间与空间复杂度](#五、分析时间与空间复杂度) * * [5.1 时间复杂度分析](#5.1 时间复杂度分析) * [5.2 空间复杂度分析](#5.2 空间复杂度分析) * [💻结尾--- 核心连接协议](#💻结尾— 核心连接协议) ## 前言 本专栏深度聚焦**双指针算法** ,精准击破高频面试真题。不仅还原**算法原理** 与**解题思维** 的推演过程,更倾囊相授笔者的实战复盘笔记。**旨在通过典型案例拆解,助你构建由点及面的算法知识体系,高效攻克面试难关** ### 一、leetcode原题 > [🔗Lucy的空间骇客裂缝:](https://leetcode.cn/problems/3sum/description/) ![在这里插入图片描述](https://i-blog.csdnimg.cn/direct/ef2f5cb7771444989f8ce43ff39d204f.png) *** ** * ** *** ### 二、题目分析 #### 2.1 核心目标 * 在给定的整数数组 nums 中,找出所有满足条件的 三元组 \[ n u m s \[ i \] , n u m s \[ j \] , n u m s \[ k \] \] \[nums\[i\], nums\[j\], nums\[k\]\] \[nums\[i\],nums\[j\],nums\[k\]

  • 数学条件:这三个数的和必须等于 0,即 n u m s [ i ] + n u m s [ j ] + n u m s [ k ] = = 0 nums[i] + nums[j] + nums[k] == 0 nums[i]+nums[j]+nums[k]==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 nums[i] \le 10^5 −105≤nums[i]≤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) )。


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

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


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

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

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

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

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



相关推荐
1104.北光c°1 小时前
Leetcode215 三种写法完成数组中的第K个最大元素 【hot100算法个人笔记】【java写法】
java·笔记·程序人生·算法·leetcode·排序算法·快速选择
AIpanda8882 小时前
当数字员工与熊猫智汇协作,如何实现销售潜力的全面提升?
算法
无限进步_2 小时前
【C++】AVL树完全解析:从平衡因子到四种旋转
c语言·开发语言·数据结构·c++·后端·算法·github
zubylon2 小时前
前端 RAG:把文档检索接到聊天页
前端·人工智能·算法
Dfreedom.2 小时前
【实战篇】分类任务全流程演示——决策树
人工智能·算法·决策树·机器学习·分类
阿梦Anmory2 小时前
【RAG相关】深入理解混合检索:BM25关键词检索与RRF融合算法详解
算法
浅念-2 小时前
LeetCode最短路必看:BFS算法原理+经典题解
数据结构·c++·算法·leetcode·职场和发展·bfs·宽度优先
aqiu1111112 小时前
ACM校赛
算法
嵌入式小杰3 小时前
一阶低通滤波入门教程:从原理到单片机 C 代码实现
c语言·开发语言·stm32·单片机·算法