0

0

Java实现无排序分组同字母异位词:哈希映射与字符计数详解

DDD

DDD

发布时间:2025-10-14 11:09:12

|

336人浏览过

|

来源于php中文网

原创

Java实现无排序分组同字母异位词:哈希映射与字符计数详解

本文详细阐述了一种在java中高效分组同字母异位词的方法,该方法通过利用字符频率计数数组作为哈希映射的键,避免了对字符串进行排序,从而优化了性能。文章深入探讨了字符频率数组如何作为唯一标识符,并提供了具体的代码实现、详细的时间复杂度分析以及相关注意事项,旨在为开发者提供一个清晰且专业的教程。

引言

在编程挑战中,分组同字母异位词(Group Anagrams)是一个常见问题。同字母异位词是指由相同字母但顺序不同的字符串组成,例如 "eat"、"tea" 和 "ate" 都是同字母异位词。解决这类问题的一个直观方法是对每个字符串进行排序,然后将排序后的字符串作为哈希表的键。然而,这种方法的时间复杂度通常较高,因为它涉及到对每个字符串进行排序操作。本文将介绍一种更高效的无排序方法,通过字符频率计数来为同字母异位词生成唯一的哈希键。

核心思想:字符频率计数作为哈希键

同字母异位词的本质在于它们拥有相同的字符集合和每个字符的相同出现次数。例如,"listen" 和 "silent" 都包含一个 'l'、一个 'i'、一个 's'、一个 't'、一个 'e' 和一个 'n'。基于这一特性,我们可以创建一个“签名”来唯一标识一组同字母异位词,而无需对字符串本身进行排序。

这个“签名”就是字符频率计数数组。对于只包含小写英文字母的字符串,我们可以使用一个长度为 26 的整型数组来记录每个字母的出现次数。数组的每个索引对应一个字母(例如,索引 0 对应 'a',索引 1 对应 'b',以此类推),存储该字母在字符串中出现的次数。

例如,对于字符串 "eat":

立即学习Java免费学习笔记(深入)”;

  • 'e' 出现 1 次
  • 'a' 出现 1 次
  • 't' 出现 1 次
  • 其他字母出现 0 次

对应的频率数组将是 [1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0](其中第0位是'a',第4位是'e',第19位是't')。所有与 "eat" 是同字母异位词的字符串,如 "tea" 和 "ate",都会生成完全相同的频率数组。

实现细节:从频率数组到哈希键

为了将这个频率数组用作 HashMap 的键,我们需要将其转换为一个不可变的、可哈希的对象,最常见且方便的方式是将其转换为 String 类型。Java 的 Arrays.toString(int[] a) 方法可以将一个整型数组转换为其字符串表示形式,例如 [1, 0, 0, ..., 1]。这个字符串表示形式将作为 HashMap 的键,而其值则是一个 List,用于存储所有具有相同频率数组(即同字母异位词)的原始字符串。

Type
Type

生成草稿,转换文本,获得写作帮助-等等。

下载

算法步骤:

  1. 初始化: 创建一个 HashMap> 来存储分组结果。键是字符频率数组的字符串表示,值是对应的同字母异位词列表。
  2. 遍历字符串数组: 迭代输入字符串数组中的每个字符串。
  3. 构建频率数组: 对于每个字符串,创建一个长度为 26 的 int 数组 input,并遍历字符串中的每个字符:
    • 通过 str.charAt(i) - 'a' 计算字符在字母表中的偏移量,将其作为 input 数组的索引。
    • 将 input[index] 的值递增,记录该字符的出现次数。
  4. 生成哈希键: 使用 Arrays.toString(input) 将频率数组转换为一个 String 类型的键 inputStr。
  5. 哈希映射操作:
    • 检查 outputMap 是否已包含 inputStr 作为键。
    • 如果存在,说明已经发现了一组同字母异位词,将当前字符串添加到 outputMap.get(inputStr) 对应的列表中。
    • 如果不存在,则创建一个新的 ArrayList,将当前字符串添加进去,然后将 inputStr 作为键,新创建的列表作为值,存入 outputMap。
  6. 收集结果: 遍历 outputMap 的所有值(即所有的 List),将它们添加到一个新的 List> 中作为最终结果返回。

代码示例

以下是基于上述思想的 Java 实现:

import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

public class AnagramGrouper {

    /**
     * 将一组字符串中的同字母异位词进行分组。
     *
     * @param strs 待分组的字符串数组。
     * @return 包含同字母异位词分组的列表。
     */
    public List> groupAnagrams(String[] strs) {
        // 最终结果列表
        List> output = new ArrayList<>();
        // 处理空输入情况
        if (strs == null || strs.length == 0) {
            return output;
        }

        // 使用 HashMap 存储分组结果
        // 键:字符频率数组的字符串表示 (e.g., "[1, 0, ..., 1]")
        // 值:包含所有对应同字母异位词的列表
        Map> outputMap = new HashMap<>();

        // 遍历输入字符串数组
        for (String str : strs) {
            // 创建一个长度为 26 的数组,用于记录每个小写字母的出现次数
            // 索引 0 -> 'a', 索引 1 -> 'b', ..., 索引 25 -> 'z'
            int[] charCounts = new int[26];
            // 遍历当前字符串的每个字符,更新 charCounts 数组
            for (int i = 0; i < str.length(); i++) {
                charCounts[str.charAt(i) - 'a']++;
            }

            // 将字符频率数组转换为字符串,作为 HashMap 的键
            // 例如,对于 "eat",charCounts 可能是 [1,0,0,0,1,...,1],
            // Arrays.toString() 会将其转换为 "[1, 0, 0, 0, 1, 0, ..., 1, 0, 0, 0, 0, 0, 0]"
            String key = Arrays.toString(charCounts);

            // 检查 HashMap 中是否已存在该键
            if (outputMap.containsKey(key)) {
                // 如果存在,说明找到了一个同字母异位词,将其添加到对应的列表中
                outputMap.get(key).add(str);
            } else {
                // 如果不存在,创建一个新的列表,将当前字符串添加进去,并将其作为新的键值对存入 HashMap
                List newList = new ArrayList<>();
                newList.add(str);
                outputMap.put(key, newList);
            }
        }

        // 将 HashMap 中所有的值(即所有的同字母异位词列表)收集到最终结果列表中
        output.addAll(outputMap.values());
        return output;
    }

    public static void main(String[] args) {
        AnagramGrouper grouper = new AnagramGrouper();
        String[] strs1 = {"eat", "tea", "tan", "ate", "nat", "bat"};
        List> result1 = grouper.groupAnagrams(strs1);
        System.out.println("示例 1 分组结果: " + result1);
        // 预期输出可能为: [[bat], [nat, tan], [ate, eat, tea]] 或类似顺序

        String[] strs2 = {""};
        List> result2 = grouper.groupAnagrams(strs2);
        System.out.println("示例 2 分组结果: " + result2); // 预期输出: [[]]

        String[] strs3 = {"a"};
        List> result3 = grouper.groupAnagrams(strs3);
        System.out.println("示例 3 分组结果: " + result3); // 预期输出: [[a]]
    }
}

时间复杂度分析

该算法的时间复杂度主要由两个嵌套循环决定:

  1. 外层循环: 遍历输入字符串数组 strs,假设数组中有 m 个字符串。
  2. 内层循环: 对于每个字符串 str,遍历其所有字符以构建频率计数数组。假设字符串的平均长度为 n。

具体操作及复杂度:

  • 初始化 charCounts 数组: new int[26] 是 O(1) 操作。
  • 构建 charCounts 数组: 对于每个字符串,需要遍历其 n 个字符。每个字符的访问和数组更新是 O(1) 操作。因此,这一步的复杂度是 O(n)。
  • Arrays.toString(charCounts): 将长度为 26 的数组转换为字符串。这可以看作是 O(1) 操作(因为它总是处理固定大小的数组)。
  • HashMap 操作 (containsKey, get, put): 在理想情况下(哈希冲突较少),这些操作的平均时间复杂度是 O(1)。

综合计算:

  • 外层循环执行 m 次。
  • 在每次外层循环中:
    • 构建 charCounts 数组:O(n)
    • Arrays.toString():O(1)
    • HashMap 操作:O(1) (平均)
    • List.add():O(1) (平均)

因此,总的时间复杂度为 m * (n + 1 + 1 + 1),简化后为 m * (n + 3)。去除常数项后,最终的时间复杂度为 *O(m n)**。

与传统的排序方法(通常为 O(m * n log n))相比,这种基于字符频率计数的方法在字符串长度较大时具有显著的性能优势。

空间复杂度分析

  • outputMap: 在最坏情况下,所有字符串都不是同字母异位词,outputMap 将存储 m 个键值对。每个键是长度为 26 的数组的字符串表示(固定长度),每个值是一个 List,其中包含原始字符串。因此,存储所有原始字符串所需的空间是 O(m * n)(m 个字符串,平均长度 n),存储键所需的额外空间是 O(m * 26),可以简化为 O(m)。
  • output 列表: 最终结果列表也存储了所有 m 个字符串,所以空间复杂度为 O(m * n)。

综合来看,空间复杂度为 *O(m n)**。

注意事项与优化

  1. 字符集限制: 当前实现假定输入字符串只包含小写英文字母。如果字符串可能包含大写字母、数字或特殊字符,或者更广泛的 Unicode 字符,则需要调整 charCounts 数组的大小(例如,使用 256 或更大的数组,或者使用 HashMap 来计数)。
  2. 键的生成: Arrays.toString() 方法是生成键的一种简洁方式。另一种方法是手动构建一个字符串,例如使用 StringBuilder 将每个字符的计数及其对应的字符拼接起来(如 "a1e1t1"),但这会稍微增加复杂性,且 Arrays.toString() 通常已足够高效。
  3. 空字符串处理: 示例代码中包含了对空字符串数组和单个空字符串的处理,它们会被正确分组。
  4. 性能对比: 尽管 O(m n) 优于 O(m n log n),但对于非常短的字符串,排序方法的常数因子可能较小,实际性能差异可能不明显。然而,对于大多数实际场景,频率计数方法在渐近性能上更优。

总结

通过利用字符频率计数数组作为哈希映射的键,我们能够有效地分组同字母异位词,避免了耗时的字符串排序操作。这种方法不仅提供了清晰的逻辑,而且在时间复杂度上达到了 O(m * n),使其成为解决此类问题的推荐方案。理解其核心原理和实现细节,对于开发者在处理字符串和哈希表相关问题时具有重要的指导意义。

热门AI工具

更多
DeepSeek
DeepSeek

幻方量化公司旗下的开源大模型平台

豆包大模型
豆包大模型

字节跳动自主研发的一系列大型语言模型

通义千问
通义千问

阿里巴巴推出的全能AI助手

腾讯元宝
腾讯元宝

腾讯混元平台推出的AI助手

文心一言
文心一言

文心一言是百度开发的AI聊天机器人,通过对话可以生成各种形式的内容。

讯飞写作
讯飞写作

基于讯飞星火大模型的AI写作工具,可以快速生成新闻稿件、品宣文案、工作总结、心得体会等各种文文稿

即梦AI
即梦AI

一站式AI创作平台,免费AI图片和视频生成。

ChatGPT
ChatGPT

最最强大的AI聊天机器人程序,ChatGPT不单是聊天机器人,还能进行撰写邮件、视频脚本、文案、翻译、代码等任务。

相关专题

更多
string转int
string转int

在编程中,我们经常会遇到需要将字符串(str)转换为整数(int)的情况。这可能是因为我们需要对字符串进行数值计算,或者需要将用户输入的字符串转换为整数进行处理。php中文网给大家带来了相关的教程以及文章,欢迎大家前来学习阅读。

463

2023.08.02

mysql标识符无效错误怎么解决
mysql标识符无效错误怎么解决

mysql标识符无效错误的解决办法:1、检查标识符是否被其他表或数据库使用;2、检查标识符是否包含特殊字符;3、使用引号包裹标识符;4、使用反引号包裹标识符;5、检查MySQL的配置文件等等。本专题为大家提供相关的文章、下载、课程内容,供大家免费下载体验。

183

2023.12.04

Python标识符有哪些
Python标识符有哪些

Python标识符有变量标识符、函数标识符、类标识符、模块标识符、下划线开头的标识符、双下划线开头、双下划线结尾的标识符、整型标识符、浮点型标识符等等。本专题为大家提供相关的文章、下载、课程内容,供大家免费下载体验。

287

2024.02.23

java标识符合集
java标识符合集

本专题整合了java标识符相关内容,想了解更多详细内容,请阅读下面的文章。

258

2025.06.11

c++标识符介绍
c++标识符介绍

本专题整合了c++标识符相关内容,阅读专题下面的文章了解更多详细内容。

124

2025.08.07

js 字符串转数组
js 字符串转数组

js字符串转数组的方法:1、使用“split()”方法;2、使用“Array.from()”方法;3、使用for循环遍历;4、使用“Array.split()”方法。本专题为大家提供js字符串转数组的相关的文章、下载、课程内容,供大家免费下载体验。

298

2023.08.03

js截取字符串的方法
js截取字符串的方法

js截取字符串的方法有substring()方法、substr()方法、slice()方法、split()方法和slice()方法。本专题为大家提供字符串相关的文章、下载、课程内容,供大家免费下载体验。

212

2023.09.04

java基础知识汇总
java基础知识汇总

java基础知识有Java的历史和特点、Java的开发环境、Java的基本数据类型、变量和常量、运算符和表达式、控制语句、数组和字符串等等知识点。想要知道更多关于java基础知识的朋友,请阅读本专题下面的的有关文章,欢迎大家来php中文网学习。

1502

2023.10.24

java入门学习合集
java入门学习合集

本专题整合了java入门学习指南、初学者项目实战、入门到精通等等内容,阅读专题下面的文章了解更多详细学习方法。

1

2026.01.29

热门下载

更多
网站特效
/
网站源码
/
网站素材
/
前端模板

精品课程

更多
相关推荐
/
热门推荐
/
最新课程
Kotlin 教程
Kotlin 教程

共23课时 | 3万人学习

C# 教程
C# 教程

共94课时 | 7.9万人学习

Java 教程
Java 教程

共578课时 | 53.1万人学习

关于我们 免责申明 举报中心 意见反馈 讲师合作 广告合作 最新更新
php中文网:公益在线php培训,帮助PHP学习者快速成长!
关注服务号 技术交流群
PHP中文网订阅号
每天精选资源文章推送

Copyright 2014-2026 https://www.php.cn/ All Rights Reserved | php.cn | 湘ICP备2023035733号