news 2026/4/15 7:39:39

LeetCode 49. 字母异位词分组 | 从排序到计数的哈希表优化之路

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
LeetCode 49. 字母异位词分组 | 从排序到计数的哈希表优化之路

在 LeetCode 的字符串类题目中,「字母异位词分组」是一道经典的中等难度题,它不仅考察字符串处理的基础能力,更是对哈希表键值设计思路的深度检验。这道题的核心是找到字母异位词的共性特征,并通过这个特征实现分组。今天我们就从最直观的暴力思路出发,一步步拆解到时间复杂度更优的计数法,带你吃透这道题的解题逻辑~

📌 题目重述

给你一个由小写字母组成的字符串数组strs,要求把数组中字母异位词归为一组,最后以任意顺序返回分组后的列表。
这里的关键是理解字母异位词:两个字符串如果包含的字母完全相同,只是排列顺序不同,那它们就是字母异位词。比如eattea,都由eat组成,只是顺序不一样,就属于一组;而bat没有对应的异位词,单独成组。
举个例子,输入["eat", "tea", "tan", "ate", "nat", "bat"],输出就是[["bat"],["nat","tan"],["ate","eat","tea"]]

🚶 阶梯思路拆解

第一步:暴力思路(两两对比)🥾

刚开始接触这道题,最直接的想法是检查每两个字符串是否为字母异位词,然后手动分组。这是暴力解法的核心逻辑,虽然容易理解,但效率极低。

💡 核心逻辑

  1. 初始化一个结果列表,用于存储最终的分组;
  2. 遍历数组中的每个字符串s
    • 如果s还未被分组,创建一个新的子列表,将s加入;
    • 再遍历数组中剩下的字符串t,检查t是否与s是字母异位词,若是则加入同一个子列表,并标记t为已分组;
  3. 最终返回结果列表。

判断两个字符串是否为字母异位词的方法:将两个字符串排序后比较是否相等(比如eat排序后是aettea排序后也是aet,则为异位词)。

✅ 代码实现(Java)

importjava.util.*;publicclassSolution{publicList<List<String>>groupAnagrams(String[]strs){List<List<String>>result=newArrayList<>();boolean[]isGrouped=newboolean[strs.length];// 标记是否已分组for(inti=0;i<strs.length;i++){if(isGrouped[i])continue;// 跳过已分组的字符串List<String>group=newArrayList<>();group.add(strs[i]);isGrouped[i]=true;// 遍历剩余字符串,找异位词for(intj=i+1;j<strs.length;j++){if(!isGrouped[j]&&isAnagram(strs[i],strs[j])){group.add(strs[j]);isGrouped[j]=true;}}result.add(group);}returnresult;}// 判断两个字符串是否为字母异位词privatebooleanisAnagram(Strings,Stringt){if(s.length()!=t.length())returnfalse;char[]sArr=s.toCharArray();char[]tArr=t.toCharArray();Arrays.sort(sArr);Arrays.sort(tArr);returnArrays.equals(sArr,tArr);}}

⚙️ 复杂度分析

复杂度类型计算结果说明
时间复杂度O(n² * k log k)n 是数组长度,k 是字符串的最大长度。两层嵌套循环是 O (n²),每次判断异位词的排序操作是 O (k log k)
空间复杂度O(n)除了结果存储,仅使用了isGrouped数组,空间为 O (n)

🚫 遇到的问题

暴力解法的效率问题非常突出:当数组长度n达到 10⁴ 时,n² 就是 10⁸ 次运算,再加上字符串排序的开销,必然会超时。问题的核心在于重复的异位词判断(比如判断eattea后,又会判断teaate),我们需要找到一种方式,让所有异位词能自动归组,避免重复比较。

第二步:排序 + 哈希表(优化思路)🗺️

既然字母异位词排序后是完全相同的字符串,那我们可以把排序后的字符串作为哈希表的键,对应的值存储该组的所有异位词。这样遍历一次数组就能完成分组,彻底解决重复比较的问题。

💡 核心逻辑

  1. 初始化一个HashMap,键为排序后的字符串,值为该组异位词的列表
  2. 遍历数组中的每个字符串s
    • s进行排序,得到key
    • 如果key不在HashMap中,创建一个新的列表并放入HashMap
    • s添加到key对应的列表中;
  3. 遍历结束后,将HashMap中的所有值取出,即为最终的分组结果。

📊 图文演示(以 strs=[“eat”,“tea”,“tan”,“ate”,“nat”,“bat”] 为例)

(如图所示)我们一步步看 HashMap 的变化过程:

  1. 遍历eat:排序后为aet,HashMap 中无aet,创建列表["eat"],存入{aet: ["eat"]}
  2. 遍历tea:排序后为aet,HashMap 中有aet,将tea加入列表,变为{aet: ["eat", "tea"]}
  3. 遍历tan:排序后为ant,HashMap 中无ant,创建列表["tan"],存入{aet: [...], ant: ["tan"]}
  4. 遍历ate:排序后为aet,加入列表,aet对应的列表变为["eat", "tea", "ate"]
  5. 遍历nat:排序后为ant,加入列表,ant对应的列表变为["tan", "nat"]
  6. 遍历bat:排序后为abt,创建列表["bat"],最终 HashMap 为{aet: [...], ant: [...], abt: ["bat"]}
  7. 取出 HashMap 的值,得到结果[["eat","tea","ate"], ["tan","nat"], ["bat"]]

✅ 代码实现(Java)

importjava.util.*;publicclassSolution{publicList<List<String>>groupAnagrams(String[]strs){Map<String,List<String>>map=newHashMap<>();for(Strings:strs){// 将字符串排序作为键char[]charArr=s.toCharArray();Arrays.sort(charArr);Stringkey=newString(charArr);// 不存在则创建新列表if(!map.containsKey(key)){map.put(key,newArrayList<>());}// 将当前字符串加入对应列表map.get(key).add(s);}// 将map的值转换为结果列表returnnewArrayList<>(map.values());}}

⚙️ 复杂度分析

复杂度类型计算结果说明
时间复杂度O(n * k log k)n 是数组长度,k 是字符串最大长度。遍历数组是 O (n),每个字符串排序是 O (k log k)
空间复杂度O(n * k)HashMap 需要存储所有字符串,空间为 O (n * k)

✨ 优化亮点

这种方法将时间复杂度从 O (n² * k log k) 降到了 O (n * k log k),在 n 较大时效率提升非常明显,也是这道题的常用解法。但它仍有优化空间:字符串排序的 O (k log k) 开销可以通过字符计数进一步降低。

第三步:计数 + 哈希表(最优解法)🔢

由于题目规定字符串仅包含小写字母(共 26 个),我们可以用一个长度为 26 的数组统计每个字符出现的次数,再将这个计数数组转换为唯一的键(比如拼接成字符串#1#0#0#...#1),这样就能避免排序的开销。

💡 核心逻辑

  1. 初始化一个HashMap,键为字符计数的拼接字符串,值为该组异位词的列表
  2. 遍历数组中的每个字符串s
    • 创建长度为 26 的数组count,统计s中每个小写字母的出现次数(count[0]对应acount[1]对应b,以此类推);
    • count数组拼接为字符串(如eat的计数数组是[1,0,0,0,1,0,...1],拼接为#1#0#0#...#1),作为key
    • 如果key不在HashMap中,创建新列表;将s加入对应列表;
  3. 最终将HashMap的值转换为结果列表。

📊 图文演示(以 strs=[“eat”,“tea”] 为例)

(如图所示)计数法的键生成过程:

  1. 处理eat
    • e是第 4 个字母,count[4] +=1a是第 0 个字母,count[0] +=1t是第 19 个字母,count[19] +=1
    • 计数数组为[1,0,0,0,1,0,...,1](仅展示关键位置),拼接为#1#0#0#0#1#...#1作为key
  2. 处理tea
    • 统计后计数数组与eat完全相同,拼接的key也一致,因此被加入同一个列表。

✅ 代码实现(Java)

importjava.util.*;publicclassSolution{publicList<List<String>>groupAnagrams(String[]strs){Map<String,List<String>>map=newHashMap<>();for(Strings:strs){int[]count=newint[26];// 统计26个小写字母的出现次数for(charc:s.toCharArray()){count[c-'a']++;// 'a'对应0,'b'对应1...}// 将计数数组转换为字符串作为键StringBuildersb=newStringBuilder();for(intnum:count){sb.append('#').append(num);// 用#分隔避免数字混淆(如1和10)}Stringkey=sb.toString();if(!map.containsKey(key)){map.put(key,newArrayList<>());}map.get(key).add(s);}returnnewArrayList<>(map.values());}}

⚙️ 复杂度分析

复杂度类型计算结果说明
时间复杂度O(n * k)n 是数组长度,k 是字符串最大长度。遍历数组是 O (n),每个字符串的计数和拼接是 O (k)
空间复杂度O(n * k)HashMap 存储所有字符串,空间为 O (n * k)

✨ 优化亮点

这种方法彻底去掉了排序的 O (k log k) 开销,时间复杂度降到了线性的 O (n * k),是这道题的最优解法。需要注意的是,拼接计数数组时要使用分隔符(如#),避免出现110拼接后混淆的情况(比如count=[1,10]count=[11,0],不加分隔符都会变成110)。

📝 总结

「字母异位词分组」的解题思路,核心是找到异位词的唯一标识作为哈希表的键,我们从暴力的两两对比,到用排序生成键,再到用计数生成更高效的键,一步步实现了优化。这里的关键技巧可以总结为:

  1. 哈希表的键设计:针对同类元素的 **共性特征 **设计唯一键,是哈希表分组问题的核心;
  2. 利用题目约束优化:本题中仅包含小写字母的约束,让计数法替代排序成为可能;
  3. 时间与空间的权衡:三种解法的空间复杂度逐渐升高,但时间效率大幅提升,这是空间换时间的典型应用。

同类题扩展建议

掌握了这道题的思路后,可以尝试这些进阶题目:

  1. LeetCode 242. 有效的字母异位词:本题的基础子问题,练习字符计数的基本用法;
  2. LeetCode 438. 找到字符串中所有字母异位词 | … :滑动窗口 + 字符计数的综合应用;
  3. LeetCode 76. 最小覆盖子串 | 滑动窗口最优解全… :滑动窗口与哈希表结合的经典难题。

算法学习的本质就是从基础思路出发,不断根据题目特征优化解法。把这道题的键设计思路吃透,面对其他哈希表分组问题就能快速找到突破口啦~

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/4/13 13:17:46

亲测!高性价比AI数字员工租赁公司分享

亲测&#xff01;高性价比AI数字员工租赁公司分享行业痛点分析当前AI数字员工领域面临着诸多技术挑战。一方面&#xff0c;技术的通用性不足&#xff0c;不同行业的业务流程和需求差异巨大&#xff0c;现有的AI数字员工难以实现全行业的深度适配。测试显示&#xff0c;在一些复…

作者头像 李华
网站建设 2026/4/10 13:35:07

LLM本地推理全流程:基于LMDeploy的pipeline实战指南

在大语言模型&#xff08;LLM&#xff09;应用落地过程中&#xff0c;本地环境的高效推理部署是开发者面临的核心挑战之一。LMDeploy作为一款轻量级推理框架&#xff0c;通过其pipeline API为开发者提供了便捷的模型调用接口。本文将系统梳理LMDeploy pipeline的配置方法与高级…

作者头像 李华
网站建设 2026/4/13 12:54:38

如何用哔哩下载姬实现B站视频高效保存?5个技巧让你效率提升150%

如何用哔哩下载姬实现B站视频高效保存&#xff1f;5个技巧让你效率提升150% 【免费下载链接】downkyi 哔哩下载姬downkyi&#xff0c;哔哩哔哩网站视频下载工具&#xff0c;支持批量下载&#xff0c;支持8K、HDR、杜比视界&#xff0c;提供工具箱&#xff08;音视频提取、去水印…

作者头像 李华
网站建设 2026/4/11 2:11:41

C语言实现阶乘(附带源码)

一、项目背景详细介绍阶乘&#xff08;Factorial&#xff09;是数学中最基础、最常见的运算之一&#xff0c;记作&#xff1a;它广泛应用于&#xff1a;排列组合计算概率论数学级数数值分析算法竞赛递归函数教学栈帧结构教学大整数计算由于阶乘随 n 增大增长极快&#xff0c;因…

作者头像 李华