(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

相关推荐
陈天伟教授1 小时前
人工智能应用- 推荐算法:05.推荐算法的社会争议
算法·机器学习·推荐算法
apcipot_rain1 小时前
原神“十盒半价”问题的兹白式建模分析
python·数学·算法·函数·数据科学·原神·数列
吴声子夜歌1 小时前
RxJava——Flowable与背压
android·java·rxjava
小刘的大模型笔记1 小时前
PPO与DPO:大模型对齐的两大核心算法,差异与选型全解析
算法
Thanwind1 小时前
大二上结束随笔
java
啊阿狸不会拉杆1 小时前
《计算机视觉:模型、学习和推理》第 1 章 - 绪论
人工智能·python·学习·算法·机器学习·计算机视觉·模型
Frostnova丶2 小时前
LeetCode 693. 交替位二进制数
算法·leetcode
_F_y2 小时前
递归搜索入门
算法
We་ct2 小时前
LeetCode 101. 对称二叉树:两种解法(递归+迭代)详解
前端·算法·leetcode·链表·typescript