(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

相关推荐
guodingdingh1 小时前
软件开发工作问题总结0718
java·开发语言·数据库
咖啡八杯3 小时前
GoF设计模式——解释器模式
java·后端·spring·设计模式
GuWenyue3 小时前
传统Agent工具两大痛点!300行代码落地MCP跨语言工具,彻底解耦LLM与工具
前端·人工智能·算法
优橙教育3 小时前
5G网优培训 vs Java开发:转行选哪个?
java·开发语言·5g
糖果店的幽灵3 小时前
【DeepAgents 从入门到精通】Context Management 上下文管理
java·人工智能·后端·spring·中间件·langgraph·deepagents
我叫洋洋4 小时前
C ++ [ hello world ]
c语言·c++·算法
腻害兔4 小时前
【若依项目-产品经理视角】RuoYi-Vue-Pro 源码拆解:字典、短信、邮件、通知——后台系统的“基础设施四件套“!
java·前端·vue.js·产品经理·ai编程
碎光拾影5 小时前
ARM交叉工具链各工具作用及IMX6ULL平台LED+蜂鸣器裸机程序实现
java·开发语言·数据库
奋发向前wcx5 小时前
y1,y2总复习笔记5 2026.7.19
数据结构·笔记·算法
Miao121316 小时前
微服务 API 测试实践:海外某民宿平台如何构建模式驱动测试基础设施
java·开发语言