这题又是一道一眼就能看出属于贪心的题型。但是解题需要用到二分查找的知识。如果还不会二分查找的小伙伴可以先不用看这题。好,废话不多,我们开始!
题目描述
给定一个包含非负整数的数组 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(),是非常恐怖的,我们必须要进行优化。那么我们能发现最容易下手的地方就是找满足条件的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(·
),还算行。毕竟数据量才不过一千个。
很开心,又AC了一道题!其实这题完全可以将时间复杂度优化到O()!感兴趣的同学可以看一下:
对时间复杂度的优化(二)
对于上面这种方法,我们是遍历两边找一边,我们如果遍历一边找两边呢?
先说结果:为什么采用遍历一边找两边就能将时间复杂度优化到O()。遍历一条边用一层for循环,遍历n-2遍,遍历两边也只需要一个左右双指针,遍历n-3遍,所以最终时间复杂度为O(
)。
不难发现因为逻辑的更改,直接弃用了二分查找。我们看一下解题步骤:
①一层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。