
这道题的本质:自定义排序
这道题不是普通的“按字母表排序”,而是要求你按照题目给定的特殊规则排序字符串 S。
排序后的顺序必须是:
- 小写字母在最前面,并且小写字母内部按字母顺序排序
- 大写字母排在小写字母后面,并且大写字母内部按字母顺序排序
- 奇数数字排在大写字母后面,并且奇数内部按从小到大排序
- 偶数数字排在最后,并且偶数内部按从小到大排序
所以它的排序优先级可以理解成:
小写字母 < 大写字母 < 奇数数字 < 偶数数字
题目给的例子是:
Sorting1234
我们先拆开看:
小写字母:o r t i n g
大写字母:S
奇数数字:1 3
偶数数字:2 4
分别排序后:
小写字母:g i n o r t
大写字母:S
奇数数字:1 3
偶数数字:2 4
拼起来就是:
ginortS1324
最容易理解的做法:分组,再排序,再拼接
初学者建议先用这种方法。因为它非常符合人的思考过程。
思路
看到一个字符,我们先判断它属于哪一类:
如果是小写字母,放进 lowercase
如果是大写字母,放进 uppercase
如果是数字:
如果是奇数,放进 odd_digits
如果是偶数,放进 even_digits
最后分别排序,再拼起来。
代码写法一:分组法,适合初学者
s = input()
lowercase = []
uppercase = []
odd_digits = []
even_digits = []
for ch in s:
if ch.islower():
lowercase.append(ch)
elif ch.isupper():
uppercase.append(ch)
elif ch.isdigit():
if int(ch) % 2 == 1:
odd_digits.append(ch)
else:
even_digits.append(ch)
result = (
sorted(lowercase)
+ sorted(uppercase)
+ sorted(odd_digits)
+ sorted(even_digits)
)
print("".join(result))
逐行解释
先读入字符串:
s = input()
比如用户输入:
Sorting1234
那么变量 s 里面保存的就是这个字符串。
然后准备四个列表,用来分别保存四类字符:
lowercase = []
uppercase = []
odd_digits = []
even_digits = []
接着遍历字符串中的每一个字符:
for ch in s:
比如 Sorting1234 会依次取出:
S
o
r
t
i
n
g
1
2
3
4
判断是否是小写字母:
if ch.islower():
lowercase.append(ch)
例如 o、r、t、i、n、g 都会进入 lowercase。
判断是否是大写字母:
elif ch.isupper():
uppercase.append(ch)
例如 S 会进入 uppercase。
判断是否是数字:
elif ch.isdigit():
数字还要继续判断是奇数还是偶数:
if int(ch) % 2 == 1:
odd_digits.append(ch)
else:
even_digits.append(ch)
这里要注意,ch 是字符串字符,比如 "1",不是数字 1。
所以要先写:
int(ch)
把字符串形式的数字转换成整数,然后才能判断奇偶。
例如:
int("3") % 2 == 1
说明 "3" 是奇数。
为什么最后要 sorted()?
题目要求每一类内部也要排序。
例如小写字母:
['o', 'r', 't', 'i', 'n', 'g']
排序后变成:
['g', 'i', 'n', 'o', 'r', 't']
所以最后写:
sorted(lowercase)
注意:sorted() 返回的是一个新的列表,不会直接变成字符串。
最后四部分拼起来:
result = (
sorted(lowercase)
+ sorted(uppercase)
+ sorted(odd_digits)
+ sorted(even_digits)
)
这时候 result 还是一个列表,大概是:
['g', 'i', 'n', 'o', 'r', 't', 'S', '1', '3', '2', '4']
所以要用:
"".join(result)
把字符列表合成字符串。
代码写法二:使用 sorted 的 key 参数
这道题在 HackerRank 上常见的简洁写法是使用 sorted() 的 key 参数。
s = input()
def sort_key(ch):
if ch.islower():
return (0, ch)
elif ch.isupper():
return (1, ch)
elif ch.isdigit() and int(ch) % 2 == 1:
return (2, ch)
else:
return (3, ch)
print("".join(sorted(s, key=sort_key)))
key 的核心思想
sorted() 默认只知道普通排序,但是这道题需要特殊规则。
所以我们告诉 Python:
每个字符排序之前,先给它一个排序标签
比如:
小写字母 -> 第 0 类
大写字母 -> 第 1 类
奇数数字 -> 第 2 类
偶数数字 -> 第 3 类
所以:
return (0, ch)
意思是:
先按照第 0 类排序
如果类别一样,再按照 ch 自身排序
例如:
'g' -> (0, 'g')
'i' -> (0, 'i')
'S' -> (1, 'S')
'1' -> (2, '1')
'2' -> (3, '2')
Python 排序元组时,会先比较第一个元素,再比较第二个元素。
所以:
(0, 'g') 会排在 (1, 'S') 前面
因为 0 < 1。
如果都是小写字母:
(0, 'g')
(0, 'i')
(0, 'n')
第一项都是 0,Python 就继续比较第二项,也就是字母本身,因此得到字母顺序。
更短的 lambda 写法
熟练以后,这道题可以写成:
s = input()
print("".join(sorted(s, key=lambda ch: (
ch.isdigit(),
ch.isdigit() and int(ch) % 2 == 0,
ch.isupper(),
ch
))))
但是这个写法对初学者不太友好,因为它利用了布尔值排序:
False < True
所以现阶段不建议你优先记这个写法。
你真正应该先掌握的是前面两个版本:
分组法
自定义 key 函数法
常见错误与易混点
1. 以为直接 sorted(s) 就可以
不可以。
print("".join(sorted(s)))
这是普通排序,不符合题目的特殊顺序。
普通排序可能会按照字符编码顺序排,不会自动满足:
小写字母 -> 大写字母 -> 奇数 -> 偶数
2. 忘记把数字字符转换成 int
错误写法:
if ch % 2 == 1:
这里会报错,因为 ch 是字符串,不是整数。
正确写法:
if int(ch) % 2 == 1:
3. 忘记 join
sorted() 得到的是列表。
例如:
['g', 'i', 'n', 'o', 'r', 't']
题目要求输出字符串,所以必须写:
"".join(result)
4. 分组顺序写错
题目要求是:
小写字母 -> 大写字母 -> 奇数数字 -> 偶数数字
不是:
小写字母 -> 大写字母 -> 所有数字
也不是:
字母 -> 数字
数字还要再分成奇数和偶数。
本题推荐你记住的解题套路
遇到这种题,可以按这个思路想:
第一步:题目是不是普通排序?
第二步:如果不是普通排序,有哪些分类?
第三步:每一类内部是否还要排序?
第四步:分类后排序,最后拼接输出。
这道题对应就是:
分类:
小写字母、大写字母、奇数数字、偶数数字
每类内部:
都要排序
最后:
小写 + 大写 + 奇数 + 偶数
推荐答案
初学阶段建议提交这个版本:
s = input()
lowercase = []
uppercase = []
odd_digits = []
even_digits = []
for ch in s:
if ch.islower():
lowercase.append(ch)
elif ch.isupper():
uppercase.append(ch)
elif ch.isdigit():
if int(ch) % 2 == 1:
odd_digits.append(ch)
else:
even_digits.append(ch)
result = (
sorted(lowercase)
+ sorted(uppercase)
+ sorted(odd_digits)
+ sorted(even_digits)
)
print("".join(result))
等你熟悉 sorted(..., key=...) 以后,可以再使用这个版本:
s = input()
def sort_key(ch):
if ch.islower():
return (0, ch)
elif ch.isupper():
return (1, ch)
elif int(ch) % 2 == 1:
return (2, ch)
else:
return (3, ch)
print("".join(sorted(s, key=sort_key)))
练习
题目:
输入一个只包含大小写字母和数字的字符串,要求按照下面规则排序:
数字排最前面
小写字母排第二
大写字母排最后
每一类内部都按升序排序
例如输入:
bA3aC1
应该输出:
13abAC
提示:
你可以准备三个列表:
digits = []
lowercase = []
uppercase = []
遍历字符串时,分别用:
ch.isdigit()
ch.islower()
ch.isupper()
判断字符属于哪一类。
补充:sorted(s, key=sort_key) 这一句整体是什么意思?
这一句可以拆成两部分看:
sorted(s, key=sort_key)
意思是:
把字符串 s 里面的每个字符拿出来排序,
但是排序时不要用 Python 默认规则,
而是用 sort_key 这个函数提供的规则来排序。
这里最关键的是:
key=sort_key
它表示:
排序之前,先把每个字符交给 sort_key 函数处理一下,
sort_key 返回什么,Python 就根据返回值来决定排序顺序。
注意:key 不是关键字本身的意思,而是“排序依据”。
不加 key 时,sorted 怎么排?
比如:
s = "Sorting1234"
print(sorted(s))
Python 会按照默认字符顺序排序。
但是默认排序不符合题目要求,因为题目要求的是:
小写字母 -> 大写字母 -> 奇数数字 -> 偶数数字
这不是 Python 默认知道的规则,所以我们要告诉 sorted() 一个新的排序规则。
key 参数的作用:给每个字符生成一个“排序标签”
假设我们写了这个函数:
def sort_key(ch):
if ch.islower():
return (0, ch)
elif ch.isupper():
return (1, ch)
elif int(ch) % 2 == 1:
return (2, ch)
else:
return (3, ch)
这个函数的作用不是直接排序,而是给每个字符生成一个“排序用的值”。
例如:
sort_key("g")
返回:
(0, "g")
表示:
"g" 是第 0 类,也就是小写字母
在小写字母内部,按照 "g" 自己排序
再比如:
sort_key("S")
返回:
(1, "S")
表示:
"S" 是第 1 类,也就是大写字母
再比如:
sort_key("3")
返回:
(2, "3")
表示:
"3" 是第 2 类,也就是奇数数字
再比如:
sort_key("2")
返回:
(3, "2")
表示:
"2" 是第 3 类,也就是偶数数字
sorted(s, key=sort_key) 的执行过程
假设:
s = "Sorting1234"
执行:
sorted(s, key=sort_key)
Python 大致会这样做:
拿到 "S" -> 调用 sort_key("S") -> 得到 (1, "S")
拿到 "o" -> 调用 sort_key("o") -> 得到 (0, "o")
拿到 "r" -> 调用 sort_key("r") -> 得到 (0, "r")
拿到 "t" -> 调用 sort_key("t") -> 得到 (0, "t")
拿到 "i" -> 调用 sort_key("i") -> 得到 (0, "i")
拿到 "n" -> 调用 sort_key("n") -> 得到 (0, "n")
拿到 "g" -> 调用 sort_key("g") -> 得到 (0, "g")
拿到 "1" -> 调用 sort_key("1") -> 得到 (2, "1")
拿到 "2" -> 调用 sort_key("2") -> 得到 (3, "2")
拿到 "3" -> 调用 sort_key("3") -> 得到 (2, "3")
拿到 "4" -> 调用 sort_key("4") -> 得到 (3, "4")
然后 Python 根据这些返回值来排序。
也就是说,它真正比较的不是原来的字符本身,而是这些元组:
"S" -> (1, "S")
"o" -> (0, "o")
"r" -> (0, "r")
"t" -> (0, "t")
"i" -> (0, "i")
"n" -> (0, "n")
"g" -> (0, "g")
"1" -> (2, "1")
"2" -> (3, "2")
"3" -> (2, "3")
"4" -> (3, "4")
为什么返回的是 (0, ch) 这种形式?
因为元组可以按顺序比较。
Python 比较两个元组时,会先比较第一个位置。
比如:
(0, "g") < (1, "S")
结果是 True,因为:
0 < 1
所以小写字母会排在大写字母前面。
再比如:
(2, "1") < (3, "2")
结果也是 True,因为:
2 < 3
所以奇数数字会排在偶数数字前面。
如果第一个位置一样,就继续比较第二个位置。
比如:
(0, "g") < (0, "i")
第一个位置都是 0,说明它们都是小写字母。
于是 Python 继续比较第二个位置:
"g" < "i"
所以 "g" 排在 "i" 前面。
因此:
return (0, ch)
可以理解成:
先按类别排,再按字符本身排。
这道题里的分类编号
我们人为规定:
0 表示小写字母
1 表示大写字母
2 表示奇数数字
3 表示偶数数字
所以:
def sort_key(ch):
if ch.islower():
return (0, ch)
elif ch.isupper():
return (1, ch)
elif int(ch) % 2 == 1:
return (2, ch)
else:
return (3, ch)
这段代码其实是在告诉 Python:
如果 ch 是小写字母,就让它排在第 0 组
如果 ch 是大写字母,就让它排在第 1 组
如果 ch 是奇数数字,就让它排在第 2 组
否则就是偶数数字,让它排在第 3 组
因为数字越小,排序越靠前,所以最终顺序就是:
小写字母 -> 大写字母 -> 奇数数字 -> 偶数数字
一个更小的例子
先看简单字符串:
s = "bA1a2"
def sort_key(ch):
if ch.islower():
return (0, ch)
elif ch.isupper():
return (1, ch)
elif int(ch) % 2 == 1:
return (2, ch)
else:
return (3, ch)
print(sorted(s, key=sort_key))
每个字符对应的排序依据是:
"b" -> (0, "b")
"A" -> (1, "A")
"1" -> (2, "1")
"a" -> (0, "a")
"2" -> (3, "2")
排序后:
"a" -> (0, "a")
"b" -> (0, "b")
"A" -> (1, "A")
"1" -> (2, "1")
"2" -> (3, "2")
所以结果是:
['a', 'b', 'A', '1', '2']
如果要输出字符串,还要用:
"".join(sorted(s, key=sort_key))
最终输出:
abA12
key=sort_key 为什么不写成 key=sort_key()?
这是一个非常重要的点。
正确写法是:
sorted(s, key=sort_key)
不是:
sorted(s, key=sort_key())
原因是:
key=sort_key
表示把函数本身交给 sorted()。
然后 sorted() 会在排序过程中,自己拿每个字符去调用这个函数。
也就是 Python 内部会做类似这样的事情:
sort_key("S")
sort_key("o")
sort_key("r")
sort_key("1")
而如果你写:
key=sort_key()
意思就变成:
现在立刻调用 sort_key 函数
可是 sort_key(ch) 需要一个参数 ch,你没有给它参数,所以会报错。
所以要记住:
key=函数名
意思是:
把这个函数交给 sorted,让 sorted 自己去调用它。
完整代码再看一遍
s = input()
def sort_key(ch):
if ch.islower():
return (0, ch)
elif ch.isupper():
return (1, ch)
elif int(ch) % 2 == 1:
return (2, ch)
else:
return (3, ch)
print("".join(sorted(s, key=sort_key)))
这句:
sorted(s, key=sort_key)
可以翻译成:
对 s 里面的字符排序;
排序时,每个字符先交给 sort_key;
sort_key 返回的元组就是排序依据。
你可以这样记
key 参数的核心记法是:
key 用来指定“按什么排序”。
普通排序:
sorted(s)
意思是:
直接按元素本身排序。
自定义排序:
sorted(s, key=sort_key)
意思是:
先用 sort_key 给每个元素生成排序依据,
再按照这个依据排序。
小练习
不要急着写完整代码,先判断下面每个字符的 sort_key 返回值是什么。
def sort_key(ch):
if ch.islower():
return (0, ch)
elif ch.isupper():
return (1, ch)
elif int(ch) % 2 == 1:
return (2, ch)
else:
return (3, ch)
请你写出下面几个字符对应的返回值:
"a"
"B"
"7"
"8"
提示:
先判断它属于哪一类,再写出对应的编号。



