1. 概述
查找字符串中出现频率最高的字符是编程中的常见任务。本文将探讨在Java中实现该功能的多种方法。
2. 使用Java Stream API
Stream API 是Java 8带来的重要特性。首先我们用Stream API解决这个问题。
在看实现前,先通过例子快速理解问题。假设有字符串 INPUT1
:
static final String INPUT1 = "aaaaaaaaaa(10) bbbbbbb ccccc dddd eee ff";
显然 INPUT1
中频率最高的是字符 'a'
。我们的目标就是找到它。
接下来创建解决方法:
static Character byStream(String input) {
return input.chars()
.mapToObj(x -> (char) x)
.collect(groupingBy(x -> x, counting()))
.entrySet()
.stream()
.max(comparingByValue())
.get()
.getKey();
}
通过Stream API,我们将多个方法调用链式组合成单条语句。这是因为 **Java Stream API采用了流畅接口模式**。
下面拆解这条语句的工作原理:
input.chars()
→ 将字符串拆解为IntStreammapToObject(x -> (char) x)
→ 将整型转为char并装箱为Charactercollect(groupingBy(x -> x, counting()))
→ 按字符分组,用counting()统计频次,收集为Map<Character, Long>
entrySet().stream()
→ 创建所有Map条目的流max(comparingByValue())
→ 按值找出最大条目(返回类型为Optional<Entry<Character, Long>>
)get()
→ 获取条目对象getKey()
→ 获取条目的键(即频次最高的字符)
验证方法是否正确:
char result1 = CharacterWithHighestFrequency.byStream(INPUT1);
assertEquals('a', result1);
测试通过 ✅。但遇到类似下面的输入时会踩坑:
static final String INPUT2 = "YYYYYYY(7) bbbbb -------(7) dddd eee kkkkkkk(7) ff";
INPUT2
中三个字符频次相同(7):'Y'
、'–'
和 'k'
。如果需求允许返回其中任意一个,当前方法可用。但如果需要返回所有最高频字符,此方案就失效了 ❌。
接下来构建能同时处理两种情况的解决方案。
3. 使用 HashMap
HashMap 维护键值对映射。我们可以 创建 HashMap<Character, Integer>
存储字符及其出现次数,然后找出最大值对应的键。
实现方法:
static Set<Character> byMap(String input) {
Map<Character, Integer> map = new HashMap<>();
for (char c : input.toCharArray()) {
map.compute(c, (character, count) -> count == null ? 1 : ++count);
}
int maxCount = map.values()
.stream()
.mapToInt(Integer::intValue)
.max()
.getAsInt();
return map.keySet()
.stream()
.filter(c -> map.get(c) == maxCount)
.collect(toSet());
}
byMap()
返回 Set<Character>
而非单个字符,能同时处理两种场景。
实现要点:
- 初始化空Map,遍历字符时用
Map.compute()
简化计数逻辑
✅ 键存在则值+1,否则初始化为1 - 通过Stream API找出
maxCount
⚠️ 也可在遍历字符时同步更新最大值(后续桶方案会展示) - 再次用Stream收集所有频次等于
maxCount
的字符
验证两种输入场景:
static final Set<Character> EXPECTED1 = Collections.singleton('a');
static final Set<Character> EXPECTED2 = ImmutableSet.of('Y', '-', 'k');
Set<Character> result1 = CharacterWithHighestFrequency.byMap(INPUT1);
assertEquals(EXPECTED1, result1);
Set<Character> result2 = CharacterWithHighestFrequency.byMap(INPUT2);
assertEquals(EXPECTED2, result2);
测试通过 ✅。
4. 使用桶(Bucket)方案
HashMap方案维护 字符→频次
的映射。如果只处理ASCII字符集,我们可以 用数组替代HashMap,避免Map开销。
ASCII字符集共128个字符,编码范围0-127。因此可以 初始化128个元素的int[]
数组(桶),数组索引直接对应字符编码。
遍历字符串时,直接递增对应桶的值。实现方法:
static Set<Character> byBucket(String input) {
int[] buckets = new int[128];
int maxCount = 0;
for (char c : input.toCharArray()) {
buckets[c]++;
maxCount = Math.max(buckets[c], maxCount);
}
int finalMaxCount = maxCount;
return IntStream.range(0, 128)
.filter(c -> buckets[c] == finalMaxCount)
.mapToObj(i -> (char) i)
.collect(Collectors.toSet());
}
关键点:
buckets
是长度为128的原始数组,初始值全为0- 遍历字符时通过
buckets[c]++
简单递增,同时用Math.max()
更新maxCount
- 用Stream API筛选值等于
maxCount
的桶,并将索引转为字符
⚠️ 需将maxCount
赋值给finalMaxCount
使其有效final,供lambda表达式使用
测试两种输入场景:
Set<Character> result1 = CharacterWithHighestFrequency.byBucket(INPUT1);
assertEquals(EXPECTED1, result1);
Set<Character> result2 = CharacterWithHighestFrequency.byBucket(INPUT2);
assertEquals(EXPECTED2, result2);
测试通过 ✅。
5. 总结
本文探讨了查找字符串最高频字符的三种方案:
Stream API单行方案
→ 代码简洁,但无法处理多字符同频场景HashMap方案
→ 通用性强,能处理所有情况桶方案
→ 针对ASCII字符集优化,性能更优
所有代码片段可在GitHub获取。