本文详细介绍了如何使用 Java 语言,结合正则表达式(Lookaround)和 Stream API,对包含数字的字符串进行单词排序。通过将单词与对应的数字关联,并利用map进行存储,最终实现按数字顺序重组字符串,解决字符串乱序问题。
问题描述
在日常编程中,我们有时会遇到需要根据字符串中内嵌的数字来重新排列单词的需求。例如,给定字符串 “my1kiran4name2is3″,其中每个单词后面都跟着一个数字(1-9),表示该单词在最终序列中的位置。我们的目标是根据这些数字,将单词重新排列为 “my name is kiran”。
示例:
- my 对应数字 1
- kiran 对应数字 4
- name 对应数字 2
- is 对应数字 3
按数字顺序排列后,结果应为:my (1) name (2) is (3) kiran (4)。
解决方案:基于正则表达式与 Stream API
解决此问题的一种高效方法是结合使用 Java 的正则表达式(特别是零宽断言)和 Stream API。这种方法能够优雅地将单词与数字分离,并进行后续处理。
立即学习“Java免费学习笔记(深入)”;
核心思路
- 初始分割: 将整个字符串分割成“单词+数字”的片段(例如:”my1″, “kiran4″)。
- 二次分割与映射: 对每个片段,进一步将其分割为独立的单词和数字,并构建一个映射(Map),其中数字作为键,单词作为值。
- 排序与重组: 根据数字键的顺序,从映射中提取单词,并用空格连接起来形成最终的字符串。
步骤详解
我们将以字符串 “my1kiran4name2is3” 为例,逐步解释代码逻辑。
1. 初始分割:利用正向后行断言 ((?
第一步是将原始字符串根据数字进行分割,但要保留数字作为每个片段的结尾。这里使用正则表达式 (?
String string = "my1kiran4name2is3"; // string.split("(?<=d)") 的结果是:["my1", "kiran4", "name2", "is3"]
经过这一步,我们得到了一个包含“单词+数字”形式的字符串列表。
2. 二次分割与映射:利用正向先行断言 ((?=d)) 并构建 Map
接下来,对上一步得到的每个片段(如 “my1″),我们需要将其进一步分割成单词和数字。这里使用 s.split(“(?=d)”),(?=d) 是一个正向先行断言(Positive Lookahead)。它会匹配紧跟在一个数字 (d) 前面的位置,但同样不将数字本身包含在匹配结果中。这使得数字成为分割后的第二个元素。
// 以 "my1" 为例,s.split("(?=d)") 的结果是:["my", "1"] // 以 "kiran4" 为例,s.split("(?=d)") 的结果是:["kiran", "4"]
然后,我们使用 Stream API 的 collect(Collectors.toMap(…)) 方法将这些分割后的数组转换为一个 Map
Map<Integer, String> map = Arrays.asList(string.split("(?<=d)")) // 将分割结果转换为 List .stream() // 创建 Stream .map(s -> s.split("(?=d)")) // 对每个元素进行二次分割 .collect(Collectors.toMap(e -> Integer.parseInt(e[1]), e -> e[0])); // 最终得到的 map 可能是:{1="my", 4="kiran", 2="name", 3="is"} // 注意:HashMap 的迭代顺序不确定,下文会详细说明。
3. 重组字符串:连接 Map 中的值
最后一步是从 map 中提取单词并用空格连接。原始解决方案直接使用了 map.values().stream().collect(Collectors.joining(” “))。
string = map.values().stream() .collect((Collectors.joining(" "))); // 假设 map.values() 按键的顺序迭代,则结果为 "my name is kiran"
完整示例代码
import java.util.Arrays; import java.util.Map; import java.util.stream.Collectors; import java.util.TreeMap; // 导入 TreeMap 以确保排序 public class StringSorter { public static void main(String[] args) { String inputString = "my1kiran4name2is3"; System.out.println("原始字符串: " + inputString); // 步骤1 & 2: 分割并构建 Map Map<Integer, String> wordMap = Arrays.asList(inputString .split("(?<=d)")) // 第一次分割:["my1", "kiran4", "name2", "is3"] .stream() .map(s -> s.split("(?=d)")) // 第二次分割:[["my", "1"], ["kiran", "4"], ...] // 使用 TreeMap::new 确保 Map 内部按键排序 .collect(Collectors.toMap( e -> Integer.parseInt(e[1]), // 键:数字 e -> e[0], // 值:单词 (oldValue, newValue) -> oldValue, // 处理重复键(此处不应发生) TreeMap::new // 指定使用 TreeMap )); // 步骤3: 从 Map 中按键顺序提取值并连接 String sortedString = wordMap .values() // TreeMap 的 values() 方法会按键的自然顺序迭代 .stream() .collect(Collectors.joining(" ")); System.out.println("排序后的字符串: " + sortedString); } }
输出:
原始字符串: my1kiran4name2is3 排序后的字符串: my name is kiran
注意事项
-
代码可读性与维护性: 虽然上述解决方案利用了正则表达式和 Stream API 的强大功能,但其简洁性有时会牺牲一定的可读性,特别是对于不熟悉正则表达式零宽断言的开发者。在实际项目中,如果团队成员对这些高级特性不熟悉,可能需要添加详细注释或考虑更直观(尽管可能更冗长)的实现方式。
-
Map 的排序问题: 原始的 Collectors.toMap 默认创建的是 HashMap。HashMap 不保证元素的迭代顺序,这意味着 map.values().stream() 所产生的单词顺序可能不是按数字从小到大排列的。为了确保最终输出的单词严格按照数字顺序排列,应采取以下两种策略之一:
- 指定使用 TreeMap: 在 collect 阶段指定使用 TreeMap,如示例代码所示。TreeMap 会自动根据键的自然顺序(对于 Integer 来说就是数值大小)进行排序。 collect(Collectors.toMap(keyMapper, valueMapper, mergeFunction, TreeMap::new))
- 对 Map.Entry 进行排序: 如果必须使用 HashMap 或其他不保证顺序的 Map 实现,可以在获取值之前对 Map.Entry 进行排序: map.entrySet().stream().sorted(Map.Entry.comparingByKey()).map(Map.Entry::getValue).collect(Collectors.joining(” “)); 本文提供的示例代码已采用第一种方法,通过指定 TreeMap::new 来保证最终输出的正确排序。
-
数字范围与格式: 本解决方案假设数字为单个数字(1-9)。如果字符串中的数字可能是多位数(例如 “word10″),则正则表达式 d 需要修改为 d+(匹配一个或多个数字),并且相应的 split 逻辑需要确保正确处理多位数字。例如,”word10” 使用 s.split(“(?=d)”) 仍会得到 [“word”, “10”],所以对于多位数是兼容的。但如果数字前或后有其他字符,则需要更复杂的正则匹配。
总结
本教程详细展示了如何利用 Java 的正则表达式(特别是正向先行和后行断言)和 Stream API 来解决根据内嵌数字对字符串中单词进行排序的问题。通过将复杂的字符串处理分解为逻辑清晰的步骤:分割、映射和重组,并注意 Map 的排序特性,我们可以构建出既高效又准确的解决方案。在实际应用中,除了关注功能的实现,代码的可读性和健壮性(如处理不同数字范围和确保排序)同样至关重要。