**标签**：`数组` `哈希表` `字符串` `排序`

## 题目理解

- **输入**：一个字符串列表 `strs`，每个字符串只包含小写英文字母。
- **输出**：将“字母异位词”分到同一组，返回一个列表（列表里每个元素是一个分组列表）。可以按任意顺序返回。
- **什么是字母异位词**：两个字符串包含的字母种类和数量完全相同，只是排列顺序不同。例如 `"eat"`、`"tea"`、`"ate"` 互为字母异位词。
- **约束条件**：字符串数组长度最大 10⁴，每个字符串最长 100 个字符。
- **例子**：`strs = ["eat", "tea", "tan", "ate", "nat", "bat"]`  
  分组结果：`[ ["bat"], ["nat","tan"], ["ate","eat","tea"] ]`  
  因为 `"bat"` 没有其他词能和它构成字母异位词，单独一组。

---

## 解法一：排序 + 哈希表（最简单解法）

> 这是最自然、最容易理解的方法：**字母异位词排序后会变成相同的字符串**，因此可以作为分组的“钥匙”。

### 思路

1. 创建一个字典（哈希表），键用来存放“排序后的字符串”，值用来存放“原始字符串组成的列表”。
2. 遍历输入的每一个字符串 `s`：
   - 将 `s` 中的字符按字母顺序排序，得到一个新的字符串 `key`。  
     （例如 `"eat"` 排序后得到 `"aet"`，`"tea"` 排序后也得到 `"aet"`）
   - 检查字典中是否已经有 `key` 这个键：
     - 如果有，就把当前的 `s` 添加到这个键对应的列表末尾。
     - 如果没有，就新建一个列表 `[s]` 放在 `key` 下。
3. 遍历结束后，字典中所有值（每个值都是一个列表）就是我们要的分组。把这些值收集到一个列表里返回。

### 代码

```python
class Solution:
    def groupAnagrams(self, strs: list[str]) -> list[list[str]]:
        # 创建一个空字典，键：排序后的字符串，值：原始字符串列表
        groups = {}

        # 遍历输入的每个单词
        for s in strs:
            # 将字符串转为字符列表，排序，再拼回字符串
            # sorted(s) 返回一个排好序的字符列表，例如 sorted("eat") -> ['a', 'e', 't']
            # ''.join(...) 将列表中的字符连接成字符串
            key = ''.join(sorted(s))

            # 如果 key 还没有在字典中，就创建一个空列表
            if key not in groups:
                groups[key] = []
            # 把当前单词加到对应的列表中
            groups[key].append(s)

        # 返回字典中所有的值（每个值是一个分组列表）
        return list(groups.values())

# 测试示例
sol = Solution()
print(sol.groupAnagrams(["eat", "tea", "tan", "ate", "nat", "bat"]))
# 期望输出（顺序可能不同）：[['bat'], ['nat', 'tan'], ['ate', 'eat', 'tea']]

print(sol.groupAnagrams([""]))   # 输出：[['']]
print(sol.groupAnagrams(["a"]))  # 输出：[['a']]
```

### 复杂度分析

| 维度 | 复杂度 | 说明 |
|------|--------|------|
| 时间 | O(n * k log k) | n 是单词个数，k 是单词的最大长度。每个单词排序需要 O(k log k)，总时间 n 次排序 |
| 空间 | O(n * k) | 字典存储所有字符串，每个字符串都要存一份 |

### 边界情况

- **空字符串**：`""` 排序后还是 `""`，多个空字符串会分到同一组。
- **单个字符**：`"a"` 排序后仍是 `"a"`，正常分组。
- **所有单词都是相同的字母异位词**：例如 `["abc","bca","cab"]`，它们排序后都是 `"abc"`，全部进入同一个组。
- **单词长度差异很大**：不影响，每个单词单独排序。

---

## 解法二：计数 + 哈希表（优化解法）

> 排序方法需要对每个单词排序，当单词很长时（k 很大），k log k 可能较慢。另一种思路是**使用字母计数作为键**。因为输入只包含小写字母（26 个），我们可以用一个长度为 26 的元组记录每个字母出现的次数，这个元组可以直接作为字典的键。

### 思路

1. 同样创建一个字典，键是“字母计数元组”，值是原始字符串列表。
2. 遍历每个单词 `s`：
   - 创建一个长度为 26 的列表 `count`，全部初始化为 0。
   - 遍历单词的每个字符 `ch`，将 `count[ord(ch) - ord('a')]` 增加 1。  
     （`ord()` 返回字符的 ASCII 码，`ord('a')` 是 97，`ord('b')` 是 98，减去之后得到 0~25 的索引）
   - 把 `count` 列表转换成元组（因为列表不能作为字典的键，元组可以），这个元组就是 `key`。
   - 同样的，如果 `key` 不在字典中，先创建空列表，再把单词加进去。
3. 最后返回字典的所有值。

### 代码

```python
class Solution:
    def groupAnagrams(self, strs: list[str]) -> list[list[str]]:
        groups = {}

        for s in strs:
            # 初始化一个长度为 26 的列表，每个位置代表一个小写字母的出现次数
            count = [0] * 26   # 例如 count[0] 对应 'a' 的个数

            # 统计当前单词中每个字母出现了几次
            for ch in s:
                # ord(ch) - ord('a') 将字符转换为 0~25 的索引
                index = ord(ch) - ord('a')
                count[index] += 1

            # 将列表转为元组，因为元组不可变，可以作为字典的键
            key = tuple(count)

            # 如果该键不存在，先创建一个空列表
            if key not in groups:
                groups[key] = []
            groups[key].append(s)

        return list(groups.values())

# 测试
sol = Solution()
print(sol.groupAnagrams(["eat", "tea", "tan", "ate", "nat", "bat"]))
# 输出：[['eat', 'tea', 'ate'], ['tan', 'nat'], ['bat']] 顺序不限
```

### 复杂度分析

| 维度 | 复杂度 | 说明 |
|------|--------|------|
| 时间 | O(n * k) | n 是单词个数，k 是单词的最大长度。每个单词只需一次线性遍历统计，不需要排序 |
| 空间 | O(n * k) | 同样要存储所有字符串，但额外存放的元组长度固定为 26，比排序法更省空间（对于很长字符串） |

- **与解法一对比**：  
  当单词长度 k 很大时（例如 100），解法二 O(k) 明显快于解法一的 O(k log k)。  
  但实际中由于 Python 中排序速度很快，且 k ≤ 100，两者差距不大。解法二更加“数学优雅”。

---

## 解法对比总结

| 解法 | 时间复杂度 | 空间复杂度 | 推荐场景 |
|------|-----------|-----------|----------|
| 解法一：排序 + 哈希表 | O(n * k log k) | O(n * k) | 代码简单直观，单词较短时首选 |
| 解法二：计数 + 哈希表 | O(n * k) | O(n * k) | 单词很长（k > 100）、追求理论最优 |

- **零基础推荐**：先掌握解法一，因为排序的思路非常自然，代码也短。
- **面试建议**：两种方法都可以。解法二能体现你对字符编码的熟悉，适合提及“因为只有小写字母，所以可以用计数数组代替排序”。

两种方法都利用了哈希表将“相同特征的字符串”聚合到一起，核心思想是 **找到字母异位词的不变量**（排序后的字符串 / 字母计数元组）。