Leetcode 49 用 “身份证‘‘巧解

Leetcode 49 用 "身份证''巧解

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

第一反应

字母异位词不看顺序,只看两件事:

text 复制代码
包含哪些字母
每个字母分别出现了几次

比如:

text 复制代码
eat
tea
ate

虽然顺序完全不一样,但它们都是:

text 复制代码
a:1
e:1
t:1

所以显然应该被分到一起。

问题是,怎么分?


我最开始想的是:拿一个字符串当"母本"

比如:

text 复制代码
["eat", "tea", "tan", "ate", "nat", "bat"]

先拿:

text 复制代码
eat

然后一个个和后面的字符串比较:

text 复制代码
eat vs tea
eat vs tan
eat vs ate
...

如果发现:

text 复制代码
tea
ate

和它是字母异位词,就把它们放进同一个列表。

这个思路乍看没问题。

但很快就出现一个让我很烦的问题:

text 复制代码
eat 已经和 tea 匹配了
eat 又和 ate 匹配了

那之后遍历到 tea 的时候怎么办?
要不要跳过?
怎么知道它已经被分过组?

如果继续这样想,很容易变成:

text 复制代码
字符串之间两两比较
+
还要记录哪些字符串已经处理过

越写越麻烦。

后来才发现,问题其实出在一开始的方向上。

根本没有必要让字符串之间互相比。


真正的关键:给每个字符串生成一张"身份证"

既然字母异位词的本质是:

text 复制代码
每个字母出现的次数完全相同

那就可以把这个统计结果直接当成它的"身份证"。

比如:

text 复制代码
eat

对应:

text 复制代码
a:1
e:1
t:1
其他字母:0

而:

text 复制代码
tea

得到的统计结果完全一样。

这样事情就简单了:

text 复制代码
身份证一样       → 放进同一组

身份证不一样     → 新建一组

这样就完全不需要考虑:

text 复制代码
谁和谁匹配
谁已经匹配过
下一次要不要跳过

这些问题直接消失了。


那这张"身份证"怎么做?

因为题目已经限定:

text 复制代码
字符串只包含小写字母

所以一共就:

text 复制代码
26 个字母

可以直接准备一个长度为 26 的列表:

python 复制代码
count = [0] * 26

也就是:

text 复制代码
下标:
0  1  2  3  4 ... 25

对应:
a  b  c  d  e ... z

这里我一开始又卡了一下:

字符是 a、e、t,我要怎么找到它们对应的下标?

后来想起来 Python 有 函数可以 把小写字符转为数字:

text 复制代码
ord()

例如:

python 复制代码
ord('a') == 97
ord('b') == 98

所以:

python 复制代码
ord(ch) - ord('a')

正好可以把:

text 复制代码
a → 0
b → 1
c → 2
...
z → 25

于是:

python 复制代码
for ch in word:
    count[ord(ch) - ord('a')] += 1

例如 "eat":

text 复制代码
e → count[4] += 1
a → count[0] += 1
t → count[19] += 1

最后就得到这整个字符串的字符统计。


中间我还想过:能不能用 set?

这个也不行。

因为 set 只能告诉我们:

text 复制代码
有什么字母

但不能告诉我们:

text 复制代码
每个字母有几个

比如:

text 复制代码
abb
ab

转成 set:

python 复制代码
set("abb") == {'a', 'b'}
set("ab")  == {'a', 'b'}

看起来一样。

但它们显然不是字母异位词。

所以这里必须保存:

text 复制代码
字符出现次数

不能只保存"字符是否出现过"。


接下来才真正轮到哈希表

这时候我们需要一个字典:

python 复制代码
groups = {}

它的结构可以理解成:

text 复制代码
key
→ 字符统计结果,也就是"身份证"

value
→ 所有拥有这张身份证的字符串

例如最后可能是:

text 复制代码
身份证 A → ["eat", "tea", "ate"]

身份证 B → ["tan", "nat"]

身份证 C → ["bat"]

这样每来一个字符串:

text 复制代码
字符串
↓
统计 26 个字母
↓
生成身份证
↓
去 groups 里找

如果这个身份证已经存在:

text 复制代码
直接 append 到原来的组

如果不存在:

text 复制代码
创建一个新的列表

这就比"字符串之间互相比"干净很多。


但这里又出现了一个问题:list 不能当字典的 key

我们统计出来的:

text 复制代码
count

是一个列表,例如:

text 复制代码
[1, 0, 0, 0, 1, ...]

但 Python 的列表不能直接作为字典的 key。

原因简单理解就是:

text 复制代码
list 可以被修改

而字典的 key 需要保持稳定。

所以这里把它转成元组:

python 复制代码
key = tuple(count)

元组和列表很像:

python 复制代码
[1, 2, 3]   # list
(1, 2, 3)   # tuple

但是 tuple 创建以后不能修改,因此可以作为字典的 key。

这题里其实不用把元组想得特别复杂,只需要记住:

text 复制代码
count 列表
↓
tuple(count)
↓
变成可以作为哈希表 key 的身份证

就够了。


我第一次写出来的代码也有几个小错误

我当时写的是类似:

python 复制代码
count[ord[i] - 97] += 1

但 ord 是函数,所以应该用:

python 复制代码
ord(i)

而且比起直接记 97,更推荐:

python 复制代码
ord(i) - ord('a')

这样一眼就知道是在算:

text 复制代码
当前字符 - a = 0~25 的下标

最后返回

我一开始也写成了取某一个元素。

但我们真正需要的不是某一个分组,而是 字典里所有的分组。

前面的 groups 大概会长成这样:

python 复制代码
groups = {
    key1: ["eat", "tea", "ate"],
    key2: ["tan", "nat"],
    key3: ["bat"]
}

题目最终需要的是:

text 复制代码
[
    ["eat", "tea", "ate"],
    ["tan", "nat"],
    ["bat"]
]

也就是说:

我们只需要字典里的所有 value,不再需要 key。

Python 字典可以通过:

python 复制代码
groups.values()

取出所有 value。

不过 values() 返回的不是普通的 list,而是一个字典的视图对象:

text 复制代码
dict_values([['eat', 'tea', 'ate'], ['tan', 'nat'], ['bat']])

所以如果想让它变成题目要求的列表形式,还要再套一层:

python 复制代码
list(...)

于是, 最后使用这个用于返回值:

python 复制代码
return list(groups.values())

就是:

text 复制代码
先取出 groups 中所有 value
↓
再把这些 value 转成 list

还有其他类似的字典操作:

python 复制代码
groups.keys()

取所有 key,

python 复制代码
groups.items()

取 key-value 对。


最后得到的代码

python 复制代码
class Solution:
    def groupAnagrams(self, strs: List[str]) -> List[List[str]]:

        groups = {}

        for word in strs:
            count = [0] * 26

            for ch in word:
                count[ord(ch) - ord('a')] += 1

            key = tuple(count)

            if key not in groups:
                groups[key] = [word]
            else:
                groups[key].append(word)

        return list(groups.values())

整个过程其实就是:

text 复制代码
遍历字符串
↓
统计 26 个字母出现次数
↓
把统计结果作为"身份证"
↓
身份证相同的字符串自动进入同一组
↓
最后取出所有组

还能继续改进吗?

可以,不过现在已经不是"算法思路有问题"了,而是可以让代码更简洁。

defaultdict

Python 里可以使用:

python 复制代码
defaultdict(list)

这样就不用每次手动判断:

python 复制代码
if key not in groups:

可以直接写:

python 复制代码
from collections import defaultdict

class Solution:
    def groupAnagrams(self, strs: List[str]) -> List[List[str]]:

        groups = defaultdict(list)

        for word in strs:
            count = [0] * 26

            for ch in word:
                count[ord(ch) - ord('a')] += 1

            key = tuple(count)

            groups[key].append(word)

        return list(groups.values())

因为 defaultdict(list) 的意思就是:

text 复制代码
这个 key 已经有列表
→ 直接用

这个 key 还没有
→ 自动帮我创建一个空列表

所以:

python 复制代码
groups[key].append(word)

一行就把两种情况都处理了。

排序

排序法只是换了一种生成 key 的方式,后面的哈希表分组逻辑完全一样。

比如:

text 复制代码
eat → aet
tea → aet
ate → aet

排序后一样,就分到同一组。

会发现它们最后都会变成:

text 复制代码
aet

所以:

排序后的字符串,也可以直接作为这一组异位词的"身份证"。

这样就不需要再统计 26 个字母出现的次数。

整个思路就变成:

text 复制代码
原字符串
↓
把字符排序
↓
得到统一形式
↓
作为哈希表的 key
↓
相同 key 的字符串放进同一个列表

排序作为key"身份证" 的代码为:

python 复制代码
key = ''.join(sorted(word))

为什么 sorted() 可以直接排字母?

Python 的 sorted() 不只可以排序数字,也可以排序字符。

例如:

python 复制代码
sorted("tea")

会得到:

text 复制代码
['a', 'e', 't']

因为字符本身也有对应的编码顺序,小写字母可以按照:

text 复制代码
a < b < c < ... < z

进行排序。

所以:

python 复制代码
sorted("eat")
sorted("tea")
sorted("ate")

最后都会得到:

text 复制代码
['a', 'e', 't']

为什么还要用 join()?

sorted() 返回的是一个 列表:

text 复制代码
['a', 'e', 't']

但我们希望得到的是一个完整字符串:

text 复制代码
"aet"

所以要用:

python 复制代码
''.join(...)

把列表里的字符重新拼起来。

例如:

python 复制代码
''.join(['a', 'e', 't'])

得到:

text 复制代码
'aet'

这里前面的:

text 复制代码
''

表示:

用什么东西把这些字符连接起来。

现在是空字符串,所以字符会直接连在一起。

例如:

python 复制代码
'-'.join(['a', 'e', 't'])

得到:

text 复制代码
a-e-t

所以这一句:

python 复制代码
key = ''.join(sorted(word))

其实可以拆成:

python 复制代码
chars = sorted(word)
key = ''.join(chars)

例如:

python 复制代码
word = "tea"

chars = sorted(word)
# ['a', 'e', 't']

key = ''.join(chars)
# "aet"

这样 "eat"、"tea"、"ate" 最后都会得到同一个:

python 复制代码
key = "aet"

后面还是哈希表分组

这一点其实和前面的计数法完全一样。

例如:

python 复制代码
groups = {
    "aet": ["eat", "tea", "ate"],
    "ant": ["tan", "nat"],
    "abt": ["bat"]
}

这里:

text 复制代码
key
→ 排序后的字符串

value
→ 属于这一组的原字符串

所以每来一个字符串:

text 复制代码
字符串
↓
排序生成 key
↓
去 groups 中找这个 key
↓
存在 → 加入原来的列表
不存在 → 新建一组

完整代码

python 复制代码
from collections import defaultdict

class Solution:
    def groupAnagrams(self, strs: List[str]) -> List[List[str]]:

        groups = defaultdict(list)

        for word in strs:
            key = ''.join(sorted(word))
            groups[key].append(word)

        return list(groups.values())

排序法和计数法有什么区别?

其实两种方法后半部分完全一样:

text 复制代码
生成身份证
↓
作为哈希表 key
↓
相同 key 自动分组

区别只在于:

text 复制代码
计数法:
身份证 = 26 个字母各自出现了多少次

排序法:
身份证 = 排序后的字符串

不过它们的 时间复杂度 稍微不同。

假设一个字符串长度为 k。

排序法需要排序:

text 复制代码
O(k log k)

而计数法只需要扫描一次字符:

text 复制代码
O(k)

所以从效率上看,计数法通常更好。

但排序法的优点也很明显:

非常直观。

因为:

text 复制代码
异位词
↓
排序以后一定完全一样

这个思路很容易理解

相关推荐
老歌老听老掉牙1 小时前
基于STL模型的轴向截面轮廓提取与三维可视化方法
python·stl
Li_RuiQi1 小时前
rtx4090_pi0_training_pitfalls
人工智能·机器学习
ouynagda1 小时前
嵌入式Linux i.MX6ULL系统移植学习笔记
linux·笔记·学习
liuchangng1 小时前
类Jev项目Kev从入门到实战(7):如何选型:决策树与 checklist
算法·决策树·机器学习
IT研究室1 小时前
最新大数据毕业设计选题推荐-基于大数据的人工智能社交媒体情绪分析与可视化的设计与实现-大数据-Spark-Hadoop-Bigdata
大数据·人工智能·课程设计
yi0111 小时前
DAY22: LeetCode 733:图像渲染——从二维网格开始理解 DFS 和 BFS
数据结构·笔记·python·算法·leetcode·深度优先·宽度优先
Figo_Cheung1 小时前
Figo生成式人工智能潜空间计算最优参数规模研究
人工智能·空间计算
这张生成的图像能检测吗1 小时前
(论文速读)一种用于图像超分辨率的有效扩散变换结构
人工智能·计算机视觉·扩散模型·超分辨率
Eric.461 小时前
ComfyUI 本地部署 AI 漫剧与 AI 视频流水线:消费级显卡显存优化、角色一致性工程实战
人工智能·自动化·音视频·comfyui·ai漫剧