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采用了流畅接口模式**。

下面拆解这条语句的工作原理:

  1. input.chars()
    → 将字符串拆解为IntStream

  2. mapToObject(x -> (char) x)
    → 将整型转为char并装箱为Character

  3. collect(groupingBy(x -> x, counting()))
    → 按字符分组,用counting()统计频次,收集为Map<Character, Long>

  4. entrySet().stream()
    → 创建所有Map条目的流

  5. max(comparingByValue())
    → 按值找出最大条目(返回类型为Optional<Entry<Character, Long>>

  6. get()
    → 获取条目对象

  7. 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> 而非单个字符,能同时处理两种场景。

实现要点:

  1. 初始化空Map,遍历字符时用 Map.compute() 简化计数逻辑
    ✅ 键存在则值+1,否则初始化为1
  2. 通过Stream API找出 maxCount
    ⚠️ 也可在遍历字符时同步更新最大值(后续桶方案会展示)
  3. 再次用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. 总结

本文探讨了查找字符串最高频字符的三种方案:

  1. Stream API单行方案
    → 代码简洁,但无法处理多字符同频场景

  2. HashMap方案
    → 通用性强,能处理所有情况

  3. 桶方案
    → 针对ASCII字符集优化,性能更优

所有代码片段可在GitHub获取。


原始标题:Find the Most Frequent Characters in a String