1. 题目解析与解题思路
力扣1207题"独一无二的出现次数"是一道考察基础数据结构和算法应用的典型题目。题目要求我们判断一个整数数组中,不同数字的出现次数是否全部唯一。换句话说,如果数字5出现3次,数字8也出现3次,这种情况就不符合要求,因为出现次数3重复了。
这道题的核心在于两个关键操作:
- 统计每个数字出现的次数
- 检查这些次数是否互不相同
1.1 哈希法的天然适配性
哈希表(Hash Table)是这个问题的完美解决方案,原因有三:
首先,统计数字出现次数是典型的键值对存储场景。数字本身作为键(Key),出现次数作为值(Value),这正是哈希表最擅长的领域。在Java中,我们有现成的HashMap类可以直接使用。
其次,哈希表的插入和查询操作平均时间复杂度都是O(1),这使得整体算法可以保持O(n)的时间复杂度,n为数组长度。这对于算法题的时间限制非常重要。
最后,Java的HashMap实现非常成熟,我们不需要自己处理哈希冲突等底层细节,可以专注于算法逻辑本身。
1.2 算法步骤拆解
完整的解题流程可以分为三个明确步骤:
- 第一次遍历:使用HashMap统计每个数字的出现次数
- 提取所有的出现次数,存入一个集合(Set)中
- 比较HashMap的size和Set的size是否相等
这里的关键洞察是:Set会自动去重。如果所有出现次数都是唯一的,那么Set的大小应该等于HashMap的大小;如果有重复的出现次数,Set的大小会小于HashMap的大小。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. Java实现详解
2.1 基础实现代码
java复制public boolean uniqueOccurrences(int[] arr) {
// 统计数字出现频率
Map<Integer, Integer> frequencyMap = new HashMap<>();
for (int num : arr) {
frequencyMap.put(num, frequencyMap.getOrDefault(num, 0) + 1);
}
// 检查频率是否唯一
Set<Integer> frequencySet = new HashSet<>(frequencyMap.values());
return frequencyMap.size() == frequencySet.size();
}
2.2 关键代码解析
-
frequencyMap.getOrDefault(num, 0) + 1:- 这是Java 8引入的便捷方法
- 当num不存在于map中时,返回默认值0
- 存在时返回当前值
- 无论哪种情况都会+1更新计数
-
new HashSet<>(frequencyMap.values()):- 直接将map的所有value存入HashSet
- HashSet会自动去重
- 这是判断唯一性的关键
-
最后的size比较:
- 如果所有出现次数都唯一,两个size应该相等
- 如果有重复的出现次数,Set的size会变小
2.3 时间复杂度分析
- 第一次遍历数组:O(n)
- 创建HashSet:O(m),m为不同数字的个数
- 总体时间复杂度:O(n + m) ≈ O(n),因为m ≤ n
空间复杂度:
- HashMap存储:O(m)
- HashSet存储:O(m)
- 总体空间复杂度:O(m)
3. 优化与变种
3.1 使用数组替代HashMap
对于特定约束条件(如数字范围已知且不大),可以用数组代替HashMap:
java复制public boolean uniqueOccurrences(int[] arr) {
int[] freq = new int[2001]; // 假设数字范围在-1000到1000
for (int num : arr) {
freq[num + 1000]++;
}
boolean[] seen = new boolean[1001]; // 最大出现次数不超过1000
for (int count : freq) {
if (count != 0) {
if (seen[count]) {
return false;
}
seen[count] = true;
}
}
return true;
}
这种实现:
- 时间复杂度仍然是O(n)
- 空间复杂度是固定大小,可能比HashMap更高效
- 但适用场景有限,需要知道数字范围
3.2 流式API实现
Java 8+可以使用流式操作:
java复制public boolean uniqueOccurrences(int[] arr) {
Map<Integer, Long> frequencyMap = Arrays.stream(arr)
.boxed()
.collect(Collectors.groupingBy(Function.identity(), Collectors.counting()));
return frequencyMap.values().stream().distinct().count() == frequencyMap.size();
}
特点:
- 代码更简洁
- 但性能可能略低于传统写法
- 可读性取决于对Stream API的熟悉程度
4. 测试用例设计
4.1 常规测试用例
java复制@Test
public void testUniqueOccurrences() {
Solution solution = new Solution();
// 标准情况
assertTrue(solution.uniqueOccurrences(new int[]{1,2,2,1,1,3}));
assertFalse(solution.uniqueOccurrences(new int[]{1,2}));
// 边界情况
assertTrue(solution.uniqueOccurrences(new int[]{-3,0,1,-3,1,1,1,-3,10,0}));
assertTrue(solution.uniqueOccurrences(new int[]{1}));
assertTrue(solution.uniqueOccurrences(new int[]{}));
// 大数情况
assertFalse(solution.uniqueOccurrences(new int[]{1,1,2,2,3,3,4,4}));
}
4.2 测试要点
- 常规情况:验证基本逻辑
- 边界情况:空数组、单个元素
- 负数情况:验证数字范围处理
- 重复情况:明确出现次数重复的场景
- 大数情况:验证大输入下的性能
5. 常见问题与解决
5.1 为什么使用HashSet而不是直接比较?
有人可能会想:为什么不直接遍历HashMap的值列表来检查重复?比如:
java复制List<Integer> frequencies = new ArrayList<>(frequencyMap.values());
for (int i = 0; i < frequencies.size(); i++) {
for (int j = i + 1; j < frequencies.size(); j++) {
if (frequencies.get(i).equals(frequencies.get(j))) {
return false;
}
}
}
return true;
问题在于:
- 时间复杂度变为O(m^2),m为不同数字的个数
- 当m较大时(如n/2),这会退化为O(n^2)
- 而HashSet方案保持O(n)时间复杂度
5.2 Integer缓存问题
在Java中,Integer有缓存机制(-128到127)。比较超出这个范围的Integer时:
java复制Integer a = 1000;
Integer b = 1000;
System.out.println(a == b); // false
因此,在比较出现次数时:
- 应该使用equals()而不是==
- 或者让自动拆箱处理:
java复制if (seen[count]) // 正确,count自动拆箱为int
5.3 原始类型与包装类型
在性能敏感场景,可以考虑使用原始类型特化的哈希表,如Eclipse Collections的IntIntHashMap:
java复制MutableIntIntMap frequencyMap = new IntIntHashMap();
for (int num : arr) {
frequencyMap.addToValue(num, 1);
}
优势:
- 避免Integer装箱开销
- 减少内存占用
- 但增加了第三方依赖
6. 同类题目推荐
掌握这道题后,可以尝试以下类似题目巩固哈希表的使用:
-
力扣1. 两数之和
- 经典哈希表应用
- 存储数值与索引的映射
-
力扣242. 有效的字母异位词
- 统计字符出现频率
- 比较频率分布
-
力扣451. 根据字符出现频率排序
- 统计频率后排序
- 哈希表与优先队列结合
-
力扣347. 前K个高频元素
- 统计频率后取Top K
- 哈希表与堆的结合使用
-
力扣205. 同构字符串
- 双向字符映射检查
- 类似唯一性检查
7. 实际应用场景
这种"统计+唯一性检查"的模式在实际开发中很常见:
-
用户行为分析:
- 统计用户各种操作频率
- 检查是否有异常重复操作
-
日志分析:
- 统计错误码出现次数
- 识别高频错误
-
数据清洗:
- 检查字段值的分布
- 发现数据异常
-
特征工程:
- 统计类别特征的出现频率
- 过滤低频类别
掌握这种基础模式,可以解决很多实际工程中的数据统计问题。
