法算day2

法算day2
lief字母异位词分组
问题描述
给你一个字符串数组,请你将 字母异位词 组合在一起。可以按任意顺序返回结果列表。
示例 1:
输入: strs = [“eat”, “tea”, “tan”, “ate”, “nat”, “bat”]
输出: [[“bat”],[“nat”,”tan”],[“ate”,”eat”,”tea”]]
解释:
在 strs 中没有字符串可以通过重新排列来形成 “bat”。
字符串 “nat” 和 “tan” 是字母异位词,因为它们可以重新排列以形成彼此。
字符串 “ate” ,”eat” 和 “tea” 是字母异位词,因为它们可以重新排列以形成彼此。
示例 2:
输入: strs = [“”]
输出: [[“”]]
示例 3:
输入: strs = [“a”]
输出: [[“a”]]
提示:
1 <= strs.length <= 104
0 <= strs[i].length <= 100
strs[i] 仅包含小写字母
java版
方法一:排序签名,即对字符排序
1 | import java.util.*; |
✅ 说明:
算法复杂度
时间:O(n·k log k),n 为字符串个数,k 为字符串最大长度(排序耗时)。
空间:O(n·k),用于存储 map 与结果。
代码疑惑点答疑
疑点一
public List<List<String>> groupAnagrams(String[] strs) 这一长串看起来吓人,其实每个关键字都在“报户口”——告诉你 谁都能调用、给的是字符串数组、还回来的是两层口袋的字符串。一句话拆 6 段解释:
public
“大家都能用”。换成private就只能类内部自己玩,别人调不到。List<List<String>>
返回类型:两层 List 嵌套。- 外层
List的每一项代表 一个分组; - 内层
List<String>装的是 该分组里的所有字符串。
为什么用接口List而不用具体类?
面向接口编程:调用者只关心“它是个列表”,不关心你背后是ArrayList还是LinkedList,给实现留余地。
- 外层
groupAnagrams
方法名,见名知意:把 anagrams(字母异位词)group(分组)。(String[] strs)
形参:一个字符串数组,里面扔进来一堆原始单词,等待被分类。为什么没有
static?
需要 先 new 一个对象 再调用,这样方法内部可以访问到对象级别的字段(虽然现在没用,但预留扩展)。如果写成static,就成了工具方法,属于类本身,无法访问实例成员。为什么泛型要写两次
<String>?
Java 泛型不支持“裸”嵌套,必须写全。List<List<String>>读作:一个列表,里面的元素还是“装字符串的列表”。
总结:
这句声明就是一份 方法招牌——“公开营业,收字符串数组,返分组列表,欢迎来调”。
疑点二
return new ArrayList<>(); 这一行干的事情可以拆成三步看:
new ArrayList<>()
在堆上真正创建了一个 空的ArrayList对象(底层数组长度默认 0,第一次 add 时才会扩容)。泛型推断
因为方法返回值已经被编译器确定为List<List<String>>,所以这里的<>被自动推断成ArrayList<List<String>>();你不用重复写泛型参数。return 出去
你拿到手的 实际上 是一个:- 空的、
- 可变的、
- 顺序保持的、
- 允许重复元素的
ArrayList实例,只不过对外展现的类型是它的接口List<List<String>>。
一句话:
“别人眼里你只是‘空列表’,本质上就是刚出炉、还没装任何分组的 ArrayList 对象。”
疑点三
for (String s : strs) 是 Java 的“增强版 for 循环”(也叫 foreach),它把“索引、边界判断、自增”这些脏活全部藏起来,让你只关心 每个元素本身。
逐字拆解:
for (
循环关键字,开场白。
String s
循环变量:每次进入循环体,都会把当前拿到的字符串塞进这个局部变量 s 里,类型必须和数组元素一致(这里是 String)。
:
读作“在……里面”,官方叫“in”。
strs
要被遍历的 数组或 Iterable 集合,这里是我们传进来的 String[] strs。
执行流程(伪代码等价):
1 | for (int i = 0; i < strs.length; i++) { |
索引 i 由编译器自动生成,但你 看不见、也用不到。
特点
只能 从前到后 一趟扫,不能跳着走,也不能反向。
不能修改数组长度(不能 add/remove),但 可以读取、可以修改元素本身(如果元素是可变对象)。
代码更短,少了下标越界风险,可读性高。
在本例里:
每轮循环 s 依次代表 “eat”、”tea”、”tan”……直到最后一个单词,循环自动结束。
你只需要把注意力放在“拿到一个字符串后怎么生成签名、怎么塞进 map”即可,下标那些事编译器全包。
疑点四
把这一行拆成“两句话”看,就秒懂:
map.computeIfAbsent(key, k -> new ArrayList<>())
意思是:
“如果 map 里还没有这个 key,就新建一个 空 ArrayList 并放进 map,然后把这个 List 返回;
如果已经存在,就直接把原来那个 List 返回。”
.add(s)
拿到上一步返回的 List(不管是刚新建的还是原来就有的),把当前字符串 s 塞进去。
所以整行等价于:
1 | List<String> list = map.get(key); // 先查 |
只是 computeIfAbsent 把“查-判-建-放”四步一次性做完,并直接返回那个 一定存在的 List,让你立刻能 .add(s)。
变量名 k 是 Lambda 的形参,代表 key,这里没用到,起个占位符名字即可。
重点理解
下面用一个完整的字符串当例子,把map.computeIfAbsent(key, k -> new ArrayList<>()).add(s);
的每一步都“慢放”给你看。
假设当前字符串 s = “tea”
生成签名
char[] chars = “tea”.toCharArray() → 排序 → {‘a’,’e’,’t’} → key = “aet”看 map 里有没有 key=”aet”
此时 map 还是空的,get(“aet”) 会返回 null。进入 computeIfAbsent
因为找不到,于是执行 Lambda:k -> new ArrayList<>()
新建一个空 ArrayList,把它放进 map,并把这个 List 返回。
此刻内存里变成:
map = {“aet” → []} (空列表)返回的 List 立刻 .add(s)
[].add(“tea”) → 列表变成 [“tea”]
最终 map 是:
{“aet” → [“tea”]}下一个循环如果碰到 “eat”
签名还是 “aet”,这次 map 里已存在,computeIfAbsent 直接返回原列表 [“tea”],
再 .add(“eat”) → 列表变成 [“tea”, “eat”],无需再建 ArrayList。
一句话:
“第一次来就给你新建个口袋,后面再来同名签名直接往旧口袋里继续扔。”
python版
1 | from collections import defaultdict # 引入一个方便的字典类,后面会解释 |









