【LeetCode 每日一题】2785. 将字符串中的元音字母排序
Problem: 2785. 将字符串中的元音字母排序
文章目录
- 整体思路
- 完整代码
- 时空复杂度
- 时间复杂度:O(N)
- 空间复杂度:O(N)
整体思路
这段代码的目的是对一个给定的字符串 S
中的所有元音字母进行排序,并保持所有辅音字母的位置不变。排序是基于元音字母的ASCII码值进行的,即升序排序。
该算法采用了一种非常高效的 “提取-排序-放回” 的策略,并利用了 计数排序 (Counting Sort) 的思想来对元音字母进行排序。
其核心逻辑步骤如下:
-
第一步:识别并提取元音信息
- 算法首先定义了一个包含所有大小写元音字母的
Set
,以便于进行 O(1) 的快速查找。 - 然后,它遍历输入的字符串
S
(已转换为字符数组s
以便修改)。 - 在遍历过程中,它做了两件重要的事情:
a. 记录元音位置:每当遇到一个元音字母,就将其在原字符串中的索引i
存入一个列表idx
。
b. 统计元音频率:使用一个大小为 128 的整型数组vowelASC
(可以覆盖所有基本ASCII字符)来充当一个桶,统计每种元音字母出现的次数。例如,遇到字符 ‘a’,则vowelASC['a']++
。
- 算法首先定义了一个包含所有大小写元音字母的
-
第二步:隐式的排序过程
- 在第一步完成后,我们有了所有元音字母的种类和数量(存储在
vowelASC
中),以及它们应该被放回的位置(存储在idx
中)。 - 接下来,代码通过一个
for
循环,从i = 0
遍历到127
,这实际上是在按ASCII码的顺序隐式地遍历所有可能的字符。 - 这个循环天然地保证了我们是先处理ASCII码小的字符(如 ‘A’),再处理大的字符(如 ‘e’)。这就是计数排序的核心思想。
- 在第一步完成后,我们有了所有元音字母的种类和数量(存储在
-
第三步:按序放回元音
- 在上述的
for
循环中,对于每个字符c = (char) i
:- 它会检查
vowelASC[i]
的值,即字符c
出现了多少次。 - 使用一个
while
循环,将字符c
按照其出现次数,依次放回到元音字母原来所在的位置上。 s[idx.get(t++)] = c;
:idx
列表保证了我们总是按从左到右的顺序填充元音位置。t
是一个指针,用于追踪下一个可用的元音位置。
- 它会检查
- 例如,循环首先会处理 ‘A’。假设
vowelASC['A']
是 2,那么代码会把s[idx.get(0)]
和s[idx.get(1)]
都设置为 ‘A’。然后t
变为 2。接着处理 ‘E’,以此类推。
- 在上述的
-
返回结果
- 当所有元音都按序放回后,字符数组
s
中就包含了最终的结果。 - 最后,通过
new String(s)
将其转换回字符串并返回。
- 当所有元音都按序放回后,字符数组
完整代码
import java.util.ArrayList;
import java.util.List;
import java.util.Set;class Solution {/*** 对字符串中的元音字母按ASCII值升序排序,保持辅音字母位置不变。* @param S 输入的字符串* @return 排序元音后的新字符串*/public String sortVowels(String S) {int n = S.length();// 使用 Set 提供 O(1) 的元音查找Set<Character> vowels = Set.of('a', 'e', 'i', 'o', 'u', 'A', 'E', 'I', 'O', 'U');// idx: 存储原字符串中所有元音字母的索引List<Integer> idx = new ArrayList<>();// vowelASC: 充当计数排序的桶,统计每种元音的频率int[] vowelASC = new int[128];// 将字符串转为字符数组,以便原地修改char[] s = S.toCharArray();// 步骤 1: 遍历字符串,提取元音信息for (int i = 0; i < n; i++) {char c = s[i];if (vowels.contains(c)) {// 记录元音的位置idx.add(i);// 统计元音的频率vowelASC[c]++;}}// t: 指针,用于在 idx 列表中定位下一个要填充的元音位置int t = 0;// 步骤 2 & 3: 隐式排序并按序放回元音// 遍历所有可能的ASCII字符 (0-127)for (int i = 0; i < 128; i++) {char c = (char) i;// 检查字符 c (作为一个元音) 出现了多少次while (vowelASC[i]-- > 0) {// 将字符 c 放入下一个可用的元音位置s[idx.get(t++)] = c;}}// 将修改后的字符数组转换回字符串并返回return new String(s);}
}
时空复杂度
时间复杂度:O(N)
- 提取元音信息:第一个
for
循环遍历整个字符串一次,长度为N
。内部的vowels.contains()
是 O(1),idx.add()
是均摊O(1),数组访问是 O(1)。因此,这部分的时间复杂度是 O(N)。 - 排序与放回:
- 第二个
for
循环的迭代次数是一个固定的常数 128。 - 内部的
while
循环的总执行次数,等于字符串中元音字母的总数。设元音字母数量为V
(V <= N
)。 while
循环体内的操作idx.get()
和数组赋值都是 O(1) 的。- 因此,这部分的总时间复杂度是 O(128 + V),可以简化为 O(V)。
- 第二个
- 字符串转换:
new String(s)
的时间复杂度是 O(N)。
综合分析:
总的时间复杂度是 O(N) + O(V) + O(N)。由于 V <= N
,所以最终的时间复杂度为 O(N)。
空间复杂度:O(N)
- 主要存储开销:
Set<Character> vowels
: 存储10个元音,占用 O(1) 的常数空间。List<Integer> idx
: 在最坏的情况下(例如,字符串全由元音组成),这个列表需要存储N
个索引。因此,其空间复杂度为 O(N)。int[] vowelASC
: 大小是固定的128,占用 O(1) 的常数空间。char[] s
:S.toCharArray()
创建了字符串的一个副本,占用了 O(N) 的空间。
综合分析:
算法所需的额外空间主要由 idx
列表和 s
字符数组决定。因此,总的空间复杂度为 O(N)。