很多人刷 LeetCode 的时候,看到 1451 这道题——“重新排列句子中的单词”,第一反应是“这不就是个按长度排个序嘛,简单”。做完了才发现,这道题表面上是排序,实际上真正想考的是三件事:你会不会处理大小写、你会不会分词、你知不知道“稳定排序”和“不稳定排序”的区别。这三件事随便哪个没想明白,提交上去就是 WA(Wrong Answer),而且大概率你一时半会儿还看不出自己错在哪。
我最初做这道题的时候,用的就是 C++ 的 std::sort,结果第三个示例就直接挂了。看了半天测试用例,才意识到问题出在“长度相同单词要保持原来的顺序”这句话上。后来我把这道题翻来覆去做了几遍,也看了不少题解,发现很多文章只给了代码,没把背后的关键点讲透。所以这篇我打算把“为什么”讲清楚:为什么是稳定排序、为什么要先整体转小写、为什么有些解法用桶排序更优雅,以及测试用例该怎么构造。
1. 题面拆解:这个“简单题”其实藏了三个考点
1.1 输入输出约束:比你想的更严格
先看题目要求。给你一个句子 text,句子里的单词由单个空格分隔,句子开头没有空格、结尾也没有空格,而且首字母大写,其余字母都是小写。你要做的,是把这些单词按照“长度从短到长”重新排列,然后重新组成一个句子。关键点有两个:第一,如果两个单词长度一样,必须保持它们在原句中的顺序;第二,重排后的句子,仍然要求“第一个单词的首字母大写,其他所有字母小写”。
很多人在第一步就理解错了。题目说的是“句子”,不是“字符串”。你不能直接对整个字符串做字符级的排序,否则整个单词会被打散成一堆字母。我见过有人把 "Leetcode is cool" 排成了 "cdeeLoo..." 这种乱七八糟的内容,这就是没把“单词”作为基本单位来处理。
还有一个容易被忽略的约束是:输入里的单词只有首字母大写,其余字母小写。这其实是个很强的简化条件。正因为如此,你才能安全地先把整个句子统一转成小写,然后排序,最后再把第一个单词的首字母变成大写。如果输入里混着多个大写字母,这个做法就不成立了。
1.2 大小写恢复:首字母大写是最容易漏的一步
我见过不少解法,排序部分写得很漂亮,最后输出的时候忘了处理大小写,直接返回 "is cool leetcode",少了开头的 I 大写,整个判题结果就错了。
正确的处理顺序是:先把所有单词都转成小写,然后做排序,最后把排序结果里的第一个单词首字母大写。
为什么不是“排序前保留原大小写,排序后再恢复”?因为这个句子里的单词,原始状态除了首字母之外都是小写,所以整体转小写不会丢失任何信息。唯一“特殊”的首字母大写,在排序完成后重新把第一个单词的首字母大写就行了。这样做的另一个好处是,你不用去记录“原句里第一个单词是什么”“它排到哪去了”,逻辑上简单很多。
顺带说一句,Python 里 s[0].upper() + s[1:] 这个写法对空字符串会直接抛异常,所以最好加个防御性判断。C++ 里同理,words[0][0] 需要保证 words 非空。
1.3 同长度单词顺序:题目里的“隐藏条件”就藏在这一句
如果不是反复看了几遍题目,我可能到现在还不会把“长度相同保持原顺序”当成一个考点。尤其在刷题量上来之后,很多人看到“排序”两个字就直接上 sort,根本不会去想这个排序是不是稳定的。
这里要记住一个结论:C++ 的 std::sort 不保证稳定,Python 的 list.sort() 和 sorted() 是稳定的,std::stable_sort 是稳定的。具体到这道题,长度相同的单词必须按输入顺序输出。如果用不稳定的排序,即便排序结果经常碰巧正确,也有可能在某些输入下乱序,导致判题失败。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 稳定排序:为什么它决定了这道题的成败
2.1 稳定与不稳定的区别:一个简单的例子
稳定排序的意思是:如果两个元素关键字相同,排序之后它们的相对顺序不改变。
举个例子,有一个句子 "b a c",三个单词长度都是 1。按题目要求,它们应该保持原来的顺序,输出应该是 "B a c"。
用不稳定排序,你可能会得到 "A b c" 之类的任意顺序,这在题目里就是错误答案。虽然有些时候不稳定排序碰巧没改变顺序,但只要用例设计得稍微多一点,比如 "d a c b",那错误就会暴露。
我在实际提交中,就是用这个思路构造了一
