【Hot100-Java中等】:字母异位词分组

在 LeetCode 的字符串题目中,"字母异位词" (Anagrams) 是一个非常高频的概念。这道第 49 题不仅考察了哈希表(HashMap)的应用,更是一个理解 Java 对象机制的绝佳案例。


1. 解决方案一:排序数组分类 (Sorting)

这是最符合直觉的解法。既然"异位词"的字母成分一样,那只要把它们按照字母顺序重新排列,长得就一模一样了。

核心逻辑

  1. 遍历每个字符串。

  2. 将其转化为字符数组并排序 (如 "tea" -> "aet")。

  3. 以排序后的字符串作为 Key,原始字符串作为 Value 加入 Map。

代码实现

Java

复制代码
class Solution {
    public List<List<String>> groupAnagrams(String[] strs) {
        Map<String, List<String>> map = new HashMap<>();
        for (String str : strs) {
            char[] array = str.toCharArray();
            Arrays.sort(array); // 关键步骤:排序
            String key = new String(array); // 关键步骤:使用内容构造 Key
            
            List<String> list = map.getOrDefault(key, new ArrayList<String>());
            list.add(str);
            map.put(key, list);
        }
        return new ArrayList<>(map.values());
    }
}

复杂度分析

  • 时间复杂度

    • 是字符串的数量。

    • 是字符串的最大长度。

    • 排序每个字符串需要 O(K \\log K) 的时间。

  • 空间复杂度。我们需要存储所有字符串的内容。


2. 解决方案二:计数法 (Counting)

如果字符串特别长( 很大),排序的 可能会成为瓶颈。我们可以利用题目条件:"仅包含小写字母"。

核心逻辑

不排序,而是给字符串做"成分体检"。

  1. 用一个长度为 26 的数组 int[26] 统计每个字母出现的次数。

  2. 将这个统计结果转换成一个唯一的 Key。

    • 例如 "eat" 的统计结果是:a=1, e=1, t=1,其他为0。

    • 我们可以构造一个 Key 字符串,如 "a1e1t1..." 或者简单的利用 StringBuilder 拼接非零字符和次数。

  3. 放入 HashMap。

代码实现

Java

复制代码
class Solution {
    public List<List<String>> groupAnagrams(String[] strs) {
        Map<String, List<String>> map = new HashMap<>();
        for (String str : strs) {
            // 1. 统计频次
            int[] counts = new int[26];
            int length = str.length();
            for (int i = 0; i < length; i++) {
                counts[str.charAt(i) - 'a']++;
            }
            
            // 2. 构造唯一 Key
            // 技巧:将字母和次数拼起来,例如 "a1e1t1"
            StringBuilder sb = new StringBuilder();
            for (int i = 0; i < 26; i++) {
                if (counts[i] != 0) {
                    sb.append((char) ('a' + i));
                    sb.append(counts[i]);
                }
            }
            String key = sb.toString();
            
            // 3. 存入 Map
            List<String> list = map.getOrDefault(key, new ArrayList<String>());
            list.add(str);
            map.put(key, list);
        }
        return new ArrayList<>(map.values());
    }
}

复杂度分析

  • 时间复杂度

    • 我们在 的时间内统计频次。

    • (即 26)的时间内生成 Key。

    • 因为没有了排序,当 很大时,这种方法比排序法更快。

  • 空间复杂度

3. 深度思考:为什么两种方法一种采用new String()初始化,另一种采用toString()?

1. 数组 (char[]) 的 toString():它是"懒惰"的

在 Java 中,数组 (无论是 int[], char[] 还是 String[])虽然是对象,但它们没有 重写(Override)Object 类的默认 toString() 方法。

默认行为(Object.toString)是:

返回 类名 + @ + 内存地址哈希值

这就是为什么你之前会得到 [C@15db9742 这种乱码。它告诉你"我是一个字符数组,我住在内存的这个地址",但它不会告诉你数组里存了什么内容。

  • 比喻 :你指着一个装满苹果的箱子问:"你是什么?"箱子回答:"我是编号 888 的箱子。"(它不告诉你里面有苹果)。

2. StringBuffer / StringBuildertoString():它是"勤奋"的

在方法二中,代码是这样的:

Java

复制代码
StringBuffer sb = new StringBuffer();
// ... 循环 append 字符和数字 ...
String key = sb.toString(); // 这里调用的是 StringBuffer 的 toString

StringBuffer(以及 StringBuilder)是专门用来处理字符串的类。它们的源代码里重写toString() 方法。

它们的行为是:

把缓冲区里存储的所有字符,拼接成一个真正的 String 对象并返回。

  • 比喻 :你指着一张写满字的问:"你是什么?"纸回答:"我上面写着'a1b2c3'。"(它直接告诉你内容)。

3. 直观对比

我们可以写一段测试代码来看看区别:

Java

复制代码
char[] arr = {'a', 'b', 'c'};
StringBuffer sb = new StringBuffer();
sb.append('a').append('b').append('c');

// 情况 1:数组调用
System.out.println(arr.toString()); 
// 输出:[C@6bc7c054 (乱码/地址) -> ❌ 即使内容一样,不同数组地址不同,导致 HashMap 认为是不同 Key

// 情况 2:StringBuffer 调用
System.out.println(sb.toString());  
// 输出:abc (内容) -> ✅ 只要内容一样,生成的 String 就一样,HashMap 认为是同一个 Key

4. 方法二的巧妙之处

方法二(计数法)之所以能成功,是因为它手动把数组里的统计信息,转化成了一个唯一的字符串

  • 对于 "eat":

    1. 统计数组:a=1, e=1, t=1

    2. sb.append 拼接过程:"a1", "e1", "t1"

    3. sb.toString() 结果:"a1e1t1"

  • 对于 "tea":

    1. 统计数组:a=1, e=1, t=1

    2. sb.append 拼接过程:"a1", "e1", "t1" (顺序是按 a-z 遍历的,所以拼接顺序固定)

    3. sb.toString() 结果:"a1e1t1"

因为最后生成的都是字符串对象 "a1e1t1",而 Java 的 String 类比较的是内容(Value),所以 HashMap 判定它们相等。

总结

对象类型 调用 toString() 的结果 能否作为 HashMap 的 Key 用于分组?
数组 (char[]) 内存地址 (如 [C@xx) ❌ 不能 (内容相同但地址不同)
StringBuffer 实际文本内容 (如 "abc") ✅ 能 (内容相同即为相等)
String 实际文本内容 ✅ 能

所以,方法一要想修复,必须显式地把 char[] 转为 String(使用 new String(arr)),而不能依赖数组自带的 toString()

相关推荐
rchmin2 小时前
Redis Key过期删除策略详解
java·redis
秋邱2 小时前
Java抽象类与接口的核心区别:定义、特性与选型逻辑全解析
java·开发语言
Word码2 小时前
LeetCode283. 移动零(双指针精讲)
算法·leetcode·职场和发展
Ccuno2 小时前
Java中核心机制的概念
java·深度学习
xiaoxue..2 小时前
二叉搜索树 BST 三板斧:查、插、删的底层逻辑
javascript·数据结构·算法·面试
程序员小白条2 小时前
提前实习的好处有哪些?有坏处吗?
java·开发语言·数据结构·数据库·链表
蒙奇D索大2 小时前
【数据结构】排序算法精讲 | 快速排序全解:分治思想、核心步骤与示例演示
数据结构·笔记·学习·考研·算法·排序算法·改行学it
石工记2 小时前
Java 作为主开发语言 + 调用 AI 能力(大模型 API / 本地化轻量模型)
java·开发语言·人工智能
石去皿2 小时前
C++校招通关秘籍:从高频考点到工程思维的跃迁
java·服务器·c++