简单易懂的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道具有代表的题目,总结解题的方法,来解决没有做过的类似题目。 下一道题见 🚀

相关推荐
嶂蘅6 分钟前
OK!用大白话说清楚设计模式(二)
前端·后端·面试
AronTing24 分钟前
03-Spring Cloud Gateway 深度解析:从核心原理到生产级网关实践
后端·面试·架构
叶小秋43 分钟前
一个前端面试官的思考:当Promise遇上真实业务场景
前端·面试
小杨xyyyyyyy1 小时前
计算机网络 - 三次握手相关问题
服务器·网络·计算机网络·面试
北京_宏哥1 小时前
🔥Jmeter(十) - 从入门到精通 - JMeter逻辑控制器 - 中篇(详解教程)
前端·jmeter·面试
Aphasia3111 小时前
面试高频考题之双token机制💻
面试
顾林海1 小时前
深度解析TreeMap工作原理
android·java·面试
江城开朗的豌豆1 小时前
CSS篇:彻底搞懂CSS百分比边距:margin-top和padding-top的计算原理
前端·css·面试
AronTing1 小时前
02-Nacos 深度解析:从核心原理到生产实践
后端·面试·架构
江城开朗的豌豆1 小时前
CSS篇: 探索CSS3新特性:这些炫酷特性你都用过了吗?
前端·css·面试