【算法指南】有效三角形的个数 | 三数之和 | 四数之和

252 阅读13分钟

力扣611. 有效三角形的个数

Problem: 611. 有效三角形的个数

题目分析

首先我们来分析一下本题的思路

  • 看到题目中给出的示例

1.jpg

  • 题目的意思很简单,就是将给到的数字去做一个组合,然后看看这三条边是否可以构成三角形。那判断的方法不用我说,相信大家如果读过小学的话应该都明白的,即三角形两边之和大于第三边则可以构成三角形

2.jpg

其实对于三角形的判断我们无需去判别三次,而是可以进行取巧👇

  • 看到这里,我们可以去找出三个数中较大的那个数,然后只需去比较a + b > c即可,而对于a + c > bb + c > a则不用去进行一个比较,因为此时[c]已经是最大的了,那再去加上a或者b的话一定会更大,所以无需去做比较
  • 不过呢,这需要我们先找出最大的那个数,即要去做一个排序的操作进行优化

3.jpg

  • 上面的这种思路对我们本题的解法很有帮助,望读者先行理解

讲解算法原理

然后我们根据上面的思路来分析一下本题的算法原理

  1. 暴力枚举
  • 首先的话第一种,大家都能想到的就是【暴力枚举】,下面的话我写了一手伪代码,也就是通过三层的for循环,去一一进行枚举的操作。不过呢这很明显,时间复杂度为 O(n3)O(n^3) 一定会造成超时。
for(int i = 0;i < n; ++i)
{
    for(int j = i + 1;j < n; ++j)
    {
        for(int k = j + 1;k < n; ++k)
        {
            check(nums[i], nums[j], nums[k]);
        }
    }
}

我们可以来看一下运行后的结果

4.jpg

再来试着分析一下复杂度:

  • 对于check()函数而言,如果我们还是使用上面判断三次的方式来看三条边是否可以构成三角形,那么最终因为外层的循环就会使得时间复杂度到达 3O(n3)3O(n^3)
  • 但是呢,如果我们使用的是取巧的办法,那需要先去使用【sort】做一个排序。只判断一次的话最终的时间复杂度为 O(NlogN+N3)O(NlogN + N^3)
  • 那么后者一定是要比前者的复杂度来得低的,所以我们要考虑到换一种解法

  1. 利用单调性,结合双指针进行求解

接下去我就来介绍一下【双指针】这种解法

  • 首先的话,上面说到过了,我们需要将整个数据先去做一个优化,使其呈现升序的样子,接下去呢我们要先拿到这个最大的数作为[c];然后呢我们拿左指针left从左向右进行遍历,拿右指针right从右往左开始遍历
  • 那我们现在看到a + b = 11 > c,那么就可以利用我们上面所介绍的这种思想,无需再去多判断

5.jpg

  • 因为我们在一开始做了优化,数据是呈现升序排列的。例如像下面这里2 + 93 + 94 + 9等等这些都是要比10要来得大的,那其实我们根本无需再去判断这些数据,从【2】~【5】这5组数据均可以组成三角形,那此时如果我们要得到这个5的话只需要让right - left即可
  • 那既然前面的数据都是与9进行结合,那这个9的话我们就使用完了,接下去让right--进行下一个数据的判断即可

6.jpg

  • 接下去我们再来看第二种,此时我们可以看到2 + 5 = 7 < 10,那么此时我们可以继续去观察从【2】~【5】的这一堆数,它们一定是比5来得小的,那我们也无需再去多做比较了,对于这个【2】来说我们就可以舍弃了

7.jpg

所以我们在来总结一下上面这种解法

  1. 先固定最大的数
  2. 在最大数的左区间内,使用双指针算法,快速统计出符合要求的三元组个数

8.jpg

复杂度

  • 时间复杂度:

来说一下双指针这种解法的时间复杂度, 首先的话我们要在N个数内找到那个最大的数,然后的话还要使用【双指针】去遍历从0 ~ n - 1这N - 1个数,那么时间复杂度即为 O(N2)O(N^2)

  • 空间复杂度:

对于空间复杂度来说,没有去开辟任何的空间,所以为 O(1)O(1)

Code

来展示一下最终的代码

class Solution {
public:
    int triangleNumber(vector<int>& nums) {
        // 1.优化
        sort(nums.begin(), nums.end());

        // 2.利用双指针解决问题
        int ret = 0, n = nums.size();
        for(int i = n - 1; i >= 2; --i)     // 先固定最大的数
        {
            // a : nums[left]
            // b : nums[right]
            // c : nums[i]
            int left = 0, right = i - 1;
            while(left < right)
            {
                if(nums[left] + nums[right] > nums[i])
                {
                    ret += right - left;
                    right--;
                }
                else
                {
                    left++;
                }
            }
        }
        return ret;
    }
};

力扣15. 三数之和

Problem: 15. 三数之和

题目解析

首先我们来分析一下本题的思路

  • 题目说到要我们在一个整数数组中去寻找三元组,而且呢这三个数字所相加的和为0,而且呢这三个数的位置还要不一样
  • 我们以这个示例1为例来看看,我列出了3种可能性,分别是[-1, 0, 1][-1, 2, -1][0, 1, -1],不过呢我们仔细看这个题意中的概念,又可以知道这些三元组还不可以重复,那么第一个和第三个我们就需要考虑到去重

1.jpg

💬 但是要如何去求解本题呢,怎么去找出这些三元组呢?找出之后又该如何去做一个去重的操作呢?我们马上进行算法原理分析

算法原理分析

接下去我将通过分析算法原理来讲解本题该如何去进行求解

排序 + 暴力枚举 + set去重

首先第一种,我们能想到的一定是暴力枚举

  • 不过在这之前呢,我们首先要对这些整型数据去做一个排序的操作,在排完序之后我们通过暴力枚举的方式在找出两个三元组,可以看出这两个三元组的是重复的,所以我们要去做一个去重的操作
  • 对于这步操作的话如果读者有学习过C++中的unordered_set和Java中的Hashset的话就可以知道如果我们将重复的数据扔到集合中的话就会去做一个自动去重的操作

2.jpg

💬 对于上面这种解法的代码就不给出了,读者可以自己去写写看

排序 + 单调性 + 双指针划分思想

我们主要的话是来讲这种解法

  • 对于比较优的解法,我们首先应该要考虑到的就是【双指针】,如果有做过 和为s的两个数字,那么就可以知道使用双指针来进行求解的话会事半而功倍
  • 之前我们是利用双指针去求解两个的和,那现在要求解三个数的和呢?其实也是一样的,我们看到下面的这张图,首先的话我们将第一个数去做一个固定,然后呢让left指针指向i + 1的位置,right指针指向nums.size() - 1的位置,然后通过这两个指针的遍历去寻找不同的三元组即可

3.jpg

💬 那有同学说,这在遍历的过程中要怎么去进行判断呢?

  • 其实的话这很简单,因为我们首先固定了一个数a,因为【三数之和需要为0】,那么接下去在遍历后面的这两个数的时只需要判断它们相加之和为-a即可,也就是这个数a的相反数

4.jpg

所以我们来梳理一下这个逻辑

5.jpg

  • 再来模拟一下这个过程,具体的双指针判断逻辑就不解释了,读者可以去看看上面的那道题,然后再通过看下面的代码来理解这个匹配的过程

6.jpg

  • 最后呢我们可以看到这三个数被找到了,那此时就可以将其放入结果集中去

所以接下去呢我们还需要去处理一些【细节】方面的问题

  1. 首先第一个的话就是让这个leftright指针还需要继续去做移动的操作,因为我们是要找到这组数据中所有的三元组
left++;right--;
  1. 第二个的话就是我们在上面所说到过的话需要考虑到的去重问题,那要怎么去重呢?
  • 其实的话这很简单,当在执行完left++之后我们需要去判断当前所指的数字和上一个数字是否相同,如果出现了相同情况的话,就让left++,跳过这个数。对于上面的这段逻辑我们需要放在【while】循环中执行,因为可能出现很多连续相同的数据
while(nums[left - 1] == nums[left]) left++;
  • 那对于右侧的这个数也是一样,不过是当前的这个数和后一个数去进行比较
while(nums[right] == nums[right + 1]) right--;
  • 除了这两块的去重逻辑,其实还有一个地方也需要考虑到去重的问题,那就是外部读这个数a,读者可以思考一下,第一次我们已经对这个【-4】做了一轮的判断,那此时如果再去做一轮的判断的话就是多次一举了

7.jpg

  • 所以呢我们还需要对这个i做去重的逻辑,和left是一致的
// i的去重【防止越界】
i++;
while(nums[i] == nums[i - 1])  i++;

你认为这么就完了吗,那也太小瞧这道题了,毕竟也是力扣中的困难题

  • 我们来看一下下面的这种情况,所有的数字都是0,那么在第一次遍历的时候就天然满足了,于是就进入继续查找的逻辑,但是呢因为ilr所指的数据都是一样的,所以在进行【while】循环的时候会一直地进行移动,此时就会造成l超过r或者r超过l的情况,不仅如此,i在遍历的时候会碰到类似的情况

8.jpg

所以我们应该把去重的逻辑做一个修改,可以起到防止越界的效果✔

left++;right--;
// left与right的去重【防止越界】
while(left < right && nums[left - 1] == nums[left]) left++;
while(left < right && nums[right] == nums[right + 1]) right--;
// i的去重【防止越界】
i++;
while(i < n && nums[i] == nums[i - 1])  i++;

💬 以上就是【双指针】算法原理的分析,读者可以在阅读完之后试着自己写写看代码,很多细节的处理对代码能力的提升很有帮助

复杂度

  • 时间复杂度:

因为我们在遍历的时候, 每次去固定一个数a,然后下标为i,接着从[i + 1, n - 1]的地方进行遍历,复杂度为: O(n2)O(n^2)

  • 空间复杂度:

因为没有去堆区申请任何空间,所以空间复杂度为 O(1)O(1)

Code

以下是整体的代码展示

class Solution {
public:
    vector<vector<int>> threeSum(vector<int>& nums) {
        // 1.排序
        sort(nums.begin(), nums.end());

        // 2.利用双指针解决问题
        vector<vector<int>> result;
        int n = nums.size();
        int i = 0;
        while(i < n)    // 固定a
        {
            // 若发现a > 0的话,则后面的nums[left] + nums[right]不会 < 0
            if(nums[i] > 0) break;  
            int left = i + 1, right = nums.size() - 1, target = -nums[i];
            while(left < right)
            {
                int sum = nums[left] + nums[right];
                if(sum < target){
                    left++;
                }else if(sum > target){
                    right--;
                }else{
                    // 找到了,放入结果集
                    result.push_back({nums[i], nums[left], nums[right]});
                    left++;right--;
                    
                    // left与right的去重【防止越界】
                    while(left < right && nums[left - 1] == nums[left]) left++;
                    while(left < right && nums[right] == nums[right + 1]) right--;
                }
            }
            // i的去重【防止越界】
            i++;
            while(i < n && nums[i] == nums[i - 1])  i++;
        }
        return result;
    }
};

力扣18. 四数之和

Problem: 18. 四数之和

解题思路

讲述看到这一题的思路

  • 首先我们来分析一下本题的思路:这题和我们之前所讲过的一题叫做 三数之和,与本题非常得类似,如果没有做过的扣友可以先去做做看
  • 那我们来分析一下本题的题意,题目中说到会给我们一个整数数组nums和一个目标值target,这和三数之和不同的是这四个数相加之和要为target,而且这个四元组不可以重复
  • 那我们来看示例1,从[1, 0, -1, 0, -2, 2]这几个数里面找到了三组符合条件的四元组

1.jpg

💬 那具体这个寻找的过程是怎样的呢,我们一起看下去

算法原理分析

马上我们就来说说这个算法的实现原理

对于【暴力解法】我在 三数之和 里面已经说过了,肯定会造成超时的现象,所以我直接来介绍双指针的解法:

  • 首先我们要先固定的就是最前面的这个数a,接下去的话就是在后面的这段区间中找到三个数,它们的和为target - a

2.jpg

  • 但是呢三个数相加我们无法去做到很好的定位,因为我们是要使用双指针来解决这道题,所以呢对于后面的小区间我们还要再做一个划分:继续定位出来最前面的一个数作为数b,然后在后面的区间中找到两个数之和等于target - a - b

3.jpg

  • 但是呢我们除了考虑一些基本的要求外,还需要去考虑到去重和越界的问题,在这里我们不仅是要考虑到leftright,而且还需要考虑到在外侧所固定的数a和数b

4.jpg

💬 所以,光就这么分析来看,本题的难度是要大于【三数之和】的,所以读者可以试着自己写写看代码,锻炼提升一下自己💪

复杂度

  • 时间复杂度:

对于时间复杂度, 最外层固定一个数a,然后遍历; 第二层遍历一个数b,然后接着遍历,在最内层呢我们通过while循环继续去做遍历,那么这个时间复杂度即为 O(n3)O(n^3)

  • 空间复杂度:

因为没有开出任何额外的空间,所以空间复杂度为 O(n)O(n)

Code

class Solution {
public:
    vector<vector<int>> fourSum(vector<int>& nums, int target) {
        // 1.排序
        sort(nums.begin(), nums.end());

        // 2.利用双指针解决问题
        int n = nums.size();
        vector<vector<int>> result;
        for(int i = 0;i < n; )      // 固定a
        {
            // 利用三数之和进行求解
            for(int j = i + 1;j < n; )      // 固定b
            {
                int left = j + 1, right = n - 1;
                long long targetSum = (long long)target - nums[i] - nums[j];
                while(left < right)
                {
                    int sum = nums[left] + nums[right];
                    if(sum < targetSum){
                        left++;
                    }else if(sum > targetSum){
                        right--;
                    } 
                    else{
                        // 将找到的数据放入结果集中
                        result.push_back({nums[i], nums[j], nums[left++], nums[right--]});

                        // left和right去重
                        while(left < right && nums[left] == nums[left - 1]) left++;
                        while(left < right && nums[right] == nums[right + 1]) right--;
                    }
                }
                // j去重
                j++;
                while(j < n && nums[j] == nums[j - 1])  j++;
            }
            // i去重
            i++;
            while(i < n && nums[i] == nums[i - 1])  i++;            
        }
        return result;
    }
};

在这里插入图片描述