简单易懂的leetcode 100题-第一篇 字母异位词分组

前言

我们来到了第二道题目,第二道题目为什么不是第二篇呢。因为计算机专业都是从0开始计位😆

题目 49.字母异位词分组 【中等】

这是leetcode热题的第二道题目。和two sum一样属于哈希的题目。 不过这道题目也有多张不同的解法,难度中等。 对于中等难度的题目,大部分都是有简单的时间的思路。我们在有多种解法的时候可以选择最容易实现的解法。

leetcode.cn/problems/gr...


题目描述

给你一个字符串数组 strs,请你将 字母异位词 组合在一起。可以按任意顺序返回结果列表。

字母异位词 是由重新排列源单词的所有字母得到的一个新单词。


示例 1:

css 复制代码
输入:strs = ["eat", "tea", "tan", "ate", "nat", "bat"]
输出:[["eat","tea","ate"],["tan","nat"],["bat"]]

示例 2:

lua 复制代码
输入:strs = [""]
输出:[[""]]

示例 3:

lua 复制代码
输入:strs = ["a"]
输出:[["a"]]

提示:

  • 1 <= strs.length <= 10⁴
  • 0 <= strs[i].length <= 100
  • strs[i] 仅包含小写字母

题目分析

看到这个题目首先觉得,其实用眼睛看就可以看出答案了。人脑对小数据集的计算和视觉分析是非常厉害,人类的进化出来的天赋吧。 不过我们毕竟是做题目所以要写出计算机能够理解的解题步骤。

排序法

  1. 理解题目 什么是字母异位词,简单来说,就是两个或多个词语虽然它们的字母顺序不同,但包含的字母以及每个字母的数量是完全一样的。换句话说,如果把一个词中的字母重新排列组合能得到另一个词,那么这两个词就是字母异位词。

  2. 题目给出一个单词数组,我们要做的是找出其中是字母异位的词,把他们进行分组。 比如:"eat", "tea", "tan", "ate", "nat", "bat"。 其中肉眼观察eat,eat,ate是字母异位词。tan和nat一组。bat自己一组。

  3. 先试着人脑自然思考分组的时候,其实我们做了一个什么操作。把eat按照某种顺序重新排序和后面的单词进行比较。eat重新排序后可以表示位tea,和后面的tea完全一样。符合条件可以分为一组。

  4. 但是某种顺序到底用什么顺序好呢?我们要统一用一种顺序,写起来方便。要不然一会用这个,一会用那个乱套了。一般就使用字母的自然顺序,也就是abcdefg字母从小到大的顺序,常见,并且各种语言里面都是默认按照自然顺序提供的sort方法。

  5. 这样eat->aet, tea->aet 排序转化后比较完全相等。他们就是字母异位词。

  6. 在这个过程里面我们如何快速找到排序后相同的字母异位词呢?暴力做法是遍历整个数组一次,这样很容易超时。上一篇有讲到,用哈希结构。哈希结构帮助我们提前分组,可以快速定位到一个元素是否存在。

  7. 哈希结构就是Map<String, List>。例如: aet作为key,value是原始的词的数组。这样就把互为字母异位词的数据分到了一个List里面。 示例结构:

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

总结:eat,aet,tea本质上是相同的字母顺序乱了。我们总过排序他们归到固定的字母从小到大的顺序后,结果相同的就是字母异位词。在处理的过程中使用哈希表来存储按照从小到大重排序的单词,可以快速找到相同的单词。

字母计数法

字母计数法本质上是桶排,或者说哈希思想的一种应用。

想象一个游戏:颜色小球分类游戏

  1. 准备26个桶

    就像你有26个颜色的桶,对应英文字母的26个字母(a到z)。每个桶只装一种颜色的小球,比如a是红色,b是橙色,c是黄色......依此类推。

  2. 装小球

    你现在有一个单词,比如"eat",你就把一个红色小球(e)、一个蓝色小球(a)、一个绿色小球(t)分别丢进相应的桶里。

  3. 再来一个单词

    比如再来一个单词"tea",你也丢一个t、一个e、一个a进去------丢到的桶跟上一个单词一样,丢进去的颜色和数量也一样。

  4. 比较桶里的小球

    我们不看单词长啥样,只看桶里的小球颜色和数量是不是一模一样。如果一样,那就说明这两个单词其实就是用同样的字母组成的,只是顺序不一样------它们就是字母异位词

ps:字母计数法的思想在做字符串统计的相关题目的时候经常会使用。 统计字符串里面字符出现的次数,对应都有哪些字符。

Java 复制代码
int[] dict = new int[26];
for(char c: str){
    dict[c - 'a']++; // char本质上是int类型数据,映射'a'为0位置。
}

两种方法时间和空间复杂度比较

方法 时间复杂度 空间复杂度 适用场景
排序法 O(n * k log k) O(n * k) 所有字符集合
字母计数法 O(n * k) O(n) 固定字母集(如a-z)

排序的时间复杂度

  • 假设每个单词长度是 k,总共有 n 个单词。
  • 每个单词要排序,排序复杂度是 O(k log k)。
  • 所以总时间是:O(n * k log k)

排序的空间复杂度:

  • 排序时需要额外的空间存排序结果,平均是 O(k),总共是 O(n * k)。

字母计数法时间复杂度:

  • 每个单词扫描一次,构建频率数组是 O(k)
  • 总共是:O(n * k)
  • 数组转换成字符串(作为哈希 key)是常数时间,因为长度是固定的 26。

字母计数法空间复杂度:

  • 每个单词对应一个长度为 26 的数组,所以空间是 O(n * 26),也就是 O(n) 级别。

ps:虽然理论上来说字母计数法的时间复杂度要小于排序,不过因为leetcode上测试单词都很短,所以我跑出来的结果是排序的时间要小于字母计数法的时间。

java 排序实现

arduino 复制代码
class Solution {
    public List<List<String>> groupAnagrams(String[] strs) {
        Map<String, List<String>> map = new HashMap<>();
        
        for (String str : strs) {
            char[] arr = str.toCharArray();
            Arrays.sort(arr); // 排序归一 
            String key = new String(arr);
            map.computeIfAbsent(key, k -> new ArrayList<>()).add(str); // 如果存在就加入到list里面,不存在就创建新的list加入进去
        }

        return new ArrayList<>(map.values());
    }
}

c++ 排序实现

arduino 复制代码
class Solution {
public:
    vector<vector<string>> groupAnagrams(vector<string>& strs) {
        unordered_map<string, vector<string>> map;

        for (string& str : strs) {
            string key = str;
            sort(key.begin(), key.end()); // 排序作为key
            map[key].push_back(str);//c++ 不存在会默认初始化。
        }

        vector<vector<string>> result;
        for (auto& pair : map) {
            result.push_back(pair.second);
        }
        return result;
    }
};

java 字母计数法

arduino 复制代码
class Solution {
    public List<List<String>> groupAnagrams(String[] strs) {
        Map<String, List<String>> map = new HashMap<>();

        for (String str : strs) {
            int[] count = new int[26];
            for (char c : str.toCharArray()) {
                count[c - 'a']++;
            }

            // 构建唯一字符串key(例如:#1#0#2#0...)
            StringBuilder sb = new StringBuilder();
            for (int i = 0; i < 26; i++) {
                sb.append('#'); // 避免 "11" 与 "1|1" 混淆
                sb.append(count[i]);
            }
            String key = sb.toString();

            map.computeIfAbsent(key, k -> new ArrayList<>()).add(str);
        }

        return new ArrayList<>(map.values());
    }
}

C++ 字母计数法

arduino 复制代码
class Solution {
public:
    vector<vector<string>> groupAnagrams(vector<string>& strs) {
        unordered_map<string, vector<string>> map;

        for (string& str : strs) {
            int count[26] = {0};
            for (char c : str) {
                count[c - 'a']++;
            }

            // 构建 key:#1#0#2#0...
            string key;
            for (int i = 0; i < 26; ++i) {
                key += "#" + to_string(count[i]);
            }

            map[key].push_back(str);
        }

        vector<vector<string>> result;
        for (auto& pair : map) {
            result.push_back(pair.second);
        }
        return result;
    }
};

总结

排序,桶排,哈希的思想在很多题目都会常见。通过做100道具有代表的题目,总结解题的方法,来解决没有做过的类似题目。 下一道题见 🚀

相关推荐
十年Java程序媛1 小时前
Java this 与 super 详解|继承高频踩坑 + 面试题整理
java·spring boot·面试
萧瑟余晖1 小时前
Spring Cloud配置中心与动态刷新详解
spring cloud·面试
白露与泡影3 小时前
一线 Java 后端面试八股(2026 更新版,含完整答案)
java·开发语言·面试
学到老活到老7 小时前
集合换成线程安全的,就不用加锁了吗?
java·面试
萧瑟余晖9 小时前
Spring Cloud Alibaba生态整合详解
spring·spring cloud·面试
Sailing10 小时前
【封闭结束2】Agent 意图识别:从语义理解到稳定路由
人工智能·面试·agent
Sailing10 小时前
【封闭结束1】端到端交付:Agent 一次请求如何走完全链路
人工智能·面试·agent
怕浪猫19 小时前
红利窗口开着,程序员别在场边囤课
面试·github·ai编程
lll23526917791 天前
HR如何快速整理面试沟通内容?
面试·职场和发展
零售校长1 天前
西湖区零售门店店长面试怎么准备
面试·职场和发展·零售