(LeetCode-Hot100)49. 字母异位词分组

问题简介

LeetCode 49. 字母异位词分组

题目描述

给你一个字符串数组,请你将 字母异位词 组合在一起。可以按任意顺序返回结果列表。

字母异位词 是由重新排列源单词的字母得到的一个新单词,所有源单词中的字母通常恰好只用一次。


示例说明

✅ 示例 1:

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

✅ 示例 2:

复制代码
输入: strs = [""]
输出: [[""]]

✅ 示例 3:

复制代码
输入: strs = ["a"]
输出: [["a"]]

解题思路

💡 方法一:排序作为键(推荐)

核心思想 :

字母异位词在排序后会变成相同的字符串。我们可以将每个字符串排序后的结果作为哈希表的 key,value 则是具有相同 key 的原始字符串列表。

步骤如下:

  1. 创建一个哈希表 Map<String, List<String>>。
  2. 遍历字符串数组中的每一个字符串:
    • 将当前字符串转为字符数组并排序;
    • 将排序后的字符数组转回字符串,作为 key;
    • 将原始字符串加入到该 key 对应的列表中。
  3. 最终将哈希表的所有 value 收集为结果列表。

✅ 优点:实现简单、直观,适用于大多数场景。


💡 方法二:字符计数作为键

核心思想 :

两个字符串是字母异位词,当且仅当它们每个字符出现的次数完全相同。因此,我们可以统计每个字符串中 26 个字母的出现次数,将其编码为一个唯一标识(如 "a2b1c0..."),作为哈希表的 key。

步骤如下:

  1. 创建哈希表 Map<String, List<String>>。
  2. 对每个字符串:
    • 初始化长度为 26 的计数数组;
    • 遍历字符串,统计每个字符出现次数;
    • 将计数数组编码为字符串(如 "1#2#0#...#0");
    • 以该编码为 key,将原字符串加入对应列表。
  3. 返回所有 value。

✅ 优点 :避免了排序开销,理论上时间复杂度更优(但常数较大)。

❌ 缺点:编码逻辑稍复杂,且对于非小写字母需扩展处理。

📌 本题假设只包含小写字母,故方法二可行;若包含大小写或 Unicode,则方法一更通用。


代码实现

java:Java 复制代码
import java.util.*;

class Solution {
    // 方法一:排序作为 key
    public List<List<String>> groupAnagrams(String[] strs) {
        Map<String, List<String>> map = new HashMap<>();
        for (String s : strs) {
            char[] chars = s.toCharArray();
            Arrays.sort(chars);
            String key = new String(chars);
            map.computeIfAbsent(key, k -> new ArrayList<>()).add(s);
        }
        return new ArrayList<>(map.values());
    }

    // 方法二:字符计数作为 key(可选)
    public List<List<String>> groupAnagramsCount(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']++;
            }
            StringBuilder sb = new StringBuilder();
            for (int i = 0; i < 26; i++) {
                sb.append(count[i]).append('#');
            }
            String key = sb.toString();
            map.computeIfAbsent(key, k -> new ArrayList<>()).add(s);
        }
        return new ArrayList<>(map.values());
    }
}
go:Go 复制代码
package main

import (
	"sort"
	"strings"
)

// 方法一:排序作为 key
func groupAnagrams(strs []string) [][]string {
	groups := make(map[string][]string)
	for _, s := range strs {
		// 将字符串转为字节切片并排序
		bytes := []byte(s)
		sort.Slice(bytes, func(i, j int) bool {
			return bytes[i] < bytes[j]
		})
		key := string(bytes)
		groups[key] = append(groups[key], s)
	}
	
	// 提取所有分组
	result := make([][]string, 0, len(groups))
	for _, group := range groups {
		result = append(result, group)
	}
	return result
}

// 方法二:字符计数作为 key(可选)
func groupAnagramsCount(strs []string) [][]string {
	groups := make(map[string][]string)
	for _, s := range strs {
		count := make([]int, 26)
		for _, c := range s {
			count[c-'a']++
		}
		var key strings.Builder
		for _, c := range count {
			key.WriteString(strconv.Itoa(c) + "#")
		}
		k := key.String()
		groups[k] = append(groups[k], s)
	}
	
	result := make([][]string, 0, len(groups))
	for _, group := range groups {
		result = append(result, group)
	}
	return result
}

⚠️ 注意:Go 中 strconv.Itoa 需要导入 strconv 包(方法二中未显式写出 import,实际使用需添加)。


示例演示

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

原字符串 排序后 key 分组情况
"eat" "aet" "eat"
"tea" "aet" "eat", "tea"
"tan" "ant" "tan"
"ate" "aet" "eat", "tea", "ate"
"nat" "ant" "tan", "nat"
"bat" "abt" "bat"

最终分组:[["eat","tea","ate"], ["tan","nat"], ["bat"]]


答案有效性证明

  • ✅ 正确性 :

    若两个字符串是字母异位词 ⇨ 它们排序后相等 ⇨ 被分到同一组。

    反之,若排序后相等 ⇨ 字符组成完全相同 ⇨ 是字母异位词。

    因此,分组结果满足题目要求。

  • ✅ 完备性 :

    所有字符串都会被处理并加入某个分组,无遗漏。

  • ✅ 无重复 :

    每个字符串仅被加入一次,且分组之间互斥。


复杂度分析

方法 时间复杂度 空间复杂度 说明
排序法 O ( N c d o t K l o g K ) O(N \\cdot K \\log K) O(NcdotKlogK) O ( N c d o t K ) O(N \\cdot K) O(NcdotK) N N N 为字符串数量, K K K 为平均字符串长度。排序每个字符串耗时 O ( K l o g K ) O(K \\log K) O(KlogK)。
计数法 O ( N c d o t K ) O(N \\cdot K) O(NcdotK) O ( N c d o t K + 26 c d o t N ) a p p r o x O ( N c d o t K ) O(N \\cdot K + 26 \\cdot N) \\approx O(N \\cdot K) O(NcdotK+26cdotN)approxO(NcdotK) 每个字符串遍历一次统计字符,编码固定长度(26),无排序开销。

📌 实际中,由于 K K K 通常较小(如 ≤ 100),排序法常数小、代码简洁,更常用。


问题总结

  • ✅ 关键洞察:字母异位词具有相同的"规范形式"------可通过排序或字符频次唯一确定。
  • ✅ 哈希表是核心工具:用于将具有相同特征的元素聚合。
  • ✅ 两种方法权衡 :
    • 排序法:简洁、通用、易写,适合面试和工程。
    • 计数法:理论更优 ,但编码复杂,仅在 K K K 很大时有优势。
  • 💡 扩展思考 :若字符串包含 Unicode 字符(如中文),排序法依然适用,而计数法需改用 Map<Character, Integer> 并序列化为 key。

🎯 推荐掌握排序法,它是解决此类"归一化分组"问题的经典范式。

github地址: https://github.com/swf2020/LeetCode-Hot100-Solutions

相关推荐
学长毕设2 小时前
基于SpringBoot的实验室资产管理系统
java·spring boot·后端
zzu123zsw3 小时前
从臃肿的模板调用到声明式契约:深入 UniHttp 动态代理与多渠道生命周期扩展
java·spring boot·microsoft·unihttp
Yuzhiyuxia4 小时前
【AI Agent面试系列】AI Agent记忆机制知识点1
人工智能·面试·职场和发展
UIU1144 小时前
P1233 [ICPC 2001 Taejon R] 木棍加工
数据结构·c++·算法·动态规划
源码集结号4 小时前
云诊所门诊系统源码,基于SpringBoot+Vue.js架构,支持多租户设计与Docker容器化部署
java·spring boot·源码·医院信息系统·门诊系统·诊所系统
论文复现现场4 小时前
WRF、FEA 能用 L40S 加速吗?求解器检查、断点恢复与长期运行
java·数据库·算法·wrf·有限元分析·gpu计算·断点续算
ZhengEnCi4 小时前
用 AI 写代码为什么拼到最后还是拼人:算力瓶颈与领域专家提示词的本质差异
算法·架构
啃瓜子的松鼠5 小时前
泛微OAe9-后端二开异步调用
java
秋天的怀念_David6 小时前
第二十一夜 · 数字黑洞与西西弗斯的惩罚
java·程序人生·算法
外收内放6 小时前
Python基础语法练习题(73)
java·前端·python