当前位置: 首页 > article >正文

编程题-三数之和(中等)

题目:

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

解法一(排序+哈希表查找-时间复杂度超限):

首先对输入数组vector<int>& nums进行排序,然后将nums中出现元素以及索引位置制作成哈希表,通过设置first,second前两个for循环索引以及设置在哈希表中查找满足三个数和为0的third值,并确保first索引值<second索引值<third索引值,将满足的且不重复的结果添加到最终输出的result二维动态数组中,由于使用到哈希表查找元素find()函数,因此时间复杂度为O(N^3),超限,因此本解法仅作为与下面解法的对比参考,不是正确解决本题的实现方式,如下为笔者代码:

class Solution {
public:
    vector<vector<int>> threeSum(vector<int>& nums) {
        sort(nums.begin(), nums.end());
        vector<vector<int>> result;
        int length = nums.size();
        //将nums中出现元素以及索引位置制作成哈希表
        std::unordered_map<int, std::vector<int>> element_positions;
        for(int i=0; i<length; i++){
            element_positions[nums[i]].push_back(i);
        }
        for(int first=0; first<length-2; first++){
            for(int second=first+1; second<length-1; second++){
                int third = 0-nums[first]-nums[second];
                auto it = element_positions.find(third);
                if(it !=element_positions.end()){
                    int a = (it->second).size();
                    if((it->second)[a-1]>second){
                        if(result.empty()){
                            result.push_back({nums[first], nums[second], third});
                        }
                        else{
                            std::vector<int> targett = {nums[first], nums[second], third};
                            auto itt = find(result.begin(), result.end(), targett);
                            if(itt==result.end()){
                                result.push_back({nums[first], nums[second], third});
                            }
                        }
                        
                    } 
                }
            }
        }
        return result;
    }
};

解法二(排序 + 双指针):

如果我们直接使用三重循环枚举三元组,会得到 O(N^3) 个满足题目要求的三元组(其中 N 是数组的长度)时间复杂度至少为 O(N^3)。在这之后,我们还需要使用哈希表进行去重操作,得到不包含重复三元组的最终答案,又消耗了大量的空间。这个做法的时间复杂度和空间复杂度都很高,因此我们要换一种思路来考虑这个问题

「不重复」的本质是什么?我们保持三重循环的大框架不变,只需要保证:

  • 第二重循环枚举到的元素不小于当前第一重循环枚举到的元素;

  • 第三重循环枚举到的元素不小于当前第二重循环枚举到的元素。

也就是说,我们枚举的三元组 (a,b,c) 满足 a≤b≤c,保证了只有 (a,b,c) 这个顺序会被枚举到,而 (b,a,c)、(c,b,a) 等等这些不会,这样就减少了重复。要实现这一点,我们可以将数组中的元素从小到大进行排序,随后使用普通的三重循环就可以满足上面的要求。

同时,对于每一重循环而言,相邻两次枚举的元素不能相同,否则也会造成重复。举个例子,如果排完序的数组为

[0, 1, 2, 2, 2, 3]
 ^  ^  ^

我们使用三重循环枚举到的第一个三元组为 (0,1,2),如果第三重循环继续枚举下一个元素,那么仍然是三元组 (0,1,2),产生了重复。因此我们需要将第三重循环「跳到」下一个不相同的元素,即数组中的最后一个元素 3,枚举三元组 (0,1,3)。

下面给出了改进的方法的伪代码实现:

nums.sort()
for first = 0 .. n-1
    // 只有和上一次枚举的元素不相同,我们才会进行枚举
    if first == 0 or nums[first] != nums[first-1] then
        for second = first+1 .. n-1
            if second == first+1 or nums[second] != nums[second-1] then
                for third = second+1 .. n-1
                    if third == second+1 or nums[third] != nums[third-1] then
                        // 判断是否有 a+b+c==0
                        check(first, second, third)

这种方法的时间复杂度仍然为 O(N3),毕竟我们还是没有跳出三重循环的大框架。然而它是很容易继续优化的,可以发现,如果我们固定了前两重循环枚举到的元素 a 和 b,那么只有唯一的 c 满足 a+b+c=0。当第二重循环往后枚举一个元素 b′ 时,由于 b′>b,那么满足 a+b′+c′=0 的 c′ 一定有 c′<c,即 c′ 在数组中一定出现在 c 的左侧。也就是说,我们可以从小到大枚举 b,同时从大到小枚举 c,即第二重循环和第三重循环实际上是并列的关系。

有了这样的发现,我们就可以保持第二重循环不变,而将第三重循环变成一个从数组最右端开始向左移动的指针,从而得到下面的伪代码:

nums.sort()
for first = 0 .. n-1
    if first == 0 or nums[first] != nums[first-1] then
        // 第三重循环对应的指针
        third = n-1
        for second = first+1 .. n-1
            if second == first+1 or nums[second] != nums[second-1] then
                // 向左移动指针,直到 a+b+c 不大于 0
                while nums[first]+nums[second]+nums[third] > 0
                    third = third-1
                // 判断是否有 a+b+c==0
                check(first, second, third)

这个方法就是我们常说的「双指针」,当我们需要枚举数组中的两个元素时,如果我们发现随着第一个元素的递增,第二个元素是递减的,那么就可以使用双指针的方法,将枚举的时间复杂度从 O(N2) 减少至 O(N)。为什么是 O(N) 呢?这是因为在枚举的过程每一步中,「左指针」会向右移动一个位置(也就是题目中的 b),而「右指针」会向左移动若干个位置,这个与数组的元素有关,但我们知道它一共会移动的位置数为 O(N),均摊下来,每次也向左移动一个位置,因此时间复杂度为 O(N)。

注意到我们的伪代码中还有第一重循环,时间复杂度为 O(N),因此枚举的总时间复杂度 O(N^2)。由于排序的时间复杂度为 O(NlogN),在渐进意义下小于前者,因此算法的总时间复杂度为 O(N^2)。上述的伪代码中还有一些细节需要补充,例如我们需要保持左指针一直在右指针的左侧(即满足 b≤c),具体可以参考下面的代码,均给出了详细的注释。

class Solution {
public:
    vector<vector<int>> threeSum(vector<int>& nums) {
        //对nums数组进行排序
        sort(nums.begin(), nums.end());
        vector<vector<int>> result;
        int length = nums.size();
        for(int first=0; first<length-2;first++){
            // 需要和此层上一次循环中枚举的数不相同,以保证result中结果三个元素组成的子数组与其他子数组不重复
            if(first>0 && nums[first]==nums[first-1]){
                continue;
            }
            for(int second=first+1; second<length-1; second++){
                // 需要和此层上一次循环中枚举的数不相同,以保证result中结果三个元素组成的子数组与其他子数组不重复
                if(second>first+1 && nums[second]==nums[second-1]){
                    continue;
                }
                int third = length-1;
                int target = 0-nums[first]-nums[second];
                while(second<third && nums[third]>target){
                    third--;
                }
                // 如果指针重合,随着 b 后续的增加
                // 就不会有满足 a+b+c=0 并且 b<c 的 c 了,可以break直接退出循环
                if(second==third){
                    break;
                }
                if(nums[third]==target){
                    result.push_back({nums[first], nums[second], nums[third]});
                }
            }
        }
        return result;
    }
};

时间复杂度:O(N2),其中 N 是数组 nums 的长度。空间复杂度:O(logN)。我们忽略存储答案的空间,额外的排序的空间复杂度为 O(logN)。然而我们修改了输入的数组 nums,在实际情况下不一定允许,因此也可以看成使用了一个额外的数组存储了 nums 的副本并进行排序,空间复杂度为 O(N)。

笔者小记:

1、指针移动(或者说指针的增减操作)在 C++ 中的时间复杂度是 O(1),即常数时间复杂度,相比单层循环时间复杂度为O(N)多使用指针思想代替for循环可以大大降低时间复杂度,避免时间超限。

2、【不重复】对于每一重循环而言,相邻两次枚举的元素不能相同,否则也会造成重复。在循环层内初始就增加一些“限制条件语句”,可以避免再对存储的数据结构结果进行去重操作(增加繁琐性)。


http://www.kler.cn/a/526688.html

相关文章:

  • SSM开发(三) spring与mybatis整合(含完整运行demo源码)
  • 数据分析系列--⑤RapidMiner进行关联分析(中文数据案例)
  • Python设计模式 - 组合模式
  • 安装zsh并美化
  • vue3中customRef的用法以及使用场景
  • SQL注入漏洞之高阶手法 宽字节注入以及编码解释 以及堆叠注入原理说明
  • 20-30 五子棋游戏
  • 【2024年华为OD机试】 (B卷,100分)- 乘坐保密电梯(JavaScriptJava PythonC/C++)
  • 如何用大语言模型做一个Html+CSS+JS的词卡网站
  • WINDOWS安装eiseg遇到的问题和解决方法
  • day1-->day7| 机器学习(吴恩达)学习笔记
  • FLTK - FLTK1.4.1 - 搭建模板,将FLTK自带的实现搬过来做实验
  • 知识管理平台在数字经济时代推动企业智慧决策与知识赋能的路径分析
  • 全面认识了解DeepSeek+利用ollama在本地部署、使用和体验deepseek-r1大模型
  • 【仓颉】仓颉编程语言Windows安装指南 配置环境变量 最简单解决中文乱码问题和其他解决方案大全
  • 360嵌入式开发面试题及参考答案
  • 【Linux指令/信号总结】粘滞位 重定向 系统调用 信号产生 信号处理
  • 【开源免费】基于Vue和SpringBoot的医院资源管理系统(附论文)
  • Python的那些事第六篇:从定义到应用,Python函数的奥秘
  • 将多目标贝叶斯优化与强化学习相结合用于TinyML
  • 2024年数据记录
  • 【16届蓝桥杯寒假刷题营】第1期DAY2
  • 创建 priority_queue - 进阶(内置类型)c++
  • React 低代码项目:项目创建
  • .Net / C# 分析文件编码 并将 各种编码格式 转为 另一个编码格式 ( 比如: GB2312→UTF-8, UTF-8→GB2312)
  • Vue中的动态组件是什么?如何动态切换组件?