611. 有效三角形的个数
问题描述
给定一个包含非负整数的数组 nums
,返回可以形成有效三角形的三元组数量。
有效三角形定义:三个正数能构成三角形,当且仅当任意两边之和大于第三边。
示例:
输入: nums = [2,2,3,4]
输出: 3
解释: 有效组合为:
- (2,3,4): 2+3>4, 2+4>3, 3+4>2
- (2,3,4): 另一个2
- (2,2,3): 2+2>3, 2+3>2, 2+3>2
算法思路
核心思想:排序 + 双指针优化
- 排序预处理:将数组升序排列
- 固定最长边:枚举每个位置作为三角形的最长边
- 双指针查找:在左侧找两个较短边,使其和大于最长边
关键:
- 三角形成立条件:
a + b > c
(其中c
是最长边) - 排序后,只需验证
nums[i] + nums[j] > nums[k]
- 固定
k
为最长边,用双指针在[0, k-1]
范围内找满足条件的i,j
优化:
- 排序后剪枝:一旦
nums[i] + nums[j] > nums[k]
,则j
到k-1
的所有位置都满足 - 双指针移动:根据和的大小决定指针移动方向
代码实现
import java.util.Arrays;
class Solution {
/**
* 计算能构成有效三角形的三元组数量
*
* @param nums 非负整数数组
* @return 有效三角形的数量
*/
public int triangleNumber(int[] nums) {
// 边界情况:少于3个元素无法构成三角形
if (nums == null || nums.length < 3) {
return 0;
}
// 第一步:排序,便于后续双指针操作
Arrays.sort(nums);
int count = 0; // 记录有效三角形数量
// 第二步:枚举最长边的位置k(从2开始,因为需要至少3个元素)
for (int k = 2; k < nums.length; k++) {
int left = 0; // 左指针,指向最短边
int right = k - 1; // 右指针,指向次长边
// 使用双指针在[0, k-1]范围内查找满足条件的组合
while (left < right) {
// 检查两个较短边之和是否大于最长边
if (nums[left] + nums[right] > nums[k]) {
// 如果成立,则[left, right-1]到right的所有组合都成立
// 因为数组已排序,nums[i] + nums[right] >= nums[left] + nums[right] > nums[k]
count += right - left;
// 尝试找更多组合,移动右指针
right--;
} else {
// 和不够大,需要更大的较短边
left++;
}
}
}
return count;
}
}
算法分析
时间复杂度:O(n²)
- 排序:O(n log n)
- 外层循环:O(n) - 枚举最长边
- 内层双指针:O(n) - 每个元素最多被访问一次
- 总体:O(n²)
空间复杂度:O(1)
- 只使用常数额外空间
- 排序使用的空间不计入(原地排序)
对比暴力解法:
方法 | 时间复杂度 | 空间复杂度 |
---|---|---|
暴力三重循环 | O(n³) | O(1) |
排序+双指针 | O(n²) | O(1) |
算法过程
nums = [2,2,3,4]
:
1:排序
原始数组:[2,2,3,4] → 已排序
2:枚举最长边
k=2(nums[k]=3):
- left=0, right=1
- nums[0]+nums[1]=2+2=4 > 3 → 满足
- count += right-left = 1-0 = 1
- right-- → right=0
- left >= right,退出
- 贡献:1个三角形(2,2,3)
k=3(nums[k]=4):
- left=0, right=2
- nums[0]+nums[2]=2+3=5 > 4 → 满足
- count += right-left = 2-0 = 2
- right-- → right=1
- nums[0]+nums[1]=2+2=4 = 4 → 不满足(必须严格大于)
- left++ → left=1
- left >= right,退出
- 贡献:2个三角形(2,3,4)和(2,3,4)
总计:1 + 2 = 3
测试用例
public static void main(String[] args) {
Solution solution = new Solution();
// 测试用例1:标准示例
int[] nums1 = {2,2,3,4};
System.out.println("Test 1: " + solution.triangleNumber(nums1)); // 3
// 测试用例2:无法构成三角形
int[] nums2 = {1,2,3};
System.out.println("Test 2: " + solution.triangleNumber(nums2)); // 0
// 1+2=3,不大于3
// 测试用例3:全相等
int[] nums3 = {3,3,3,3};
System.out.println("Test 3: " + solution.triangleNumber(nums3)); // 4
// C(4,3)=4种组合,都满足3+3>3
// 测试用例4:小数组
int[] nums4 = {1};
System.out.println("Test 4: " + solution.triangleNumber(nums4)); // 0
// 测试用例5:包含0
int[] nums5 = {0,1,2,3};
System.out.println("Test 5: " + solution.triangleNumber(nums5)); // 0
// 0不能作为边长
// 测试用例6:大数组
int[] nums6 = {2,2,4,4,5,6};
System.out.println("Test 6: " + solution.triangleNumber(nums6)); // 11
// 测试用例7:降序输入
int[] nums7 = {4,3,2,2};
System.out.println("Test 7: " + solution.triangleNumber(nums7)); // 3
// 排序后与test1相同
// 测试用例8:边界值
int[] nums8 = {1,1,1};
System.out.println("Test 8: " + solution.triangleNumber(nums8)); // 1
}
关键点
-
排序:
- 确保
nums[k]
是最大边 - 只需验证
a + b > c
- 其他不等式自动满足(
a + c > b
,b + c > a
)
- 确保
-
双指针:
left
从0开始,right
从k-1
开始- 当
nums[left] + nums[right] > nums[k]
时,left
到right-1
的所有位置都满足 - 因此可以一次性添加
right - left
个组合
-
指针移动策略:
- 和太大:
right--
(尝试更小的次长边) - 和太小:
left++
(需要更大的最短边)
- 和太大:
-
边界处理:
- 数组长度小于3:返回0
- 包含0或负数:排序后0会在前面,自然被排除
常见问题
-
为什么只需验证一个不等式?
排序后nums[k] ≥ nums[right] ≥ nums[left]
,所以:nums[left] + nums[k] ≥ nums[left] + nums[right] > nums[k] ≥ nums[right]
→ 成立nums[right] + nums[k] ≥ nums[left] + nums[k] > nums[right]
→ 成立
只需验证nums[left] + nums[right] > nums[k]
-
能否用哈希表优化?
不适合。三角形问题具有顺序性,双指针利用了排序后的单调性。 -
如何处理重复元素?
算法自然处理重复元素,每个位置被视为独立元素。 -
算法的鲁棒性:
已处理:- 空数组
- 小数组
- 包含0
- 无解情况
-
是否有数学优化?
对于特定分布的数据(如等差数列),可能有数学公式,但通用解法中双指针最优。