算法训练-力扣.611有效三角形的个数(贪心)

#611有效三角形的个数. - 力扣(LeetCode)

这题又是一道一眼就能看出属于贪心的题型。但是解题需要用到二分查找的知识。如果还不会二分查找的小伙伴可以先不用看这题。好,废话不多,我们开始!

题目描述

给定一个包含非负整数的数组 nums ,返回其中可以组成三角形三条边的三元组个数。

示例 1:

输入: nums = [2,2,3,4]
输出: 3
解释:有效的组合是: 
2,3,4 (使用第一个 2)
2,3,4 (使用第二个 2)
2,2,3

示例 2:

输入: nums = [4,2,3,4]
输出: 4

提示:

  • 1 <= nums.length <= 1000
  • 0 <= nums[i] <= 1000

解题

思路

相信不少小伙伴已经马上想到一个公式:三角形两边之和大于第三边。也就是a + b > c,没错,那么我们解题关键也就是找出这其中的a,b,c。我们不妨直接来个双层for循环,遍历数组中的元素,假设外层for循环遍历的是边a,内层for循环遍历的是边b,那么我们依据题意在数组索引为

b < index <= nums.length的范围内找出满足小于a + b的数即可,将题目中的数据代入a + b > c也就是: nums[i] + nums[j] > nums[index],i和j分别是两层for循环的变量,index就是我们要找的,找到满足条件的index让结果+1即可。所以我们整体解题思路就是:遍历边a和边b,寻找符合条件的边c

对时间复杂度的优化(一)

其实思路到这里这道题也就解出来了,但是如果就按照这个思路写,那么时间复杂度就为O(n^{3}),是非常恐怖的,我们必须要进行优化。那么我们能发现最容易下手的地方就是找满足条件的index这一层循环。我们为什么不用二分进行查找呢?我们先对数组nums进行排序,保持边a和边b由外面两层for循环得出,我们只需要找到最后一个比a + b小的数就可以知道符合三角形的第三边的数量。而这就是这道题贪心所在。因为我们只要找到最后一个比a + b小的数,那么比它小的数肯定也符合a + b > c这个公式。

例如:[1,2,4,5,6,8,9],当遍历到i=2,j=3的时候,三角形的边a和边b的长度分别为4和5,那么我们只需要找到最后一个比4+5=9小的数的位置即可,在这个数组中,最后一个比9小的数就是8,下标为5,那么我们就可以知道有 index-j个三角形符合,也就是两个三角形符合,分别为[4, 5, 6]和[4, 5, 8]。

现在就变成了如何用二分查找找到最后一个比a+b小的数?

下面展示一种写法:

①跟正常二分查找一样,进入while循环进行二分查找

②结束while循环,现在有两种情况,mid指向 最后一个小于a+b的数 或者指向 第一个大于a+b的数。例如:[2 ,4 ,6 ,8],目标为5,这时候二分查找结束,mid指向的是6,也就是第一个大于a+b的情况

③如果是最后一个小于a+b的数我们直接获取 mid - j就是我们想要的结果,如果是第一个大于a+b的数,那么mid--然后获取mid - j就是结果。

注意特殊情况:依据题目,数组中可能会出现0,我们知道是不存在边长为0的三角形,所以进入双层for循环的第一步判断边长是否为0,是0直接跳过本次循环即可。

那么我们最终可以得到代码:

public static int triangleNumber(int[] nums) {
    Arrays.sort(nums);
    int res = 0;
    for (int i = 0;i < nums.length - 2;++i){//第一层for循环遍历边a,范围为 [0,nums.length-2],就是遍历到倒数第三个数
        if (nums[i] == 0)//边a边长为0,跳过本次循环
            continue;
        for (int j = i + 1;j < nums.length - 1;++j){//第二层for循环遍历边b,范围为 [i + 1,nums.length-1],就是遍历到倒数第二个数
            if (nums[j] == 0)//边b边长为0,跳过本次循环
                continue;
            
            //通过二分查找,找到最后一个小于nums[i] + nums[j]的数的坐标
            int left = j + 1,right = nums.length - 1;
            int mid = (left + right) / 2;
            while (left <= right){
                if (nums[mid] < nums[i] + nums[j]){
                    left = mid + 1;
                    mid = (left + right) / 2;
                } else {
                    right = mid - 1;
                    mid = (left + right) / 2;
                }
            }
            //这时候的mid不一定是最后一个小于nums[i] + nums[j]的数的索引,可能是第一个大于nums[i] + nums[j]的数的索引
            res += nums[mid] >= nums[i] + nums[j] ? (mid - j - 1) : (mid - j);
        }
    }
    return res;
}

时间复杂度就被我们降到了O(n^{2}·\log_{}n),还算行。毕竟数据量才不过一千个。

很开心,又AC了一道题!其实这题完全可以将时间复杂度优化到O(n^{2})!感兴趣的同学可以看一下:

对时间复杂度的优化(二)

对于上面这种方法,我们是遍历两边找一边,我们如果遍历一边找两边呢?

先说结果:为什么采用遍历一边找两边就能将时间复杂度优化到O(n^{2})。遍历一条边用一层for循环,遍历n-2遍,遍历两边也只需要一个左右双指针,遍历n-3遍,所以最终时间复杂度为O(n^{2})。

不难发现因为逻辑的更改,直接弃用了二分查找。我们看一下解题步骤:

①一层for循环,从右向左遍历数组(i从nums.length-1遍历到2),这次遍历的是三角形的长边,边c。至于为什么从右到左而不是从左到右,在文章末尾会有解释。

②定义双指针,左指针从0开始,右指针从 i - 1开始。左右指针分别对应着三角形边a和边b,进入while循环,循环条件是left<rigth,也就是左指针小于右指针。

③若nums[left] + nums[right] > nums[i],也就满足a + b > c,那么说明b、c边固定下的情况下,a边从nums[left]到nums[right]-1范围内都是满足条件的,所以结果直接+(right-left)。然后右指针向左移动,尝试缩小边b。

    若nums[left] + nums[right] <= nums[i],说明不满足a + b > c,我们应该将最小的边尝试增大,也就是右移左指针。

最终代码如下:

最终代码

public static int triangleNumber(int[] nums) {
        Arrays.sort(nums);
        int res = 0;
        for (int i = nums.length - 1;i >= 2;--i){//一层for循环,遍历的是边c
            int left = 0,right = i - 1;
            while (left < right){
                if (nums[left] + nums[right] > nums[i]){
                    //从left到right的元素都符合两边之和大于第三边
                    res += (right - left);
                    --right;
                } else {
                    //两边之和不大于第三边
                    ++left;
                }
            }
        }
        return res;
    }

解疑

在优化二的第一步中,为什么我们采取从右至左遍历长边c而不采取从左至右遍历边a?

原因很简单,就拿nums = [2 ,2 ,3 ,4]举例,当外层for循环指向下标为0,也就是边a长度等于2,左指针下标为1,右指针下标为nums.length - 1,边b c长度分别为2和4,这时候明显是不符合三角形公式的。按照上面的步骤,我们应该右移左指针,但是很明显,左移右指针也可以解决,也就是说:这时候的左右指针无论移动哪个,效果是一样的(可以理解为不管移动哪个指针结果都等效于增大a + b),这显然不是合格的双指针,我们应该保证左右指针移动后产生的效果不一样。如最终代码中的双指针,左指针移动的效果是增加a+b,右指针移动的效果是减小a+b。

  • 29
    点赞
  • 28
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值