1. 项目概述与题目核心
1.1 题目到底在问什么
LeetCode 1451 这道题,中文名叫“重新排列句子中的单词”(Rearrange Words in a Sentence),是一道非常典型的字符串处理题,也是很多人在刷 LeetCode 早期会遇到的中等难度题。题目本身不复杂,但里面藏了两个容易翻车的小坑:一个是首字母大写,另一个是稳定排序。很多新手第一次 AC 之后觉得自己懂了,其实只是套路记住了,并没有真正理解题目背后的排序稳定性和字符串不可变特性。
先把题目用大白话翻译一遍。给你一个句子 text,句子里的单词用空格分隔。这个句子有一个特点:整个句子只有第一个单词的首字母是大写,其他字母全是小写。比如 "Leetcode is cool",只有 L 是大写,剩下的 eetcode is cool 全是小写。现在要你把这个句子里的单词按照“长度从小到大”重新排列,如果两个单词长度一样,它们的前后顺序必须保持和原句子一样。排完之后,再把新句子的首字母变成大写,其余字母全部变成小写,最后返回这个新句子。
举个例子,text = "Leetcode is cool",三个单词长度分别是 8、2、4。按长度升序排,得到 "is cool Leetcode",但注意这时 Leetcode 跑到了最后面,它的首字母大写如果保留,就会变成句子中间出现一个大写字母,违反要求。所以要先把所有单词都转成小写,排序后再把整个句子的首字母大写,最终输出 "Is cool leetcode"。这个例子里,is 的首字母 i 被大写成 I,Leetcode 变成了小写的 leetcode。
还有一个容易被忽略的细节:单词长度相同的时候,必须保持原来的相对顺序。比如 "Keep calm and code on",单词和长度分别是:Keep(4)、calm(4)、and(3)、code(4)、on(2)。按长度排序后,on 排第一,and 排第二,剩下三个长度为 4 的单词必须保持原顺序:Keep 在 calm 之前,calm 在 code 之前。所以结果是 "On and keep calm code"。这里 Keep 变成 keep,看起来和 calm、code 一样都是小写,顺序就完全依赖稳定性了。
1.2 为什么这题值得做
这道题表面上是简单的字符串排序,但它的考点非常密集。第一,它考了字符串的分割与拼接,这是几乎所有编程语言处理文本的入门基本功。第二,它考了大小写转换,而且是“部分转换”,不是无脑 toLowerCase 整个句子。第三,它考了排序算法的稳定性,这是很多人在学校学排序时容易忽略的概念。第四,它考了边界条件,比如只有一个单词、所有单词长度一样、单词长度从 1 到很大跨度等。
从面试角度看,这题很常被用作热身题或者字符串综合题的铺垫。比如一些大厂面试官会先问这题,然后扩展成“如果单词里有标点怎么办”“如果要求按长度降序,相同长度按字典序怎么办”“如果句子很长,内存有限怎么办”。如果你只是背住了一个答案,扩展一下就露馅了。所以把这道题真正吃透,对后续刷类似题目很有帮助,比如 LeetCode 937(重新排列日志文件)、LeetCode 791(自定义字符串排序)等,它们的内核和这题有相通之处。我个人觉得,这题是“字符串 + 排序”这个组合里性价比很高的一道题,适合用来检验自己是否真的理解了稳定排序。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 从暴力到最优:解题思路拆解
2.1 第一个关键点:首字母大写带来的干扰
拿到这题,最直观的方案是:用空格把句子拆成单词数组,然后按长度排序,再拼回去。但如果你直接这样做,会出问题。问题就出在“首字母大写”上。
原句子里第一个单词的首字母是大写,比如 "Leetcode is cool" 中的 L。按长度排序之后,原来的第一个单词很可能不在新句子的第一位,它可能跑到中间甚至最后。如果它的首字母仍然大写,那么新句子里就会出现多个大写字母,不符合题目要求的“最终句子只有首字母大写,其余小写”。
所以处理步骤里必须包含一步:在排序之前,先把第一个单词的首字母转成小写。具体来说,对单词数组 words,执行 words[0] = words[0][0].lower() + words[0][1:]。因为题目保证除了第一个单词的首字母,其他字母本来就都是小写,所以这一步就够用了。当然,更稳妥的做法是直接把所有单词整体转成小写,反正最后还要把首字母大写,多一次遍历成本也不高。但在面试中,最好向面试官说明两种做法的区别,体现出你思考过边界条件。
这里有一个常见的误区:有人会把 text 整个先转成小写,再分割。这样也能得到全小写的单词数组,逻辑上没错。但在某些语言里,比如 Java 的 String.toLowerCase() 会为每个字符串创建一个新对象,如果句子很长、单词很多,会产生不必要的内存开销。题目没给极端数据,但刷题时养成好习惯,尽量用更精准的方式操作。
2.2 第二个关键点:稳定排序
排序稳定性是这题的核心考点。所谓稳定排序,就是指如果两个元素的关键字相等,排序后它们的相对顺序保持不变。放在这题里,就是“长度相同的单词,必须保持原句中的相对位置”。为什么必须这样?因为题目明确要求了:“If two words have the same length, they must maintain their original order.”
如果使用不稳定的排序算法,比如快速排序,那么长度相同的单词就可能被交换位置,导致结果不符合题意。虽然你跑测试用例时可能碰巧没错,但 LeetCode 的判题用例里一定会包含相同长度单词的用例,一旦顺序错了就会 WA。
那么解决问题的办法有两条路:第一条,使用语言内置的稳定排序算法。Python 的 sorted() 和 list.sort() 底层是 TimSort,稳定;Java 的 Collections.sort() 底层是 TimSort,也稳定;C++ 的 std::stable_sort() 专门保证稳定。第二条,如果只能用不稳定的排序算法,比如 C++ 的 std::sort(),可以给每个单词额外记录一个原始索引,排序规则变成:先比较长度,长度相等时比较原始索引。这样做相当于手动把“相等条件”扩展成“索引也参与比较”,从而让排序结果等价于稳定排序。后一种思路在面试中特别加分,因为它展示了你对排序本质的理解。
2.3 复杂度分析
这道题的时间复杂度主要取决于排序。设句子中单词个数为 n,每个单词的平均长度为 m,那么分割字符串的时间复杂度是 O(n*m),也就是 O(len(text))。排序的时间复杂度是 O(n log n),因为比较两个单词长度是 O(1) 操作(只要取 length() 即可)。最后拼接字符串的时间复杂度是 O(len(text))。所以整体时间复杂度是 O(len(text) + n log n)。在 LeetCode 的约束下,这个复杂度完全够用。
空间复杂度方面,我们需要存储单词数组,占用 O(n*m),也就是 O(len(text))。如果考虑排序算法的额外空间,Python 的 TimSort 最坏是 O(n),C++ 的 stable_sort 也可能需要 O(n) 的临时空间。整体空间复杂度可以记为 O(len(text))。如果面试官要求优化空间,可以尝试在原字符串上原地操作,但那样会非常别扭,而且题目没有这种要求,不必过度设计。
3. 多语言实现与核心代码
3.1 Python 实现:简洁到不能再简洁
Python 的写法非常直观,几乎就是翻译题目描述。先上代码:
python复制class Solution:
def arrangeWords(self, text: str) -> str:
words = text.split(" ")
# 将第一个单词的首字母转为小写
words[0] = words[0][0].lower() + words[0][1:]
# 按长度稳定排序
words.sort(key=len)
# 拼接句子,并将新句子首字母大写
res = " ".join(words)
return res[0].upper() + res[1:]
这里每一步都值得解释一下。text.split(" ") 按空格分割,题目保证单词之间只有一个空格,所以直接用空格作为分隔符是安全的。如果题目改成任意空格,就改成 text.split(),但那样会把多个空格自动折叠,结果不同。这里维持原题语义。
words[0] = words[0][0].lower() + words[0][1:] 这行代码,如果第一个单词是 "Leetcode",执行后变成 "leetcode"。注意不能直接写 words[0] = words[0].lower(),那样会把整个单词都转小写,结果一样,但语义上不够精确。如果句子只有一个单词,比如 "Hello",分割后数组长度是 1,这行代码没问题,排序后还是这一个单词,最后 res[0].upper() + res[1:] 会把首字母大写,返回 "Hello",完全正确。
words.sort(key=len) 是 Python 列表的原地排序,默认稳定。这里传入 key=len,意思是按长度比较,len 是内建函数,会提取每个单词的长度作为排序键。由于 Python 排序稳定,相同长度的单词保持原顺序。如果你不放心,也可以写成 words.sort(key=lambda x: len(x)),但没必要。
最后一行 return res[0].upper() + res[1:],如果 res 非空,这是安全的。题目没说句子可能为空,但严谨一点可以加个判断:if not res: return res。不过 LeetCode 的测试用例不会给空句子,所以不写也行。
这个实现非常干净,核心逻辑 4 行搞定。其实很多人会想用 text.lower().split(),但那样会把首字母也小写,后面排序和拼接一样能过。我的建议是使用上面的写法,因为每一步都对应着题目要求,读代码的人更容易理解。
3.2 C++ 实现:用 stable_sort 解决稳定性
C++ 的标准库里有 sort 和 stable_sort 两种排序函数。sort 通常是不稳定的快速排序,stable_sort 保证稳定,但性能可能略差。由于题目要求相同长度保持原顺序,我们必须用 stable_sort。代码示例如下:
cpp复制class Solution {
public:
string arrangeWords(string text) {
vector<string> words;
string word;
istringstream iss(text);
while (iss >> word) {
words.push_back(word);
}
// 将第一个单词首字母转为小写
if (!words.empty()) {
words[0][0] = tolower(words[0][0]);
}
// 稳定排序,按长度升序
stable_sort(words.begin(), words.end(), [](const string& a, const string& b) {
return a.size() < b.size();
});
// 拼接结果
string res;
for (int i = 0; i < words.size(); i++) {
if (i > 0) res += " ";
res += words[i];
}
// 将结果首字母大写
if (!res.empty()) {
res[0] = toupper(res[0]);
}
return res;
}
};
这里用 istringstream 分割字符串,好处是它会自动跳过空格,即使输入有两个连续空格也能正确分割。但注意题目明确说“单词之间由一个空格分隔”,所以用 >> 是安全的。分割完后,words[0][0] = tolower(words[0][0]) 只把第一个字符转成小写,tolower 是 <cctype> 里的函数,注意它接收 int,返回 int,直接赋值给 char 没问题。
stable_sort 的第三个参数是 lambda 表达式,比较规则是 a.size() < b.size()。这里只比较长度,不比较内容,因为稳定性由 stable_sort 保证。如果面试官不允许用 stable_sort,你可以改成先给每个单词添加索引,然后用普通 sort,比较时 if (a.size() != b.size()) return a.size() < b.size(); return idx_a < idx_b;。这种手动稳定排序的方法在 C++ 中也很常见。
拼接时用 string 的 += 操作。最后把 res[0] 用 toupper 转大写。注意如果 res 为空,res[0] 是未定义行为,所以要加判断。整个实现非常标准,适合在面试中现场写出来。
3.3 Java 实现:利用 Stream 的优雅写法
Java 的 String.split(" ") 可以直接分割,Arrays.sort 或 List.sort 是稳定的(Timsort),所以写起来也很清爽。我提供两种写法,先看普通写法:
java复制class Solution {
public String arrangeWords(String text) {
String[] words = text.split(" ");
// 首字母转小写
words[0] = words[0].substring(0, 1).toLowerCase() + words[0].substring(1);
// 稳定排序
Arrays.sort(words, (a, b) -> a.length() - b.length());
// 拼接
String res = String.join(" ", words);
return res.substring(0, 1).toUpperCase() + res.substring(1);
}
}
这里 words[0].substring(0, 1) 取出首字符,toLowerCase() 转小写,后面的部分用 substring(1) 截取。注意 Java 的 String 是不可变的,所以必须生成新字符串。Arrays.sort 对于对象数组使用的是稳定的归并排序(Timsort),所以不会打乱同长度单词的顺序。
String.join(" ", words) 是 Java 8 提供的拼接方法,非常方便。最后同样处理首字母大写。如果 text 可能为空,words 长度是 1 且内容为空字符串,words[0].substring(0,1) 会报错,但 LeetCode 不会给空字符串,所以可以不处理。如果面试官追问,你可以加判断。
如果你喜欢用 Stream,可以这样写:
java复制public String arrangeWords(String text) {
String[] words = text.toLowerCase().split(" ");
Arrays.sort(words, (a, b) -> a.length() - b.length());
String res = String.join(" ", words);
return Character.toUpperCase(res.charAt(0)) + res.substring(1);
}
text.toLowerCase().split(" ") 直接把整个句子变成小写再分割,这样就不需要单独处理第一个单词了。缺点是多遍历一遍字符串,但代码更短。两种写法都可以,看你的偏好。我个人倾向于第一种,因为它更精确地表达了“只把第一个单词的首字母转小写”这个意图。
3.4 注意事项与边界情况
无论用哪种语言,有几个边界情况一定要测一下。
第一个边界:句子只有一个单词,比如 "Hello"。分割后数组长度是 1,排序没有变化,最后首字母大写,返回 "Hello"。这个用例可以验证你的代码不会因为 words[0] 被修改后变成空串,也不会因为最后 res[1:] 越界。
第二个边界:所有单词长度相同,比如 "I am he"?注意 I 长度 1,am 长度 2,he 长度 2,不是全部相同。可以用 "A bb cc",长度都是 1、2、2?不对。用 "Ab cd ef",三个单词长度都是 2,排序后应该保持 "Ab cd ef" 的原顺序,但首字母要变成小写再首字母大写,最终结果是 "Ab cd ef"?实际上 "Ab" 原句首字母 A 大写,转成小写后变成 "ab cd ef",再首字母大写得到 "Ab cd ef",顺序不变。这个用例能验证稳定性。
第三个边界:第一个单词本身长度最短,比如 "A bb ccc",排序后仍然是 "A bb ccc" 的顺序,但是输出时要首字母大写,且 A 本来就大写,所以结果还是 "A bb ccc"。如果第一个单词是最短的,排序后它还在第一位,前面的首字母转换和最后的首字母大写相当于做了两次操作,但结果正确。
第四个边界:首字母大写且排序后它跑到中间。比如 "Bbb a cc",排序后变成 "a cc bbb",输出应该是 "A cc bbb"。注意原来的 B 被转成了 b,不能被恢复成大写,因为它在句子中间。
这些用例在你的本地环境里跑一遍,能有效避免提交后出现低级 WA。我一般会在写完之后,把这些用例整理成一个测试函数,手动确认输出。
4. 实操过程:从读题到 AC 的完整复盘
4.1 审题容易踩的坑
我第一次做这道题的时候,其实翻过车。那时候我已经知道要用排序,也想到了把首字母转小写,但忽略了一个关键点:题目里说的“重新排列”到底稳不稳定。我当时想当然地用了 C++ 的 sort,结果提交后有一组测试用例没过。去查了 WA 的用例才发现,长度相同的单词顺序被我打乱了。从那以后,我只要看到“保持原有顺序”这种字眼,就会立刻想到稳定性。
还有一个容易踩的坑是输出格式。很多人会把结果里除了首字母之外的大写字母全都清除,但忘了“首字母大写”指的是整个句子的第一个字符,而不是第一个单词的首字母。比如输入 "Hello world",输出应该是 "Hello world" 吗?等等,按长度排序后,Hello 和 world 都是 5,保持原顺序,所以结果就是 "Hello world",首字母已经是 H,不需要再改动。但输入 "World hello",排序后还是 "World hello"?长度都是 5,保持原顺序,输出应该是 "World hello"?不对,原句 "World hello" 中 W 是大写,排序后 World 在第一位,首字母大写,所以结果还是 "World hello"。但如果你先把第一个单词首字母转小写,得到 "world hello",再用最后一个步骤把首字母大写,得到 "World hello",也可以。关键是,如果原来的第一个单词不在第一位,它的首字母已经转小写了,所以最终不会出现中间大写。
另外,有些人会忽略空格拼接。LeetCode 的输出要求是单词之间一个空格,并且句子末尾没有多余空格。如果你用 String.join 或者 " ".join(words),就没问题。但如果你手动拼接,小心不要在末尾多加空格。
4.2 代码提交中的各种踩坑记录
我把自己在写这题时踩过的坑整理一下,供你参考。
第一个坑:Java 里用 text.split(" ") 时,如果句子开头或结尾有空格,会产生空字符串。虽然原题保证没有多余空格,但如果你用一些在线测试工具输入了带空格的字符串,代码可能出错。解决方法是使用 text.trim().split("\\s+"),但要注意 trim 会删掉首尾空格,不会影响单词本身。不过这样改之后,如果句子本来就是空字符串,trim 后变成空字符串,split 返回的数组长度是 1,包含空字符串,处理起来更麻烦。所以最稳妥的做法是:不依赖额外空格,保持原题的输入规范。
第二个坑:Python 里如果用 words.sort(key=lambda x: len(x)),其实用 key=len 更简洁,而且更快。但要注意,不要写成 words.sort(key=len, reverse=True),因为那是降序。如果题目改成“长度最长优先”,你再改成 reverse=True。但当前题目是升序,别顺手写错。
第三个坑:C++ 里 tolower 和 toupper 函数都定义在 <cctype> 中,如果你忘了包含头文件,编译会报错。还有,这两个函数返回的是 int,如果直接给 char 赋值,在某些编译环境下会产生警告,但不会报错。为了保险,可以写成 words[0][0] = static_cast<char>(tolower(words[0][0]))。另外,不要对非字母字符调用 tolower,但本题的单词只包含字母,所以没问题。
第四个坑:C++ 的 istringstream 虽然很方便,但它的 operator>> 会跳过所有空白字符,包括换行符和制表符。如果句子中包含多个空格,它会忽略,这反而符合我们的需求。但要注意,如果单词本身是空字符串,它不会读进来,也就是说 "" 这种情况下你会得到一个空的 vector。原题不会出现,但你需要防御。
第五个坑:Java 的 Arrays.sort(words, (a, b) -> a.length() - b.length()) 这个比较器有个小问题:如果两个单词长度是 Integer.MIN_VALUE 之类的极端值,相减可能溢出。但单词长度最多就是几百,不会溢出。不过更严谨的写法是 Comparator.comparingInt(String::length),这样不会溢出。我在代码审查时看到很多人直接相减,虽然快捷,但不够严谨。面试时可以主动提一句,展示你对 Java 比较器原理的掌握。
4.3 测试用例设计与验证
为了确保代码正确,我通常会在本地跑下面这几个测试用例。如果有自动化测试框架,可以用参数化的方式批量验证;没有的话,写一个循环逐个打印结果也行。
第一组:常规用例,验证基本流程。
- 输入:
"Leetcode is cool",预期输出:"Is cool leetcode"。 - 输入:
"Keep calm and code on",预期输出:"On and keep calm code"。 - 输入:
"To be or not to be",预期输出是什么?我们来算一下:To长度 2,be长度 2,or长度 2,not长度 3,to长度 2,be长度 2。稳定排序后,所有长度为 2 的单词保持原顺序:To,be,or,to,be,然后是not。注意To首字母转小写变to,然后整体首字母大写,得到"To be or to be not"?不对,排序后的顺序是To(2), be(2), or(2), to(2), be(2), not(3),拼接后是To be or to be not,但第一个To的T在最终首字母大写后还是T,所以输出"To be or to be not"。这里有个小细节:原句子里的To和单词to存在吗?原句是"To be or not to be",单词顺序是To,be,or,not,to,be。排序后长度 2 的单词按原顺序依次是To,be,or,to,be,然后not。所以拼接后是"To be or to be not"。但语义上没有“to be not”这种说法,不过题目不管语义,只按长度排。这个用例可以验证两个缩写To和to在大小写处理后是否都变成小写,再首字母大写时To是否恢复大写。
第二组:长度全部相同。
- 输入:
"Ab cd ef",预期输出:"Ab cd ef"。因为长度都是 2,保持原顺序,且首字母是A,最终输出不变。注意原句子的Ab首字母大写,中间有小写b,排序后Ab还在第一位,所以最终首字母大写后仍是A,没有变化。这个用例能验证稳定性。
第三组:第一个单词长度最短和最长。
- 输入:
"A bb ccc",预期输出:"A bb ccc"。因为A长度 1 最短,排序后仍在第一位,首字母大写不变。 - 输入:
"Bbb a cc",预期输出:"A cc bbb"。因为排序后a长度 1 排第一,cc长度 2 排第二,Bbb长度 3 排第三,但Bbb被转成小写bbb,最终首字母大写的是a,所以输出"A cc bbb"而不是"A cc Bbb"。
第四组:单个单词。
- 输入:
"Hello",预期输出:"Hello"。 - 输入:
"world",预期输出:"World"。注意原句首字母w是小写,但题目说句子中的第一个单词首字母应该大写?实际上题目描述里句子以大写字母开头,所以"world"不是合法输入。但如果你自己测试,最终结果会把w变成大写。这个用例可以验证我们的处理是否会把单个单词首字母大写。
把这些用例跑一遍,如果全部通过,代码基本就稳了。我自己写的时候还会加一个“空字符串”用例,但 LeetCode 不会给,所以不强制。如果你追求完美,可以在代码开头加一句 if (text.isEmpty()) return "";,不会有副作用。
5. 常见问题与排查技巧实录
5.1 问题速查表
这是我整理的一份关于这题的常见问题速查表,涵盖了我在社区里看到过的问题,以及我自己遇到过的坑。你可以直接收藏,遇到 WA 时对号入座。
| 现象 | 可能原因 | 解决方案 |
|---|---|---|
| 输出结果里中间位置出现大写字母 | 没有在排序前将第一个单词首字母转成小写,或者排序后直接把原字符串拼回去了 | 在分割后立即把第一个单词首字母转小写,最后再统一将结果首字母大写 |
| 相同长度的单词顺序和原来不一致 | 使用了不稳定的排序算法,比如 C++ 的 sort,或者 Python 里错误地用 set 等去重结构 |
使用稳定排序( stable_sort、sorted、Arrays.sort),或手动附加原始索引进行比较 |
| 输出结果没有首字母大写,或者只有某个单词首字母大写 | 最后一步没有正确处理首字母大写,或者错误地对所有单词都调用了 capitalize() |
拼接到最终字符串后,只对 res[0] 执行 upper,其余部分原样保留 |
| 多空格输入导致分割出空单词 | 使用了 split(" ") 而输入带有连续空格 |
如果原题保证单空格可忽略;如果面试扩展,改用 split()(Python)、String.split("\\s+")(Java)、istringstream(C++) |
单词只有一个字符,res[1:] 越界 |
对长度为 1 的结果字符串使用了 substring(1) 或 res[1:],其实不会越界,但要注意空串 |
先判断 res 是否为空,再取 res[0] 和 res[1:] |
| 首单词本身大写且长度最短,结果不正常 | 逻辑上没问题,可能只是你测试用例没设计好,预期输出写错了 | 手动按题目要求一步步推演,别凭直觉写预期 |
5.2 独家技巧与心得
这题我刷过好几遍,每次都有新体会。第一个技巧是:遇到“保持原顺序”这几个字,第一反应就该是“稳定排序”。如果不能确定语言内置排序是否稳定,就手动加索引。这个习惯能帮你避免一类非常隐蔽的 bug。比如 Java 的 Collections.sort 对 List 是稳定排序,但 Arrays.sort 对基本类型数组使用的是双轴快速排序,不稳定。如果你把单词放在 char[] 数组里排序,那就不稳定了。正因为有这些细节,刷题时一定要树立“稳定性敏感”的意识。
第二个技巧是:处理大小写转换时,尽量缩小操作范围。比如这题只需要改第一个单词的首字母,就不要把整个字符串 toLowerCase。这样做不仅高效,而且语义更清晰。如果你在代码注释里写明“只处理首字母大写导致的干扰,之后统一恢复”,面试官能一眼看到你的思考过程。
第三个技巧是:测试用例不要只看 LeetCode 给出的两个例子,要自己构造边界用例。我一般会在写好代码后,把“一个单词”、“所有单词同长”、“第一个单词最短”、“第一个单词最长”、“第一个单词首字母必须被小写化”、“最终首字母来自第二个单词”这些场景都过一遍。如果你能把测试思维融入日常刷题,慢慢就会形成条件反射,提交错误率会明显下降。
第四个技巧:面试的时候,如果可以,主动跟面试官聊一聊时间复杂度。这题的核心是排序,但很多人会忽略比较单词长度是 O(1) 还是 O(m)。实际上,如果排序时直接比较字符串内容,复杂度就不是 O(n log n) 了,而是 O(n log n * m)。而按长度排序,比较键是整数,所以是 O(n log n)。这种细节虽然不影响最终代码,但能体现你对复杂度的理解。
最后再分享一个小技巧:如果面试官让你不能用内置排序,你会怎么实现?其实可以桶排序。因为单词长度范围是有限的,句子长度最多也就是 10^5,我们可以先遍历一遍单词,找到最大长度,然后创建桶数组,把每个单词放到对应长度的桶里,最后按桶下标从小到大连起来。这样时间复杂度是 O(n + maxLen),空间复杂度是 O(n)。虽然这个优化对本题没什么必要,但如果你能随口说出来,会显得你思维开阔。当然,主流的解法还是用稳定排序,桶排序可以作为备选方案。
我在实际面试中被问过这题的变体:如果把“按长度升序”改成“按长度降序,相同长度按字典序”,你会怎么改?其实改动很小,排序的 key 变成一个元组 (-len(word), word) 或比较器里先比长度降序,再比字典序。但如果不假思索地写 reverse=True,就会把同长度单词的字典序也反了。这就是为什么我总强调,不要死记代码,要理解每个参数的语义。如果你能把这道题背后的稳定排序、比较器、大小写处理这些点都吃透,那么无论它怎么变体,你都能应对。
这题本身不难,但它像一块试金石,能检验你字符串处理的基本功是否扎实。刷题刷到后面你会发现,很多中等题都是基础知识的组合。把 1451 这种题目彻底搞懂,比盲目刷十道变形题更有用。希望这篇复盘能帮到你,也欢迎你在评论区分享你在这题上踩过的坑。
