将字符串中的元音字母排序 图解题解
这道题到底在问什么
- 输入
- s = "lEetcOde"
- 输出
- "lEOtcede"
- 输入
- s = "lYmpH"
- 输出
- "lYmpH"(没有元音,原样返回)
最优解:为什么这么做
一句话答案:LeetCode 2785 将字符串中的元音排序:辅音钉在原下标不动,只把元音抠出来按 ASCII 升序排好再依次回填,大写整体排在小写前,时间 O(n log n)、空间 O(n)。
把元音抠出来重排,辅音要放回哪里
给一个下标从 0 开始的字符串 s,元音是 a e i o u、大写小写都算,其余全是辅音。要重排成新串:辅音留在原来的下标一个都不挪,元音则按 ASCII 值从小到大重新排好。题面例子 s=lEetcOde,元音在下标 1、2、5、7,依次是 E e O e,排好塞回去得到 lEOtcede;再看 s=lYmpH,一个元音都没有,原样返回。
元音散在辅音中间,挨个交换为什么容易乱
难点在元音是散落的:lEetcOde 里下标 1、2、5、7 才是元音,中间夹着辅音 l t c d。若想在原串上两两比较元音、谁小就换过去,每挪一步都得重新数哪些位置是元音、跳过中间的辅音,下标来回错位,稍不留神连辅音也被换动。而且这种相邻交换本质是冒泡,最坏要比 O(n²) 次。
辅音钉死、元音单飞:收集、排序、回填三步
既然辅音全程待在原地,它们放哪跟元音怎么排毫无关系,干脆先把它们当背景。做法分三步:先从左扫一遍,把所有元音按遇到的先后抠进一个单独的篮子,辅音只记住位置;再对篮子里这几个元音排序;最后从头再扫一遍,碰到辅音原样照抄,碰到元音位置就从排好的篮子里从左往右依次取一个填进去。两趟扫描加中间一次排序,元音和辅音就干净分开了。
排序按 ASCII 不按字母表,大写为什么排在小写前
排序这一步有个坑:比的是 ASCII 值,不是字母表顺序。ASCII 里大写字母 A 到 Z 是 65 到 90,小写 a 到 z 是 97 到 122,所以任意大写字母都小于任意小写字母。篮子里 E 是 69、O 是 79、两个 e 都是 101,按 ASCII 从小到大就排成 E、O、e、e,大写的 E、O 反而站在小写 e 前面。若把 E 和 e 当同一个字母、按字母表比就会排错。判元音时可以先转小写再看,但排序时要用字符本身的 ASCII。
lEetcOde 走一遍,四个元音怎么各自归位
从左扫 lEetcOde:下标 1 收 E、下标 2 收 e、下标 5 收 O、下标 7 收 e,篮子按先后是 E e O e,ASCII 依次 69、101、79、101,辅音 l t c d 分别钉在下标 0、3、4、6。给篮子排序得到 69、79、101、101,也就是 E、O、e、e。再从头回填:下标 0 辅音 l 照抄;下标 1 取 E;下标 2 取 O,原来的 e 被换掉;下标 3、4 辅音 t、c 照抄;下标 5 取 e;下标 6 辅音 d 照抄;下标 7 取最后一个 e。拼起来正是 lEOtcede。
大写漏判会错、顺序比错会乱,时间为什么是 O(n log n)
漏判大写元音是头一个坑:只写小写 a e i o u,会把 E、O、A 当辅音钉住不排,答案直接错,判元音时得把大写也算上。第二处是排序按字母表而不是 ASCII,E 和 e 一旦被当相等,输出顺序就乱。也有人排序时连辅音一起卷进去,或者回填时按收集顺序、从大到小取,这两种都会破坏「辅音不动、元音升序」的要求。复杂度上,两趟扫描各是 O(n),中间给 k 个元音排序是 O(k log k),k 最多等于 n,合起来时间 O(n log n),篮子和结果串占 O(n) 空间。
▶ 动画逐步走查(共 23 步)——想跟着动画一帧帧对照就展开
- 3记牢这套三步法:收集元音、单独排序、按序回填。辅音自始至终待在原地,我们只搬动元音。下面从收集元音开始,一格一格扫过去。
- 4先认一下画面。上面这排就是字符串 lEetcOde,一共 8 个字符。我用一个指针从左往右扫,紫色就是它当前站的位置。扫的时候把元音格子点亮成绿色、收进右边的元音篮子,辅音格子压成灰色留在原地。现在篮子还是空的,咱们从下标 0 开始。
- 5指针走到下标 0,这里是 l。它是辅音,不参与排序,直接把这一格压成灰色,记住它就钉在下标 0 这个位置,后面谁都不许挪它。指针继续往右。
- 6指针走到下标 1,这里是 E。它是元音,把它点亮成绿色,并且丢进元音篮子。这是收到的第 1 个元音,它的 ASCII 值是 69。别管它现在什么大小写,先原封不动收进来,排序留到第二步。
- 7指针走到下标 2,这里是 e。它是元音,把它点亮成绿色,并且丢进元音篮子。这是收到的第 2 个元音,它的 ASCII 值是 101。别管它现在什么大小写,先原封不动收进来,排序留到第二步。
- 8指针走到下标 3,这里是 t。它是辅音,不参与排序,直接把这一格压成灰色,记住它就钉在下标 3 这个位置,后面谁都不许挪它。指针继续往右。
- 9指针走到下标 4,这里是 c。它是辅音,不参与排序,直接把这一格压成灰色,记住它就钉在下标 4 这个位置,后面谁都不许挪它。指针继续往右。
- 10指针走到下标 5,这里是 O。它是元音,把它点亮成绿色,并且丢进元音篮子。这是收到的第 3 个元音,它的 ASCII 值是 79。别管它现在什么大小写,先原封不动收进来,排序留到第二步。
- 11指针走到下标 6,这里是 d。它是辅音,不参与排序,直接把这一格压成灰色,记住它就钉在下标 6 这个位置,后面谁都不许挪它。指针继续往右。
- 12指针走到下标 7,这里是 e。它是元音,把它点亮成绿色,并且丢进元音篮子。这是收到的第 4 个元音,它的 ASCII 值是 101。别管它现在什么大小写,先原封不动收进来,排序留到第二步。
- 13一整趟扫完了。字符串里 4 个元音全进了篮子,按遇到的先后是 E e O e,ASCII 分别是 69、101、79、101。辅音 l t c d 已经乖乖钉在灰色位置上。接下来只对篮子里这四个元音动手,把它们按 ASCII 从小到大排好。
- 14排序前先想清一件事:比的是 ASCII 值,不是字母表本身。大写字母 A 到 Z 的 ASCII 是 65 到 90,小写 a 到 z 是 97 到 122,所以任何大写字母的 ASCII 都小于任何小写字母。这里 E 是 69、O 是 79,都比小写 e 的 101 小。按 ASCII 从小到大,顺序就是 E、O、e、e,两个大写排在前面,两个小写 e 排在后面。
- 15排序完成。篮子里的元音现在按 ASCII 非递减排成了 E、O、e、e。你看,原来收集顺序是 E e O e,现在两个大写 E 和 O 被提到了前面,两个小写 e 落到后面。这就是第二步的成果,接下来第三步把它们按这个顺序依次填回元音位置。
- 16现在第三步,从下标 0 往右扫。规则很简单:碰到辅音,原样不动;碰到元音位置,就从排好序的篮子里从头开始,依次取出一个填进去。我用一个取用指针 j 指着篮子,现在 j 指着第 1 个元音 E。开始扫。
- 17指针到下标 0,这里是辅音 l。辅音不参与排序,原样保留,篮子的取用指针 j 一动不动。指针继续往右。
- 18指针到下标 1,这是个元音位。从排好序的篮子里取出第 1 个元音 E,填到这一格。原来这里是 E,现在换成 E。取用指针 j 往后挪一格。
- 19指针到下标 2,这是个元音位。从排好序的篮子里取出第 2 个元音 O,填到这一格。原来这里是 e,现在换成 O。取用指针 j 往后挪一格。
- 20指针到下标 3,这里是辅音 t。辅音不参与排序,原样保留,篮子的取用指针 j 一动不动。指针继续往右。
- 21指针到下标 4,这里是辅音 c。辅音不参与排序,原样保留,篮子的取用指针 j 一动不动。指针继续往右。
- 22指针到下标 5,这是个元音位。从排好序的篮子里取出第 3 个元音 e,填到这一格。原来这里是 O,现在换成 e。取用指针 j 往后挪一格。
- 23指针到下标 6,这里是辅音 d。辅音不参与排序,原样保留,篮子的取用指针 j 一动不动。指针继续往右。
- 24指针到下标 7,这是个元音位。从排好序的篮子里取出第 4 个元音 e,填到这一格。原来这里是 e,现在换成 e。取用指针 j 往后挪一格。
- 25扫完了。四个元音位从左到右依次填进了 E、O、e、e,辅音 l、t、c、d 始终待在原地没挪窝。最终字符串是 lEOtcede,和一开始说的对上了。回头看,大写 E 和 O 因为 ASCII 小,排到了前面的元音位,小写 e 落到了后面,辅音框架纹丝不动。
⚠️ 容易写错的地方
✗ 错:判元音时只写小写 a e i o u,漏了大写
✓ 对:统一转小写再判,或把大小写十个元音都列上
题目明说大写元音也算,像 E、O、A 这些漏判就会被当辅音钉住不排,答案直接错
✗ 错:把大写小写当同一个字母排,按字母表比
✓ 对:严格按 ASCII 值比,大写整体小于小写
ASCII 里大写 65 到 90、小写 97 到 122,大写永远排在小写前面。按字母表把 E 和 e 视作相等会排错顺序
✗ 错:排序时连辅音一起排,或挪动了辅音位置
✓ 对:只把元音抠出来排,辅音下标全程不变
题目要求辅音留在原位,只有元音重排。把辅音也卷进排序会彻底打乱原串结构
✗ 错:回填时按从大到小或收集顺序填
✓ 对:按 ASCII 从小到大,用一个递增指针依次取
要求是非递减排列,必须升序回填;拿收集顺序或降序填都不满足元音有序
完整代码(Python / C++ / Java)
Python
from __future__ import annotations
from typing import *
from collections import *
from functools import *
from itertools import *
from math import *
from heapq import *
from bisect import *
class Solution:
def sortVowels(self, s: str) -> str:
vs = [c for c in s if c.lower() in "aeiou"]
vs.sort()
cs = list(s)
j = 0
for i, c in enumerate(cs):
if c.lower() in "aeiou":
cs[i] = vs[j]
j += 1
return "".join(cs)C++
#include <algorithm>
#include <array>
#include <cctype>
#include <climits>
#include <cmath>
#include <deque>
#include <functional>
#include <iostream>
#include <limits>
#include <map>
#include <numeric>
#include <queue>
#include <set>
#include <sstream>
#include <stack>
#include <string>
#include <tuple>
#include <unordered_map>
#include <unordered_set>
#include <utility>
#include <vector>
using namespace std;
class Solution {
public:
string sortVowels(string s) {
string vs;
for (auto c : s) {
char d = tolower(c);
if (d == 'a' || d == 'e' || d == 'i' || d == 'o' || d == 'u') {
vs.push_back(c);
}
}
sort(vs.begin(), vs.end());
for (int i = 0, j = 0; i < s.size(); ++i) {
char d = tolower(s[i]);
if (d == 'a' || d == 'e' || d == 'i' || d == 'o' || d == 'u') {
s[i] = vs[j++];
}
}
return s;
}
};Java
import java.util.*;
class Solution {
public String sortVowels(String s) {
List<Character> vs = new ArrayList<>();
char[] cs = s.toCharArray();
for (char c : cs) {
char d = Character.toLowerCase(c);
if (d == 'a' || d == 'e' || d == 'i' || d == 'o' || d == 'u') {
vs.add(c);
}
}
Collections.sort(vs);
for (int i = 0, j = 0; i < cs.length; ++i) {
char d = Character.toLowerCase(cs[i]);
if (d == 'a' || d == 'e' || d == 'i' || d == 'o' || d == 'u') {
cs[i] = vs.get(j++);
}
}
return String.valueOf(cs);
}
}复杂度
时间
O(n log n)
设字符串长度为 n,元音个数 k 不超过 n。两趟线性扫描各 O(n);中间对 k 个元音排序是 O(k log k),最坏 k 等于 n 就是 O(n log n)。排序这一步压过线性扫描,所以总时间 O(n log n)
空间
O(n)
按峰值算。元音篮子最多装 n 个字符,是 O(n);结果串也要 O(n)。排序本身的额外开销分语言:C plus plus 和 Java 通常是 O(log n) 的递归栈,Python 的 Timsort 最坏 O(n)。合起来空间是 O(n)
看不够?换成动画再走一遍
上面的推演每一步都对应一帧动画。点开交互动画版,能一步步看着 将字符串中的元音字母排序 的数据怎么变、指针怎么走,还能切 Python / Java / C++ 跟着练。
面试官可能追问
辅音到底要不要跟着一起挪位置?+
辅音位置固定不用管,只把元音抠出来按 ASCII 排好,再按顺序填回原来的元音位置。落到实现就是收集、排序、回填三步:一趟扫描收元音、记辅音位置,中间给元音排一次序,最后一趟扫描把排好的元音依次填进元音位、辅音位照抄。
大小写元音怎么统一处理,会不会漏判?+
判断一个字符是不是元音时,先把它转成小写再看在不在 a e i o u 里,这样 E、O、A 这些大写元音也能识别,不会被当辅音。但排序时不转大小写,直接用字符本身的 ASCII 值比,于是大写字母会自然排在小写前面,正好符合按 ASCII 非递减的要求。
时间 O(n log n),能不能不排序做到更快?+
可以。瓶颈是给元音排序,但元音连大小写一共只有十种,所以能改用计数排序:先统计每种元音出现几次,回填时按 ASCII 从小到大依次吐出,时间降到 O(n)。这是进阶优化,通用排序写起来更直接,面试里说清楚有这条优化路子即可。
想听吴师兄把这道题讲给你听?
文字版和动画都随便看。开通图解算法年卡,可以听吴师兄把 将字符串中的元音字母排序 一步步讲透(全站已上线 905 份讲透,持续扩充), 卡住的地方还有 AI 私教小欧就着动画帮你拆到懂。
本平台为独立第三方培训机构,与华为技术有限公司无任何关联;课程的服务内容与权益以购买协议为准,学习效果因个人情况而异。「华为 OD」「华为可信」等仅为对岗位与考试方向的客观描述,相关商标归各自权利人所有。