1. 题目解析:适龄好友请求问题
这道题目描述了一个社交媒体平台上用户之间发送好友请求的规则。给定一个包含n个用户年龄的数组ages,我们需要计算在这些用户之间会产生多少条好友请求。
1.1 题目核心规则
题目给出了三个不发送好友请求的条件,只有当这三个条件都不满足时,用户x才会向用户y发送好友请求:
- 年龄太小:ages[y] ≤ 0.5 × ages[x] + 7
- 年龄太大:ages[y] > ages[x]
- 特殊年龄限制:ages[y] > 100且ages[x] < 100
经过分析可以发现,第二个条件实际上已经包含了第三个条件。因此我们可以简化为:当且仅当满足以下两个条件时,用户x会向用户y发送好友请求:
- ages[y] > 0.5 × ages[x] + 7
- ages[y] ≤ ages[x]
1.2 数学简化
将这两个条件合并,可以得到:
0.5 × ages[x] + 7 < ages[y] ≤ ages[x]
由于年龄都是整数,我们可以将其改写为:
⌊ages[x]/2⌋ + 8 ≤ ages[y] ≤ ages[x]
这里有一个重要的边界情况:当ages[x] < 15时,⌊ages[x]/2⌋ + 8 > ages[x],这意味着不会有任何y能满足这个条件。因此,我们只需要考虑年龄≥15的用户。
2. 解法一:排序+二分查找
2.1 算法思路
这个解法的核心思想是:
- 首先对年龄数组进行排序
- 对于每个年龄≥15的用户,计算其可能发送好友请求的年龄范围[lower, upper]
- 使用二分查找确定数组中处于这个范围内的用户数量
- 减去用户自己(因为不能向自己发送请求)
2.2 代码实现细节
java复制class Solution {
public int numFriendRequests(int[] ages) {
int requests = 0;
Arrays.sort(ages);
int n = ages.length;
for (int i = 0; i < n; i++) {
if (ages[i] < 15) {
continue;
}
int lower = ages[i] / 2 + 8;
int upper = ages[i];
int left = findFirstGreaterOrEqual(ages, lower);
int right = findLastLessOrEqual(ages, upper);
requests += right - left;
}
return requests;
}
// 查找第一个≥target的元素下标
private int findFirstGreaterOrEqual(int[] nums, int target) {
int left = 0, right = nums.length;
while (left < right) {
int mid = left + (right - left) / 2;
if (nums[mid] >= target) {
right = mid;
} else {
left = mid + 1;
}
}
return left;
}
// 查找最后一个≤target的元素下标
private int findLastLessOrEqual(int[] nums, int target) {
int left = -1, right = nums.length - 1;
while (left < right) {
int mid = left + (right - left + 1) / 2;
if (nums[mid] <= target) {
left = mid;
} else {
right = mid - 1;
}
}
return left;
}
}
2.3 复杂度分析
- 时间复杂度:O(n log n),主要来自排序和n次二分查找
- 空间复杂度:O(log n),排序所需的栈空间
2.4 注意事项
- 二分查找的实现需要注意边界条件
- 对于年龄<15的用户可以直接跳过
- 两个二分查找函数的实现有所不同,需要特别注意
3. 解法二:排序+双指针
3.1 算法思路
这个解法利用了年龄范围随着x增加而单调递增的特性:
- 先对年龄数组排序
- 维护两个指针left和right,表示当前年龄x的有效范围[lower, upper]
- 随着x增加,left和right只需要向右移动,不需要回溯
3.2 代码实现
java复制class Solution {
public int numFriendRequests(int[] ages) {
int requests = 0;
Arrays.sort(ages);
int n = ages.length;
int left = 0, right = 0;
for (int age : ages) {
if (age < 15) continue;
int lower = age / 2 + 8;
int upper = age;
while (ages[left] < lower) left++;
while (right + 1 < n && ages[right + 1] <= upper) right++;
requests += right - left;
}
return requests;
}
}
3.3 复杂度分析
- 时间复杂度:O(n log n),排序占主导
- 空间复杂度:O(log n),排序所需的栈空间
3.4 优势与局限
优势:
- 比二分查找解法更高效,因为left和right指针不需要每次都从头开始
- 代码更简洁
局限:
- 仍然需要排序步骤
- 对于小规模数据可能优势不明显
4. 解法三:计数排序+前缀和
4.1 算法思路
这个解法利用了年龄范围有限(1-120)的特点:
- 统计每个年龄出现的次数
- 计算前缀和数组,便于快速查询区间和
- 对于每个年龄≥15的用户,直接计算有效范围内的用户数
4.2 代码实现
java复制class Solution {
public int numFriendRequests(int[] ages) {
int[] count = new int[121];
for (int age : ages) count[age]++;
int[] prefix = new int[121];
for (int i = 1; i <= 120; i++) {
prefix[i] = prefix[i - 1] + count[i];
}
int requests = 0;
for (int i = 15; i <= 120; i++) {
if (count[i] == 0) continue;
int lower = i / 2 + 8;
int upper = i;
int friends = prefix[upper] - prefix[lower - 1] - 1;
requests += count[i] * friends;
}
return requests;
}
}
4.3 复杂度分析
- 时间复杂度:O(n + m),其中m是最大年龄(120)
- 空间复杂度:O(m)
4.4 适用场景
这是最高效的解法,特别适合:
- 年龄范围有限的情况
- 数据量大的情况
- 需要多次查询的情况
5. 性能对比与选择建议
5.1 三种解法对比
| 解法 | 时间复杂度 | 空间复杂度 | 适用场景 |
|---|---|---|---|
| 排序+二分查找 | O(n log n) | O(log n) | 通用解法 |
| 排序+双指针 | O(n log n) | O(log n) | 数据量大时优于二分查找 |
| 计数排序 | O(n + m) | O(m) | 年龄范围有限时最优 |
5.2 选择建议
- 如果年龄范围很大(比如没有限制),使用双指针解法
- 如果年龄范围有限(如本题1-120),使用计数排序解法
- 二分查找解法作为通用解法,适合面试时快速实现
6. 常见问题与调试技巧
6.1 常见错误
- 忘记处理年龄<15的情况
- 在计算好友数时忘记减去自己
- 二分查找实现错误,特别是边界条件
- 双指针解法中指针移动条件错误
6.2 调试技巧
- 对于小测试用例手动计算验证
- 打印中间结果(如每个年龄的有效范围)
- 特别注意边界情况:
- 所有用户年龄相同
- 所有用户年龄都小于15
- 最大年龄和最小年龄的情况
6.3 测试用例建议
java复制// 测试用例示例
int[] test1 = {16,16}; // 预期2
int[] test2 = {16,17,18}; // 预期2
int[] test3 = {20,30,100,110,120}; // 预期3
int[] test4 = {10,10,10}; // 预期0(都小于15)
int[] test5 = {120,120,120}; // 预期6(三个120互相发送)
7. 算法优化思考
7.1 进一步优化空间
- 可以合并计数和前缀和的计算步骤
- 对于年龄范围可以预先计算所有可能的lower和upper
- 使用更紧凑的数据结构存储计数
7.2 扩展思考
- 如果好友请求是双向的(即x→y和y→x算作不同请求),如何修改算法?
- 如果年龄范围扩大到更大值(如1-10000),哪种解法更优?
- 如果增加更多限制条件,算法该如何调整?
在实际编码面试中,建议先提出排序+双指针的解法,然后根据面试官的提示进一步优化到计数排序的解法。同时要注意代码的整洁性和边界条件的处理。