Leetcode 三数之和

解题思路:

  1. 排序数组:首先对数组进行排序,以便使用双指针技术来查找三元组。
  2. 双指针法:在遍历数组时,遍历固定三元组的第一个元素,然后使用双指针(分别指向剩下数组的头和尾并相向而行,因为下标两两不同)来寻找另外两个元素,使三者之和为零。
  3. 去重处理:为了避免重复的三元组,跳过重复的元素。

为什么需要对原始数组进行排序?

在这道题中,对数组进行排序是为了简化解决过程,并有效避免重复结果。排序在解决这类问题时的作用是不可忽视的。下面详细解释为什么排序是必要的,以及不排序会遇到什么样的问题。

1. 简化双指针操作

排序的一个主要目的是方便使用双指针 技巧。在经过排序的数组中,我们可以利用双指针分别从数组的两端开始,来寻找和为 0 的三元组。 排序后,双指针可以轻松地通过调整指针(根据当前和的大小)来决定向哪边移动,从而减少时间复杂度

如果不进行排序,双指针策略将无法应用,因为在无序的数组中,无法简单判断是否应该移动左指针还是右指针来缩小差距 。我们会陷入遍历每一个组合的情况,这样会使时间复杂度增加至 O ( n 3 ) O(n^3) O(n3),性能远远不如 O ( n 2 ) O(n^2) O(n2) 的双指针方法。

2. 去重处理

排序的另一个重要作用是去除重复的三元组。排序后,相同的数字会排在一起,因此在遍历时很容易跳过重复的元素。这个去重过程是通过在遍历中跳过连续相同的元素来实现的。

如果数组没有排序,想要去重就需要额外的数据结构(如哈希表)来存储已经出现的三元组,并且需要进行额外的查找操作,这样就会增加时间和空间的复杂度。

3. 使用排序的例子

假设我们有一个数组 [-1, 0, 1, 2, -1, -4],如果不排序,我们会发现所有可能的三元组(假设通过暴力搜索),会包含:

[-1, 0, 1]
[0, -1, 1]
[-1, 2, -1]
[-4, 1, 2]

可以看到,有些三元组(如 [-1, 0, 1][0, -1, 1])在不排序的情况下会被计算多次。而排序后,我们可以确保相同的数字只出现一次,并且可以跳过重复的三元组,得到的结果更加简洁。

4. 避免 O ( n 3 ) O(n^3) O(n3) 的暴力解法

不排序的情况下,你可以通过三重循环遍历所有的三元组来解决问题,即暴力解法,其时间复杂度是 O ( n 3 ) O(n^3) O(n3)。在实际场景中,如果输入数组较大,这种方法的效率会非常低。通过排序并使用双指针方法,我们可以将时间复杂度优化到 O ( n 2 ) O(n^2) O(n2)。

总结:

虽然不排序也可以通过某些方法找到和为 0 的三元组,但排序有如下显著优势:

  1. 使用双指针减少时间复杂度到 O ( n 2 ) O(n^2) O(n2)。
  2. 简化了去重逻辑,避免了复杂的查重操作。
  3. 排序后方便高效地跳过重复元素。

因此,排序在这道题中是必要的,它使得解决方案更加高效和简单。

注意点:

不仅需要在固定三元组第一个元素时进行脱重处理,同时也需要在双指针移动时,匹配到三元组时对双指针指向的元素进行脱重处理!!

固定三元组第一个元素进行脱重处理时,从第二个元素开始进行脱重判断

cpp 复制代码
class Solution {
public:
    vector<vector<int>> threeSum(vector<int>& nums) {
        vector<vector<int>> result; //创建一个存储结果的向量
        sort(nums.begin(), nums.end()); //先对原始数组进行排序

        for(int i = 0; i < nums.size(); i++) {
            //然后首先进行去重处理,从第二个元素开始进行判断
            if(i > 0 && nums[i] == nums[i-1]) continue;
            
            //初始化双指针
            int left = i + 1;
            int right = nums.size() - 1;
            
            //初始完双指针之后,两个指针开始移动,并且移动需要有终止条件,那就是左指针小于右指针
            while(left < right) {
                int sum = nums[left] + nums[right] + nums[i];
                if(sum == 0) {
                    result.push_back({nums[i], nums[left], nums[right]});
                    //这里再次需要去重处理!
                    while(left < right && nums[left] == nums[left+1]) left++;
                    while(left < right && nums[right] == nums[right-1]) right--;
                    left++;//左指针指向的下一个值增大
                    right--;//右指针指向的下一个值减小
                }else if(sum < 0) {
                    left++;
                    //这里之所以左指针右移, 是因为数组已经经过排序了, 所以右指针已经是当前剩余元素中的最大值了
                }else {
                    right--;
                    //这里之所以右指针左移, 是因为数组已经经过排序了, 所以左指针已经是当前剩余元素中的最小值了
                }
            }
        }
        //返回结果
        return result;
    }
};

时间复杂度:

  • 排序的时间复杂度是 O ( n log ⁡ n ) O(n \log n) O(nlogn)。
  • 遍历数组和使用双指针查找的时间复杂度是 O ( n 2 ) O(n^2) O(n2)。
  • 因此总的时间复杂度为 O ( n 2 ) O(n^2) O(n2)。

空间复杂度:

  • 排序算法使用 O ( log ⁡ n ) O(\log n) O(logn) 的空间,此外只有常数级别的额外空间,因此空间复杂度为 O ( n ) O(n) O(n)(不考虑输出结果的空间)。
相关推荐
逝去的秋风5 分钟前
【代码随想录训练营第42期 Day57打卡 - 图论Part7 - Prim算法与Kruskal算法
算法·图论·prim算法
QXH20000014 分钟前
数据结构—双向链表
c语言·数据结构·算法·链表
旺小仔.33 分钟前
【数据结构篇】~排序(1)之插入排序
c语言·数据结构·算法·链表·性能优化·排序算法
绎岚科技1 小时前
深度学习自编码器 - 随机编码器和解码器篇
人工智能·深度学习·算法·机器学习
jingling5551 小时前
后端开发刷题 | 数字字符串转化成IP地址
java·开发语言·javascript·算法
云边有个稻草人1 小时前
【刷题】Day5--数字在升序数组中出现的次数
开发语言·笔记·算法
ymchuangke1 小时前
评价类——熵权法(Entropy Weight Method, EWM),完全客观评价
人工智能·python·算法·机器学习·数学建模
Crossoads2 小时前
【数据结构】排序算法---希尔排序
c语言·开发语言·数据结构·算法·排序算法
skaiuijing2 小时前
巧用二级指针
c语言·开发语言·算法·架构·操作系统
jingling5552 小时前
后端开发刷题 | 最长上升子序列
java·开发语言·数据结构·后端·算法·动态规划