【Hot100 | 2 LeetCode49 字母异位词分组问题】

Python3.8

Python 是一种高级、解释型、通用的编程语言,以其简洁易读的语法而闻名,适用于广泛的应用,包括Web开发、数据分析、人工智能和自动化脚本

LeetCode 49. 字母异位词分组 - 多语言解法详解

一、问题理解

「字母异位词分组」问题要求:给定一个字符串数组 strs,将字母异位词组合在一起,可以按任意顺序返回结果列表。字母异位词是由重新排列源单词的字母得到的一个新单词,所有源单词中的字母通常恰好只用一次。

示例

text

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

其中 "eat", "tea", "ate" 是字母异位词,因为它们包含相同的字母,只是排列顺序不同。

二、核心思路

字母异位词的核心特征是:排序后会得到完全相同的字符串。因此,我们可以用「排序后的字符串」作为分组的 key,用「存储原字符串的列表」作为分组的 value,通过哈希表/字典实现快速分组。

算法步骤

  1. 创建一个哈希表/字典,key 是排序后的字符串,value 是原字符串列表

  2. 遍历每个字符串:

    • 将字符串转换为字符数组/列表并排序

    • 将排序后的字符数组/列表转换为字符串作为 key

    • 将原字符串添加到该 key 对应的列表中

  3. 返回哈希表/字典中所有值的集合

三、代码逐行解析

Java 解法

java

class Solution {
    public List<List<String>> groupAnagrams(String[] strs) {
        // 1. 创建哈希表:key是排序后的字符串,value是同组原字符串的列表
        // HashMap<String, List<String>> 表示键是字符串,值是字符串列表
        Map<String, List<String>> m = new HashMap<>();
        
        // 2. 遍历输入的每个字符串
        for (String s : strs) {
            // 2.1 将字符串转换为字符数组(方便排序)
            // String 是不可变的,转换为 char[] 后才能排序
            char[] sortedS = s.toCharArray();
            
            // 2.2 对字符数组排序(异位词排序后会得到相同的字符序列)
            // Arrays.sort() 使用双轴快速排序算法
            Arrays.sort(sortedS);
            
            // 2.3 核心:使用 computeIfAbsent 方法处理分组
            // new String(sortedS):将排序后的字符数组转为字符串作为 key
            // _ -> new ArrayList<>():如果 key 不存在,创建新的空列表
            // .add(s):将原字符串 s 添加到列表中
            m.computeIfAbsent(new String(sortedS), _ -> new ArrayList<>()).add(s);
        }
        
        // 3. 哈希表的 value 集合就是所有分组,转换为 List 返回
        // m.values() 返回 Collection<List<String>>,用 ArrayList 包装
        return new ArrayList<>(m.values());
    }
}

Python 解法

python

from typing import List
from collections import defaultdict

class Solution:
    def groupAnagrams(self, strs: List[str]) -> List[List[str]]:
        # 1. 使用 defaultdict 创建字典,默认值为空列表
        # 这样就不需要检查键是否存在,直接追加即可
        anagram_dict = defaultdict(list)
        
        # 2. 遍历输入的每个字符串
        for s in strs:
            # 2.1 将字符串排序作为 key
            # ''.join(sorted(s)) 完成:将字符串 s 转换为列表并排序,再拼接回字符串
            # 例如:"eat" -> sorted('eat') -> ['a', 'e', 't'] -> "aet"
            key = ''.join(sorted(s))
            
            # 2.2 将原字符串添加到对应 key 的列表中
            # 由于使用了 defaultdict(list),如果 key 不存在会自动创建空列表
            anagram_dict[key].append(s)
        
        # 3. 返回字典中所有值的列表
        # dict.values() 返回的是视图对象,需要用 list() 转换为列表
        return list(anagram_dict.values())

四、Java 与 Python 语法对比

1. 哈希表/字典的创建与使用

操作JavaPython
创建哈希表Map<String, List<String>> m = new HashMap<>();anagram_dict = {} 或 defaultdict(list)
检查键是否存在m.containsKey(key)key in anagram_dict
获取值m.get(key)anagram_dict[key] 或 anagram_dict.get(key)
设置默认值m.computeIfAbsent(key, k -> new ArrayList<>())anagram_dict.setdefault(key, []) 或 defaultdict(list)
获取所有值m.values()anagram_dict.values()

2. 字符串排序处理

操作JavaPython
字符串转字符数组s.toCharArray()list(s)
排序字符数组Arrays.sort(sortedS)sorted(s)
字符数组转字符串new String(sortedS)''.join(sorted_chars)
完整排序操作new String(Arrays.sort(s.toCharArray())) ❌ (错误,sort 返回 void)''.join(sorted(s)) ✅

3. 核心 API 对比

Java 的 computeIfAbsent 方法

java

// 语法:V computeIfAbsent(K key, Function<? super K, ? extends V> mappingFunction)
// 作用:如果 key 不存在,使用 mappingFunction 创建值并存入,返回该值
// 如果 key 存在,直接返回对应的值

Map<String, List<String>> map = new HashMap<>();
// 传统写法
if (!map.containsKey(key)) {
    map.put(key, new ArrayList<>());
}
map.get(key).add(value);

// 使用 computeIfAbsent 的简洁写法
map.computeIfAbsent(key, k -> new ArrayList<>()).add(value);
Python 的等价操作

python

# 方法1:使用 setdefault
anagram_dict = {}
anagram_dict.setdefault(key, []).append(value)

# 方法2:使用 defaultdict(更简洁)
from collections import defaultdict
anagram_dict = defaultdict(list)
anagram_dict[key].append(value)  # 如果 key 不存在会自动创建空列表

# 方法3:普通字典 + 判断
anagram_dict = {}
if key not in anagram_dict:
    anagram_dict[key] = []
anagram_dict[key].append(value)

4. 返回结果处理

操作JavaPython
获取哈希表所有值m.values() (返回 Collection<V>)anagram_dict.values() (返回 dict_values 视图)
转换为列表new ArrayList<>(m.values())list(anagram_dict.values())
列表嵌套结构List<List<String>>List[List[str]]

五、实例演示

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

Java 执行过程:

  1. 初始状态:m = {} (空哈希表)

  2. 处理 "eat":

    • sortedS = ['e','a','t'] → 排序 → ['a','e','t']

    • key = new String(['a','e','t']) = "aet"

    • m.computeIfAbsent("aet", ...) → key 不存在,创建空列表 []

    • 添加 "eat" → m = {"aet": ["eat"]}

  3. 处理 "tea":

    • sortedS = ['t','e','a'] → 排序 → ['a','e','t']

    • key = "aet"

    • m.computeIfAbsent("aet", ...) → key 已存在,返回列表 ["eat"]

    • 添加 "tea" → m = {"aet": ["eat", "tea"]}

  4. 处理 "tan":

    • sortedS = ['t','a','n'] → 排序 → ['a','n','t']

    • key = "ant"

    • 添加 "tan" → m = {"aet": ["eat", "tea"], "ant": ["tan"]}

  5. 继续处理剩余字符串...

  6. 最终返回:[["eat", "tea", "ate"], ["tan", "nat"], ["bat"]] (顺序可能不同)

Python 执行过程:

  1. 初始状态:anagram_dict = defaultdict(list)

  2. 处理 "eat":

    • key = ''.join(sorted("eat")) = "aet"

    • anagram_dict["aet"].append("eat") → {"aet": ["eat"]}

  3. 处理 "tea":

    • key = ''.join(sorted("tea")) = "aet"

    • anagram_dict["aet"].append("tea") → {"aet": ["eat", "tea"]}

  4. 处理 "tan":

    • key = ''.join(sorted("tan")) = "ant"

    • anagram_dict["ant"].append("tan") → {"aet": ["eat", "tea"], "ant": ["tan"]}

  5. 继续处理剩余字符串...

  6. 最终返回:list(anagram_dict.values())

六、复杂度分析

时间复杂度

  • Java: O(n * k log k)

    • n 是字符串数组的长度

    • k 是字符串的最大长度

    • 对每个字符串排序:O(k log k)

    • 哈希表操作:O(1) 平均时间复杂度

  • Python: O(n * k log k)

    • 与 Java 相同,sorted() 函数使用 Timsort 算法,时间复杂度为 O(k log k)

空间复杂度

  • Java: O(n * k)

    • 哈希表存储所有字符串

    • 排序需要 O(k) 的额外空间(字符数组)

  • Python: O(n * k)

    • 字典存储所有字符串

    • sorted() 创建新的列表,需要 O(k) 额外空间

七、优化思路与变体解法

1. 计数法(避免排序)

使用字符计数作为 key,而不是排序后的字符串:

python

from collections import defaultdict

class Solution:
    def groupAnagrams(self, strs: List[str]) -> List[List[str]]:
        anagram_dict = defaultdict(list)
        
        for s in strs:
            # 创建长度为26的计数数组(只包含小写字母)
            count = [0] * 26
            for char in s:
                count[ord(char) - ord('a')] += 1
            
            # 将计数数组转换为元组作为key(元组是可哈希的)
            key = tuple(count)
            anagram_dict[key].append(s)
        
        return list(anagram_dict.values())

复杂度分析

  • 时间复杂度:O(n * k),其中 k 是字符串长度(避免了排序的 O(k log k))

  • 空间复杂度:O(n * k)

2. Java 计数法实现

java

class Solution {
    public List<List<String>> groupAnagrams(String[] strs) {
        Map<String, List<String>> map = new HashMap<>();
        
        for (String s : strs) {
            int[] count = new int[26];
            for (char c : s.toCharArray()) {
                count[c - 'a']++;
            }
            
            // 将计数数组转换为字符串作为key,如 "#1#2#0#3..."
            StringBuilder keyBuilder = new StringBuilder();
            for (int num : count) {
                keyBuilder.append('#').append(num);
            }
            String key = keyBuilder.toString();
            
            map.computeIfAbsent(key, k -> new ArrayList<>()).add(s);
        }
        
        return new ArrayList<>(map.values());
    }
}

八、常用函数积累

Java 常用函数

java

// 字符串操作
String s = "hello";
char[] chars = s.toCharArray();          // 字符串转字符数组
String sortedStr = new String(chars);    // 字符数组转字符串
Arrays.sort(chars);                      // 数组排序

// 哈希表操作
Map<String, List<String>> map = new HashMap<>();
map.computeIfAbsent(key, k -> new ArrayList<>());  // 如果不存在则创建
map.getOrDefault(key, defaultValue);               // 获取值或默认值
map.containsKey(key);                              // 检查键是否存在
map.values();                                      // 获取所有值

Python 常用函数

python

# 字符串操作
s = "hello"
chars = list(s)                      # 字符串转列表
sorted_str = ''.join(sorted(s))      # 排序并拼接回字符串
sorted_chars = sorted(s)             # 返回排序后的列表

# 字典操作
from collections import defaultdict
d = defaultdict(list)                # 默认值为空列表的字典
d[key].append(value)                 # 自动处理不存在的键

d = {}
d.setdefault(key, []).append(value)  # 设置默认值并追加

# 字符计数
from collections import Counter
counter = Counter(s)                  # 统计字符出现次数

九、总结

核心要点

  1. 字母异位词的本质:排序后相同的字符串是字母异位词

  2. 哈希表/字典是关键:用排序后的字符串作为 key,原字符串列表作为 value

  3. 多语言实现差异

    • Java 使用 computeIfAbsent 简化「检查键是否存在→创建默认值」的操作

    • Python 使用 defaultdict 或 setdefault 实现类似功能

  4. 优化思路:使用字符计数法可以避免排序,将时间复杂度从 O(n * k log k) 优化到 O(n * k)

面试常见问题

  1. 为什么排序后的字符串可以作为 key?

    • 因为字母异位词排序后得到相同的字符串

  2. 为什么不能直接用字符数组作为 key?

    • 数组的 equals() 方法是引用比较,而字符串的 equals() 是内容比较

    • 在 Python 中,列表不能作为字典的 key(不可哈希),而元组可以

  3. 如何处理大小写敏感的情况?

    • 可以在排序前统一转换为小写:s.toLowerCase() (Java) 或 s.lower() (Python)

  4. 如果字符串包含 Unicode 字符怎么办?

    • 使用字符计数法更通用,排序法可能受字符编码影响

掌握这种分组问题的解法,不仅有助于解决字母异位词分组,还可以应用于其他需要根据某种规则对元素进行分组的问题,是面试中的高频考点。

您可能感兴趣的与本文相关的镜像

Python3.8

Python3.8

Conda
Python

Python 是一种高级、解释型、通用的编程语言,以其简洁易读的语法而闻名,适用于广泛的应用,包括Web开发、数据分析、人工智能和自动化脚本

评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值