【LeetCode 热题 100】No.49—— 字母异位词分组(Java 版)

大家好!今天我们继续讲解 LeetCode 热题 100 系列,第二题聚焦高频中等题 "字母异位词分组"。这道题是哈希表的经典应用场景,核心考查对 "字符共性提取" 与 "键值对映射" 的理解。接下来,我们从题目分析、思路推导到 Java 代码实现,逐步拆解这道题。

一、题目描述

首先明确题目要求(基于 LeetCode 官方原题):

  • 给你一个字符串数组,请你将 字母异位词 组合在一起。可以按任意顺序返回结果列表。
  • 字母异位词指的是:由相同字母按照不同顺序组成的字符串(例如 "eat" 和 "tea",二者包含的字母及数量完全一致,仅顺序不同)。

示例

输入:strs = ["eat","tea","tan","ate","nat","bat"]

输出:[["bat"],["nat","tan"],["ate","eat","tea"]]

解释:"bat" 无其他异位词,单独成组;"tan" 和 "nat" 是一组,"eat""tea""ate" 是另一组。

提示

  • 1 <= strs.length <= 10⁴(数组长度可能较大,需考虑效率)
  • 0 <= strs [i].length <= 100(字符串可能为空,需特殊处理)
  • strs [i] 仅包含小写英文字母(无需考虑大小写或特殊字符)

二、解题思路分析

要解决 "分组" 问题,核心是找到字母异位词的 "共同标识" ------ 即让同一组异位词映射到同一个 "键",不同组映射到不同 "键",再用哈希表将 "键" 与 "对应的字符串列表" 关联,最终输出所有列表。

关键:如何定义 "共同标识"?

字母异位词的本质是 "字符种类和数量完全相同",基于这一特性,有两种常见的标识生成方式:

1. 方式一:排序法(推荐,代码简洁)
  • 逻辑:对每个字符串的字符进行排序,排序后相同的字符串即为异位词。

例如:"eat" → 排序后为 "aet","tea" → 排序后也为 "aet",因此二者会映射到同一键 "aet"。

  • 优势:代码实现简单,借助 Java 自带的排序函数即可完成,无需手动统计字符。
  • 注意:空字符串排序后仍为空字符串,可正常处理。
2. 方式二:计数法(进阶,效率更高)
  • 逻辑:统计每个字符串中 26 个小写字母的出现次数,用 "计数数组" 作为标识(可转化为字符串,如 "1,0,0,...1" 表示 a 出现 1 次、z 出现 1 次)。

例如:"eat" 中 a:1、e:1、t:1,计数数组转化为 "1,1,0,...0,1"(共 26 个元素),"tea" 的计数数组完全相同,因此同组。

  • 优势:排序的时间复杂度是 O (k log k)(k 为字符串长度),而计数法是 O (k),对于长字符串场景效率更高。
  • 注意:需处理空字符串(计数数组全为 0,转化后为 26 个 0 组成的字符串)。

下面我们先以排序法展开讲解(代码更易理解,适合面试快速实现),后续补充计数法的 Java 代码。

三、代码实现(排序法,Java 版)

java 复制代码
        public static List<List<String>> groupAnagrams(String[] strs) {
        Map<String,List<String>> map = new HashMap<>();
        for (String str : strs){
            // 1. 将字符串转为字符数组,便于排序
            char[] a = str.toCharArray();
            // 2. 对字符数组排序(核心:生成共同标识)
            Arrays.sort(a);
            // 3. 将排序后的字符数组转回字符串,作为哈希表的key
            String key = new String(a);
            // 4. 检查key是否在哈希表中:不存在则新建列表,存在则直接添加
            if(!map.containsKey(key)) {
                map.put(key,new ArrayList<>());
            }
            map.get(key).add(str);
        }
        // 5. 哈希表的value集合就是最终的分组结果
        return new ArrayList<>(map.values());
    }

代码解析

  1. 哈希表初始化:用HashMap<String, List<String>>存储 "标识 - 分组列表" 的映射,key 是排序后的字符串,value 是同一组异位词的列表。
  1. 遍历字符串数组:对每个字符串str,先转为字符数组并排序,生成唯一标识key。
  1. 分组逻辑:若key不在哈希表中,新建列表存入;若已存在,直接将s加入对应列表。
  1. 返回结果:将哈希表的所有 value(即分组列表)转为ArrayList返回,符合题目要求。

四、代码实现(计数法,进阶 Java 版)

java 复制代码
public static List<List<String>> groupAnagrams(String[] strs) {
        Map<String, List<String>> map = new HashMap<>();
        for (String s : strs) {
            // 1. 初始化计数数组(26个小写字母,初始值0)
            int[] count = new int[26];
            // 2. 统计每个字符的出现次数:'a'-'a'=0,'b'-'a'=1,对应数组下标
            for (char c : s.toCharArray()) {
                count[c - 'a']++;
            }
            // 3. 将计数数组转为字符串作为key(加逗号避免歧义,如11和1,1)
            StringBuilder keySb = new StringBuilder();
            for (int num : count) {
                keySb.append(num).append(',');
            }
            String key = keySb.toString();
            // 4. 添加入哈希表:不存在key则新建列表,存在则直接添加
            if (!map.containsKey(key)) {
                map.put(key, new ArrayList<>());
            }
            map.get(key).add(s);
        }
        // 5. 返回所有分组列表
        return new ArrayList<>(map.values());
    }

代码解析

  1. 计数数组:用长度为 26 的int数组统计每个小写字母的出现次数,数组下标对应字母(0→a,1→b,...,25→z)。
  1. 生成 key:通过StringBuilder将计数数组转为字符串(如计数数组[1,1,0,...1]转为"1,1,0,...,1"),加逗号可避免 "11"(表示某字母出现 11 次)与 "1,1"(表示两个字母各出现 1 次)的歧义。
  1. 分组逻辑:与排序法一致,通过哈希表关联key和分组列表,最终返回所有列表。

五、测试案例验证

测试案例 1:基础示例

输入:["eat","tea","tan","ate","nat","bat"]

排序法处理过程:

  • "eat" → 排序后"aet",列表添加 "eat";
  • "tea" → 排序后"aet",列表添加 "tea";
  • "tan" → 排序后"ant",列表添加 "tan";
  • "ate" → 排序后"aet",列表添加 "ate";
  • "nat" → 排序后"ant",列表添加 "nat";
  • "bat" → 排序后"abt",列表添加 "bat";

输出:["bat"]],[["eat","tea","ate"],["tan","nat"](顺序不影响,符合题目要求)。

测试案例 2:含空字符串

输入:[""]

处理过程(以排序法为例):

  • 空字符串排序后仍为空,key为"",列表添加空字符串;

输出:[[""]]]

测试案例 3:单个字符

输入:["a"]

输出:[["a"]]

六、复杂度分析

排序法(Java 版)

  • 时间复杂度:O (n * k log k)。n 是字符串数组长度,k 是字符串的平均长度;每个字符串转字符数组并排序耗时 O (k log k),遍历数组耗时 O (n),整体为 O (n * k log k)。
  • 空间复杂度:O (n * k)。哈希表存储的所有字符串总长度为 O (n * k),排序过程中字符数组的临时空间可忽略(视为 O (1))。

计数法(Java 版)

  • 时间复杂度:O (n * k)。统计每个字符串的字符计数耗时 O (k),遍历数组耗时 O (n),整体为线性时间,效率高于排序法。
  • 空间复杂度:O (n * k)。哈希表存储的字符串总长度为 O (n * k),计数数组长度固定为 26(视为 O (1))。

七、总结

"字母异位词分组" 的核心是找到异位词的共同标识,再用哈希表实现 "标识→分组" 的映射,Java 实现的关键要点如下:

  1. 排序法代码简洁,借助Arrays.sort()即可快速生成标识,适合面试快速写题;
  1. 计数法效率更高,通过固定长度的计数数组生成标识,避免排序的对数时间开销,适合长字符串场景;
  1. 处理空字符串时,两种方法均无需额外判断(排序后为空,计数数组全为 0);
  1. 哈希表的 "键设计" 需确保 "同组同键、异组异键",计数法中加逗号可避免歧义。

下一篇我们会讲解 LeetCode 热题 100 的第3题最长连续序列,感兴趣的同学可以持续关注~如果有疑问,欢迎在评论区留言讨论!

相关推荐
未知陨落2 小时前
LeetCode:99.下一个排列
算法·leetcode
2401_841495642 小时前
【数据结构】链栈的基本操作
java·数据结构·c++·python·算法·链表·链栈
元亓亓亓2 小时前
SSM--day2--Spring(二)--核心容器&注解开发&Spring整合
java·后端·spring
毕设源码-赖学姐2 小时前
【开题答辩全过程】以 SpringMVC在筑原平面设计定制管理信息系统的应用与实践为例,包含答辩的问题和答案
java·eclipse
Archie_IT2 小时前
「深入浅出」嵌入式八股文—P2 内存篇
c语言·开发语言·数据结构·数据库·c++·算法
是那盏灯塔2 小时前
【算法】——动态规划算法及实践应用
数据结构·c++·算法·动态规划
程序定小飞3 小时前
基于springboot的蜗牛兼职网的设计与实现
java·数据库·vue.js·spring boot·后端·spring
MATLAB代码顾问3 小时前
MATLAB计算标准径流指数(Standard Runoff Index,SRI)
数据结构·算法·matlab
咖啡Beans3 小时前
RestTemplate调用API的常用写法
java·spring boot·网络协议