【算法面试必刷】15. 三数之和

目录

题目

题目链接

思路

复杂度

1. 排序阶段

2. 双指针搜索阶段

3. 总时间复杂度

4. 空间复杂度

代码

题目

给你一个整数数组 nums ,判断是否存在三元组 [nums[i], nums[j], nums[k]] 满足 i != ji != k 且 j != k ,同时还满足 nums[i] + nums[j] + nums[k] == 0 。请你返回所有和为 0 且不重复的三元组。

注意:答案中不可以包含重复的三元组。

题目链接

15. 三数之和 - 力扣(LeetCode)https://leetcode.cn/problems/3sum/description/?envType=study-plan-v2&envId=top-100-liked

思路

核心思想排序 + 双指针

为什么需要排序?

  1. 有序数组才能使用双指针
  2. 方便去重
  3. 可以提前终止搜索

为什么用双指针?
将 O(n³) 的暴力搜索优化为 O(n²)

复杂度

1. 排序阶段

  • 使用快速排序或归并排序
  • 时间复杂度:O(n log n)

2. 双指针搜索阶段

外层循环:执行 n-2 次,O(n) 内层循环:双指针,最坏 O(n) 总计:O(n × n) = O(n²)

为什么是 O(n²)?

  • 固定 nums[i],在 [i+1, n-1] 区间用双指针找两个数
  • 双指针遍历区间长度平均为 n/2
  • 所以是:n × (n/2) ≈ O(n²)

3. 总时间复杂度

text

O(n log n) + O(n²) = O(n²)

因为 n² 的增长速度比 n log n 快,所以主导项是 O(n²)

4. 空间复杂度

排序:O(log n) 到 O(n)(取决于排序算法) 结果存储:O(k),k是结果数量 额外空间:O(1)(双指针只用常数空间) 总空间复杂度:O(n)(如果考虑结果存储) 如果不考虑结果存储:O(log n) 或 O(1)

代码

class Solution { public: vector<vector<int>> threeSum(vector<int>& nums) { vector<vector<int>> ans; // 存储结果 int n = nums.size(); // 数组长度 // 1. 特殊情况处理:数组长度正好为3 if (n == 3) { int sum = nums[0] + nums[1] + nums[2]; if (sum == 0) { ans.push_back({nums[0], nums[1], nums[2]}); } return ans; // 直接返回,不需要复杂处理 } // 2. 关键步骤:排序(使双指针法可行) sort(nums.begin(), nums.end()); // 3. 遍历每个数字作为第一个数 for (int i = 0; i < n; i++) { // 3.1 去重:跳过重复的第一个数 // 因为排序后,相同的数字会相邻 if (i != 0 && nums[i - 1] == nums[i]) { continue; } // 3.2 双指针初始化 int l = i + 1; // 左指针,从i后面开始 int r = n - 1; // 右指针,从末尾开始 // 3.3 双指针查找剩余两个数 while (l < r) { int sum = nums[i] + nums[l] + nums[r]; if (sum > 0) { // 和太大,需要减小,右指针左移 r--; } else if (sum < 0) { // 和太小,需要增大,左指针右移 l++; } else { // 找到和为0的三元组 ans.push_back({nums[i], nums[l], nums[r]}); // 去重:跳过重复的左指针元素 while (l < r && nums[l] == nums[l + 1]) { l++; } // 去重:跳过重复的右指针元素 while (l < r && nums[r] == nums[r - 1]) { r--; } // 移动指针,继续查找其他可能 l++; r--; } } } return ans; } };

Read more

C++ 二叉搜索树全解析!增删查改 + key/value 场景 + 完整代码,一篇通关

C++ 二叉搜索树全解析!增删查改 + key/value 场景 + 完整代码,一篇通关

✨ 孤廖:个人主页 🎯 个人专栏:《C++:从代码到机器》 🎯 个人专栏:《Linux系统探幽:从入门到内核》 🎯 个人专栏:《算法磨剑:用C++思考的艺术》 折而不挠,中不为下 文章目录 * 正文: * 1. ⼆叉搜索树的概念 * 2. ⼆叉搜索树的性能分析 * 3. ⼆叉搜索树的插⼊ * 4. ⼆叉搜索树的查找 * 5. ⼆叉搜索树的删除 * 6. ⼆叉搜索树key和key/value使⽤场景 * 6.1 key搜索场景: * 6.2 key/val搜索场景 * 7. ⼆叉搜索树的实现代码 * 7.1 key模型代码实现 * 7.2 key/val代码实现 * 结语 正文: 1. ⼆叉搜索树的概念

By Ne0inhk
【C++贪心】P8769 [蓝桥杯 2021 国 C] 巧克力|普及+

【C++贪心】P8769 [蓝桥杯 2021 国 C] 巧克力|普及+

本文涉及知识点 C++贪心 [蓝桥杯 2021 国 C] 巧克力 题目描述 小蓝很喜欢吃巧克力,他每天都要吃一块巧克力。 一天小蓝到超市想买一些巧克力。超市的货架上有很多种巧克力,每种巧克力有自己的价格、数量和剩余的保质期天数,小蓝只吃没过保质期的巧克力,请问小蓝最少花多少钱能买到让自己吃 x x x 天的巧克力。 输入格式 输入的第一行包含两个整数 x x x, n n n,分别表示需要吃巧克力的天数和巧克力的种类数。 接下来 n n n 行描述货架上的巧克力,其中第 i i i 行包含三个整数 a i a_i ai , b i b_i bi

By Ne0inhk
RPC魔法揭秘:从原理到BRPC实战,用C++玩转分布式通信

RPC魔法揭秘:从原理到BRPC实战,用C++玩转分布式通信

文章目录 * 本篇摘要 * 一.什么是rpc * 简单理解 * 核心特点 * RPC 工作原理 * 常见 RPC 框架 * 典型使用场景 * 二.BRPC介绍 * 是什么? * 比gRPC强在哪? * 三.基于brpc实现简单的服务调用 * brpc安装教程 * 简单实现客户端向brpc服务端口请求服务完成应答过程(以echo回显为例) * 测试效果 * 代码汇总 * 四.封装每个服务的channels及所有服务管理者 * 五.基于etcd实现服务上下线监控来完成brpc服务调用 * 测试效果 * 代码汇总 * 六.本篇小结 本篇摘要 本文从RPC核心概念出发,阐释其“透明远程调用”的本质与工作原理,对比主流框架后聚焦百度开源的C++高性能RPC框架BRPC,详解其安装、Echo服务示例代码(含客户端/服务端实现),并延伸介绍基于ETCD的服务注册发现与信道管理封装,完整呈现分布式通信方案落地过程。 一.什么是rpc 简单理解 RPC(远程过程调用)就是让程序调用

By Ne0inhk
【C++ 类与对象 (下)】:进阶特性与编译器优化的深度实战

【C++ 类与对象 (下)】:进阶特性与编译器优化的深度实战

🎬 博主名称:月夜的风吹雨 🔥 个人专栏: 《C语言》《基础数据结构》《C++入门到进阶》 ⛺️任何一个伟大的思想,都有一个微不足道的开始! 💬 前言: 掌握了类的基础封装与默认成员函数后,很多开发者会在 “进阶特性” 上栽跟头: 为什么引用、const 成员必须用初始化列表?static 成员为什么不能在类内初始化?友元如何突破封装又不破坏设计?编译器为什么能把 “构造 + 拷贝” 优化成一步? 这些问题的答案,藏在 C++ 类与对象的进阶设计里。本篇文章将从 “实战痛点” 出发,结合底层逻辑与代码示例,带你理解这些特性的 “设计初衷” 与 “正确用法”,避开工程开发中的高频陷阱。 ✨ 阅读后,你将掌握:初始化列表的底层逻辑与强制使用场景静态成员的共享机制与实战案例(如对象计数)友元与内部类的封装权衡技巧匿名对象的生命周期与使用场景编译器对对象拷贝的优化规则与验证方法 文章目录 * 一、再探构造函数:初始化列表的底层逻辑 * 1. 初始化列表的基础语法 * 2. 必须用初始化列表的

By Ne0inhk