Python 排序指南包括 list排序,字典排序,对象排序,正序逆序,字典value,含有多个元素的list或者元组的排序

本文详细介绍了Python中的排序方法,包括使用`list.sort()`和`sorted()`函数进行基本排序,设置关键函数实现自定义排序,使用`operator`模块进行多级排序,以及处理升序和降序排列。此外,还讨论了排序的稳定性、复杂度和Python 2.x到3.x的排序变化。

Python 排序指南

摘自原文https://docs.python.org/zh-cn/3.9/howto/sorting.html
作者
Andrew Dalke 和 Raymond Hettinger

发布版本
0.1

Python 列表有一个内置的 list.sort() 方法可以直接修改列表。还有一个 sorted() 内置函数,它会从一个可迭代对象构建一个新的排序列表。

在本文档中,我们将探索使用Python对数据进行排序的各种技术。

基本排序

简单的升序排序非常简单:只需调用 sorted() 函数即可。它会返回一个新的已排序列表。

sorted([5, 2, 3, 1, 4])

你也可以使用 list.sort() 方法,它会直接修改原列表(并返回 None 以避免混淆),通常来说它不如 sorted() 方便 ——— 但如果你不需要原列表,它会更有效率。

a = [5, 2, 3, 1, 4]
a.sort()
a

另外一个区别是, list.sort() 方法只是为列表定义的,而 sorted() 函数可以接受任何可迭代对象。

sorted({1: 'D', 2: 'B', 3: 'B', 4: 'E', 5: 'A'})
关键函数

list.sort() 和 sorted() 都有一个 key 形参用来指定在进行比较前要在每个列表元素上调用的函数(或其他可调用对象)。

例如,下面是一个不区分大小写的字符串比较:

sorted("This is a test string from Andrew".split(), key=str.lower)

key 形参的值应该是个函数(或其他可调用对象),它接受一个参数并返回一个用于排序的键。 这种机制速度很快,因为对于每个输入记录只会调用一次键函数。

一种常见的模式是使用对象的一些索引作为键对复杂对象进行排序。例如:

student_tuples = [
    ('john', 'A', 15),
    ('jane', 'B', 12),
    ('dave', 'B', 10),
]
sorted(student_tuples, key=lambda student: student[2])   # sort by age

同样的技术也适用于具有命名属性的

<think>我们有一个需:对字典列表(records)按照一个动态的顺列表(order_list)进行排序。 要1. 将order_list中的字段名转换为小驼峰命名(例如:如果order_list中有"first_name",则排序时使用"firstName"作为键) 2. order_list中的元素可能是字段名,也可能带有负号(表示逆序)。例如:[&#39;name&#39;, &#39;-age&#39;] 表示先按name升,再按age降3. 我们需要一个高效的算法。 步骤: a) 首先,我们需要一个函数将字符串转换为小驼峰格式。注意:原始数据中的键可能是下划线分隔,也可能是其他形式?但根据需,我们只处理order_list中的字段名,并将其转换为小驼峰,然后与records中的字典键进行比较。 注意:records中的字典键可能已经是小驼峰?或者不是?根据问题,我们需要将order_list中的元素转换为小驼峰,然后按照这个转换后的键去records中查找。 但是,records中的键是什么格式?问题没有明确,但根据需,我们是要按照转换后的键去排序,所以我们需要将records中的键与转换后的键进行比较。然而,records中的键可能不是小驼峰。因此,我们需要在排序时使用转换后的键来访问字典。 然而,这里有一个问题:records中的键是原始键,而order_list中给出的字段名需要转换为小驼峰后,才能与records中的键匹配吗?还是说,records中的键也需要转换? 根据问题描述:“将order_list中的元素转换为小驼峰命名格式”,然后按照这个order_list对records排序。所以,在排序时,我们使用转换后的键(小驼峰)来访问records中的值。但是,records中的键并没有被转换,所以我们需要在排序时,将order_list中指定的字段(转换为小驼峰后)作为键去records中取值。 但是,records中的键是什么?比如,records可能是这样的: [{&#39;firstName&#39;: &#39;Alice&#39;, &#39;age&#39;: 30}, ...] 而order_list可能是:[&#39;name&#39;, &#39;-age&#39;],那么我们需要将&#39;name&#39;转换为小驼峰(如果原来是下划线则转换,但这里已经是小驼峰?)所以,实际上,我们可能需要将order_list中的字段转换为小驼峰,然后直接使用这个字符串作为键。 然而,问题在于:records中的键和order_list中的字段名可能是不同的命名格式。例如,order_list中可能是"first_name",而records中键是"firstName"。所以,我们需要将order_list中的每个字段转换为小驼峰,然后这个字符串就是我们要在records中查找的键。 因此,假设records中的键已经是小驼峰格式,那么我们就用转换后的键去访问。但问题没有明确records中的键的格式,所以我们需要按照转换后的键来访问。 所以,在排序时,对于每个记录(字典),我们使用转换后的键(小驼峰)来获取值。 b) 排序支持多字段,且支持逆序(通过字段前的负号)。 我们可以使用Python的sorted函数,并指定key参数为一个函数,该函数返回一个元组元组包含多个字段的值。同时,我们可以根据排序方向(升或降)来调整。 但是,由于排序字段是动态的,我们需要构建一个key函数,它能够根据order_list动态生成这个元组,并且处理逆序。 注意:逆序的处理可以在key函数中通过将数值取反来实现(对于数字),但是字符串等类型取反可能不行。另一种方法是使用sorted的reverse参数,但reverse参数是针对整个排序的,不能分别指定每个字段。 因此,常见的做法是:对于每个字段,如果要,则我们返回一个经过变换的值,使得在升排序下,原本较大的值变成较小的值(对于数字,可以取负数;对于字符串,可以取相反数?但是字符串不能取负数)。所以,我们可以使用一个技巧:对于需要降的字段,我们可以返回该字段的负值(仅适用于数字)?这显然不通用。 更通用的方法是:我们可以为每个字段分别指定排序方向,然后在key函数中,对于需要降的字段,我们使用一个包装器,将其转换为可以反向比较的对象。但是,Python中有一个简单的做法:我们可以使用元组,并在每个字段上使用不同的比较方式,但是元组默认都是升。 另一种标准做法:使用多个排序(多次排序,并且每次排序都是稳定的),但这样效率较低。 推荐做法:使用一个key函数,返回一个元组,其中每个元素根据排序方向进行调整。对于需要降的字段,我们返回该字段的负值(如果字段是数字)?但是,如果字段是字符串,我们无法取负。 因此,我们可以使用一个技巧:对于需要降的字段,我们返回该字段的相反对象(即,如果升就返回原值,降就返回一个包装对象,该包装对象的比较操作是相反的)。但是,自定义包装类可能会降低效率。 实际上,Python的sorted函数允许我们为每个字段分别指定排序方式,但只能通过多次排序(使用稳定排序)来实现。然而,Python排序是稳定的,但多次排序在字段多时效率不高。 更高效且简洁的方法是:构建一个key函数,返回一个元组元组中的每个元素根据排序方向进行变换。对于数值类型,我们可以通过取负来实现降(但要都是数值)。但我们的字段可能是任意类型(字符串、数字等)。 因此,我们可以这样处理:对于需要降的字段,我们将其值进行反转,但是如何反转?我们可以使用一个统一的转换:对于所有类型,我们可以将其转换为可以支持反向比较的元组,例如:(False, x) 表示升,(True, -x) 并不通用。 有一个通用的方法:如果我们需要降,那么我们可以返回一个元组,其中第一个元素是一个标志,这个标志可以反转排序。但是,这样会使得不同字段之间的类型不一致,而且对于混合类型也不友好。 实际上,我们可以这样:对于降字段,我们返回该字段的负值(如果是数字),或者返回该字段的逆序(如果是字符串,我们可以反转字符串?但这样不行,因为字符串比较不是简单的反转,比如"abc"反转后是"cba",但"cba"应该比"abc"大?不对,字符串比较是字典,反转字符串会完全改变顺)。 因此,我们需要一个通用的方法:使用一个自定义的比较函数?但是sorted的key函数不支持自定义比较函数(除非使用cmp参数,但cmp参数在Python3中被移除了,不过可以通过functools.cmp_to_key来使用)。 所以,我们可以考虑两种方案: 方案1:使用多次排序(稳定排序),从最后一个排序字段开始倒排序。这样效率会低一些,因为要多次排序。 方案2:使用一个key函数,返回一个元组元组中每个元素根据排序方向进行包装。对于降,我们使用一个包装类,该包装类实现了__lt__方法,将比较结果反转。 包装类的实现: class Descending: def __init__(self, obj): self.obj = obj def __lt__(self, other): return self.obj > other.obj def __eq__(self, other): return self.obj == other.obj # 注意:还需要实现其他比较方法?但sorted只需要__lt__ 但是,这样会使得排序过程中创建很多对象,可能会影响效率。 方案3:使用cmp_to_key,然后写一个比较函数。比较函数根据order_list动态比较。 考虑到效率,我们可能希望使用内置的操作,避免使用Python层的比较函数(因为cmp_to_key会多次调用比较函数,效率可能低于使用key函数)。 因此,我们可以使用方案2的包装,但仅当字段是降时才包装,升则不包装。这样,在元组比较时,降字段就会因为包装而反向比较。 但是,包装类可能会影响性能,因为创建对象和调用特殊方法会有开销。而且,如果字段是基本类型,包装后比较效率会下降。 方案4:对于每个字段,如果降,则我们将其值取反(如果支持取负操作),否则不变。但是,对于不支持取负的类型(如字符串),我们可以将其映射到一个可比较的域,使得取负操作可行?例如,我们可以使用一个函数将字符串映射到一个数字(比如hash?),但这样不可行,因为不同的字符串可能映射到同一个数字,而且映射后无法反映原始顺。 因此,我们需要一个通用的方法,同时兼顾效率。 考虑到Python中sorted函数的key函数返回的元组比较是高效的(因为元组比较是C实现的),我们希望尽可能使用原生的比较。所以,我们可以这样: 如果字段是数字类型,并且需要降,则返回负值。 如果字段是字符串,并且需要降,则我们可以使用一个技巧:将整个字符串映射到一个元组,其中每个字符取反(比如用255-char),但这样很麻烦,而且不通用(非ASCII字符?)。 实际上,这个问题在标准库中有一个解决方案:我们可以利用元组比较的规则,对于降字段,我们返回一个包装值,该值在比较时与原始值相反。Python中有一个常见的技巧:对于降,我们可以返回(-1 * x)对于数字,但字符串不行。 由于问题没有明确字段的类型,我们必须考虑任意类型。因此,我们选择使用包装类。 但是,为了效率,我们可以尽量避免使用包装类,而采用多次排序(稳定排序)。虽然多次排序在字段多时效率较低(时间复杂度O(k*n*log(n)),其中k是字段数,因为每次排序都是O(n*log(n))),但实际中如果k不大,且n不是特别大,还是可以接受的。 然而,题目要高效,所以我们希望使用一次排序。因此,我们使用包装类。 步骤: 1. 将order_list中的每个字段处理成两部分:字段名(去掉负号)和排序方向(有负号表示降)。 2. 将字段名转换为小驼峰格式。 3. 定义一个key函数,该函数接收一个记录(字典),并返回一个元组元组中的每个元素是: 如果该字段是升,则直接返回记录中该字段的值(通过转换后的字段名获取) 如果是降,则返回一个包装类实例(该包装类实现了__lt__,用于反转比较) 但是,注意:元组比较时,如果某个位置是包装类,那么比较时会调用包装类的__lt__。但是,元组比较时,如果两个元素类型不同,可能会出错。所以,我们需要确保同一个位置总是相同类型(要么都是包装类,要么都是原始类型)。但是,由于我们只在降时使用包装类,升时不用,那么同一个字段在所有记录上都是同样的处理(要么都包装,要么都不包装),所以同一个位置类型是一致的。 但是,不同字段之间,有的包装,有的不包装,这是可以的,因为元组比较是按顺比较每个元素,每个元素独立比较。 因此,我们可以这样实现: 定义包装类(内部类): class ReverseOrder: def __init__(self, obj): self.obj = obj def __lt__(self, other): # 注意:这里我们假设other也是ReverseOrder实例 return self.obj > other.obj # 注意:我们只需要实现__lt__,因为sorted函数在比较时使用<操作 在key函数中: for field in order_list: if field以负号开头: 实际字段名 = field[1:] 转换后的字段名 = to_camel_case(实际字段名) 值 = record[转换后的字段名] 然后返回 ReverseOrder(值) else: 转换后的字段名 = to_camel_case(field) 返回 record[转换后的字段名] # 注意:这里直接返回值 但是,这样返回的元组中,有些元素是原始值,有些是ReverseOrder对象。这样在比较元组时,当比较到ReverseOrder对象时,就会调用ReverseOrder的__lt__方法,实现反向比较。 但是,有一个问题:元组比较时,如果两个元素类型不同(比如一个是原始类型,另一个是ReverseOrder),那么比较可能会出错(比如比较整数和ReverseOrder对象)。但是,我们不会将不同字段的包装对象和原始值进行比较,因为元组比较是按位置独立比较的。而同一个位置,要么都是原始值(升),要么都是ReverseOrder对象(降),所以不会出现跨类型比较。 因此,这个方法是可行的。 但是,注意:如果某个字段的值本身是自定义类型,那么我们需要确保它实现了比较方法。另外,如果字段缺失怎么办? 处理缺失值:如果record中没有转换后的字段名,我们可以考虑使用一个默认值(比如None),但这样可能会出错。所以,我们最好确保records中都有这些字段。 如果字段缺失,我们可以使用get方法,设置默认值,比如None。然后,在比较时,None可能会和任何类型比较,Python3中None和任何非None类型比较会抛出TypeError。所以,我们需要处理缺失值。根据需,我们可以假设字段都存在?或者输入数据必须包含这些字段。 为了健壮性,我们可以设置默认值为None,并处理None值:我们可以将None视为最小值(在升时排在最前,降时排在最后?)或者最大值。这取决于需。这里,我们假设字段都存在。 实现步骤: 1. 编写一个函数将下划线命名转换为小驼峰: def to_camel_case(s: str) -> str: parts = s.split(&#39;_&#39;) return parts[0] + &#39;&#39;.join(part.capitalize() for part in parts[1:]) 注意:order_list中的字段名可能已经包含负号,但我们在转换前要去掉负号(如果有)。 2. 解析order_list,得到每个字段的原始名(去掉负号)和方向(升或降)。 3. 定义包装类(用于降)。 4. 定义key函数。 但是,注意:包装类可能会增加开销。我们也可以尝试不使用包装类,而是通过一个转换函数,将值转换为可反向比较的值。例如,我们可以使用一个函数,对于降字段,返回一个元组,该元组在比较时相当于反向比较。但是,这需要针对不同类型进行不同的处理,很难通用。 因此,我们采用包装类。 代码结构: def dynamic_sort(records, order_list): # 步骤1:解析order_list,生成一个列表,每个元素是一个元组:(字段名(转换后的小驼峰), 是否降) order_fields = [] for field in order_list: if field.startswith(&#39;-&#39;): field_name = field[1:] camel_field = to_camel_case(field_name) order_fields.append((camel_field, True)) else: camel_field = to_camel_case(field) order_fields.append((camel_field, False)) # 步骤2:定义包装类(内部定义,避免污染命名空间) class ReverseOrder: def __init__(self, obj): self.obj = obj def __lt__(self, other): return self.obj > other.obj # 步骤3:定义key函数 def key_func(record): # 注意:record是一个字典 result = [] for field, reverse in order_fields: value = record.get(field) # 使用get,避免KeyError,如果不存在则返回None if reverse: # 降,使用包装 result.append(ReverseOrder(value)) else: result.append(value) return tuple(result) # 步骤4:排序 sorted_records = sorted(records, key=key_func) return sorted_records 但是,这里有一个性能问题:每次比较元组时,如果遇到ReverseOrder对象,就会调用其__lt__方法,而__lt__方法本身会调用两个对象的obj的比较。这比直接比较多了一层函数调用。但是,由于Python中函数调用开销较大,如果数据量很大,可能会影响性能。 另一种思路:我们可以使用cmp_to_key,然后写一个比较函数,这样我们可以避免为每个值创建包装对象。但是,cmp_to_key会将比较函数转换为key函数,而比较函数会被调用O(n*log(n))次,每次比较可能会进行多次字段比较(最坏情况下,每个记录比较k次)。所以,总比较次数是O(k*n*log(n)),和多次排序一样。 而使用包装类,创建对象的开销是O(n*k)(每个记录的每个字段创建一个对象),比较时每个字段比较一次,但每次比较需要两次访问(先取obj,再比较)。所以,我们需要权衡。 考虑到代码简洁性,我们使用包装类。 但是,我们可以优化:如果字段是数字,并且是降,我们可以直接取负值,而不用包装类。这样,对于数字类型,我们可以避免包装类开销。但是,我们需要判断字段值的类型?而且,同一个字段在不同记录中可能有不同类型(虽然不常见),所以我们需要小心。 因此,我们可以增加一个判断:如果字段是降,并且值是数字类型(int, float等),则我们取负值;否则,使用包装类。 这样,对于数字类型,我们可以避免包装类,提高效率。 修改key_func: for field, reverse in order_fields: value = record.get(field) if reverse: if isinstance(value, (int, float)): result.append(-value) else: result.append(ReverseOrder(value)) else: result.append(value) 但是,注意:如果值是None,取负会出错。所以,我们需要先判断value是否为None。如果是None,我们就不取负,而是直接使用None(然后使用包装类包装None?或者直接使用None?)。但是,None和数字比较会出错。 因此,我们需要统一处理None值。我们可以将None视为一个特殊值,比如在排序时,我们希望None排在最前还是最后?这里,我们可以规定:None在升时排在最前(即最小),在降时排在最后(即也是最小?)或者最大?这取决于需。 但是,为了简单,我们假设没有None值(即字段都存在)。如果有None,我们需要定义None的排序行为。 我们可以这样:对于升,None应该排在最前(比任何非None值都小);对于降,我们希望None排在最后(即比任何非None值都小?不对,降时,我们希望大的值在前,那么None因为是小的值,所以应该还在最后)。所以,无论升,None都应该排在同向的最后?不对。 实际上,在升时,我们希望None排在最前(最小),在降时,我们希望None排在最后(因为降是大的在前,小的在后,而None是最小的,所以应该在最后)。所以,降时,我们需要将None视为最小的值,那么降排序后,最小的值在最后。 因此,在降时,如果我们直接使用包装类,那么包装类的__lt__方法会反转比较,所以None会变成“最大”的?这样就会排在最前?这不符合我们的要。 我们需要统一处理None:在key函数中,我们先将None转换为一个可比较的值,使得在升时None最小,降时我们希望None在最后,那么降时我们实际上希望None在比较时表现为“最大”(这样在降排序时,因为降是大的排前,所以None就会排到最后?不对,因为降排序时,我们使用包装类,包装类将比较反转,那么None在包装类中会变成“最小”的(因为原始比较中None是最小,反转后变成最大?),这样在降排序时,None就会排在最前面?这不符合要。 为了避免混淆,我们可以在key函数中,将None值替换为一个特殊的对象,该对象在比较时表现为比所有非None值都小(或者都大),然后根据排序方向调整。 由于时间关系,我们假设字段都存在,不处理None。 因此,我们优化:对于降字段,如果是数字,则取负;否则(包括字符串、None等)使用包装类。 但是,如果值是字符串,我们使用包装类;如果是数字,我们取负。这样,在同一个字段上,如果有些记录是数字,有些是字符串,那么比较时会出错(因为类型不同)。所以,我们必须确保同一个字段的所有值都是同一种类型(数字或非数字)。这通常由数据保证。 因此,我们按照上述优化。 最终代码: 注意:我们只处理int和float,其他类型(如complex)不处理,使用包装类。 另外,对于布尔值,它是int的子类(True=1, False=0),所以也会被取负。但是,取负后,True变成-1,False变成0,这样在降排序时,原本True(1)大于False(0),取负后,-1<0,所以降排序后,False排在True前面,而原本升排序是False, True,降应该是True, False。所以,取负后排序,降得到的是False, True?这不对,因为-1<0,所以False(0)大于True(-1),所以降排序后,False在True前面,而我们需要的是True在False前面。所以,取负对于布尔值是不确的。 因此,我们排除布尔值?或者将布尔值视为整数?但这样会得到错误的结果。所以,我们最好将布尔值视为非数字类型,使用包装类。 修改:只处理整数和浮点数,不处理布尔值(虽然布尔值是整数子类,但我们可以用type判断?或者直接排除布尔值?)。但是,在Python中,布尔值就是整数,所以isinstance(True, int)返回True。所以,我们需要单独判断布尔值。 因此,我们这样:对于降字段,如果值是整数或浮点数,并且不是布尔值(因为布尔值也是int的实例),则取负。否则,使用包装类。 判断布尔值:type(value) is bool,如果是,则不用取负。 所以: if reverse: if isinstance(value, (int, float)) and type(value) is not bool: result.append(-value) else: result.append(ReverseOrder(value)) 但是,这样写,对于布尔值,我们使用包装类,那么包装类会反转比较。例如,两个布尔值:True和False,在升时,False<True,所以升是False,True;降时,我们使用包装类,那么包装类中,True>False,所以比较时,包装类(True) < 包装类(False) 会返回 False(因为包装类的__lt__定义为:self.obj>other.obj,所以当比较包装类(True)和包装类(False)时,实际上是True>False,所以包装类(True)应该大于包装类(False)?不对,在包装类的__lt__中,我们定义a<b为a.obj>b.obj。所以,对于两个包装类对象a和b,a<b 等价于 a.obj>b.obj。所以,如果a.obj=True, b.obj=False,那么a.obj>b.obj为True,所以a<b为True,即a小于b。所以,排序时,a(True)会排在b(False)前面?因为a小于b,所以升排序时,小的在前,所以True在False前面?但我们需要的是降排序,降排序时,我们希望True在前,False在后。所以,这个结果符合要:True排在False前面。 因此,对于布尔值,使用包装类可以得到确的降排序(True在前,False在后)。 所以,我们不需要特殊处理布尔值,因为布尔值也是整数,但是取负对布尔值无效(因为True取负是-1,False取负是0,这样会导致排序错误:False(0)大于True(-1),所以False排在True前面,但我们需要True排在False前面)。所以,我们不应该对布尔值取负,而应该使用包装类。 因此,修改:在取负的条件中,排除布尔值。 if reverse: if isinstance(value, (int, float)) and not isinstance(value, bool): result.append(-value) else: result.append(ReverseOrder(value)) 这样,布尔值就会使用包装类。 另外,对于其他非数字类型(如字符串、元组、列表等),都使用包装类。 最后,我们编写完整的代码。 注意:如果记录中不存在某个字段,我们使用record.get(field)会返回None,然后None会被包装(如果是降且非数字)。但是,None和任何非None值比较会抛出TypeError。所以,我们需要处理None。 为了健壮性,我们可以将None转换为一个特殊值,比如一个最小值对象,这样就不会抛出异常。我们定义一个最小值对象,它比任何对象都小(除了它自己)。 例如: class MinType: def __lt__(self, other): return True # 表示自己小于任何其他对象(除了MinType,我们也要处理) def __gt__(self, other): return False def __eq__(self, other): return isinstance(other, MinType) 然后,在取值时: value = record.get(field, MinType()) 但是,这样会使得所有缺失字段都使用同一个MinType对象,并且它小于任何其他值。这样,在升时,缺失字段会排在最前面;在降时,由于我们使用包装类(或者取负),对于降字段,如果使用包装类,那么MinType对象会被包装,而包装类的__lt__会反转,那么MinType包装后就会变成最大的?这样在降排序时,缺失字段就会排在最前面?但我们需要的是在降时,缺失字段排在最后(因为降是大的在前,缺失字段应该算小的,所以排最后)。所以,我们希望在降时,缺失字段排最后。 因此,在降时,我们希望缺失字段在key函数中返回一个最小值(这样在包装后,最小值会变成最大值?因为包装类的__lt__反转,那么最小值反转后变成最大值?这样在降排序时,最大值排在最前,但缺失字段变成了最大值,就会排在最前?这不符合要(我们希望排最后)。 所以,我们需要在降时,对缺失字段做特殊处理:在降时,我们希望它表现为最小值(这样在包装后,最小值反转成最大值,就会排在最前?)不对,我们希望它排最后,所以应该表现为最大值?不对,因为降排序是大的在前,小的在后。缺失字段应该算小的,所以排最后。 因此,在降时,我们希望缺失字段在比较时表现为最小值(这样在降排序时,最小值排最后)。但是,在包装类中,我们反转比较,所以如果我们在降时返回一个最小值(在包装类中),那么反转后最小值变成最大值,就会排在最前,这不是我们想要的。 所以,我们这样:在降时,我们不对缺失字段进行包装,而是直接返回一个特殊对象,该对象在比较时表现为最大值(这样在降排序时,最大值排在最前?不对,我们希望缺失字段排最后,所以应该是最小值)。这很混乱。 为了避免复杂,我们规定:缺失字段在升时排在最前,在降时也排在最前(即作为最小值处理)。这样,无论升,缺失字段都在最前面。这符合一些系统的行为(如SQL的NULLS FIRST)。但是,用户可能希望缺失字段排在最后(NULLS LAST)。所以,我们需要明确需。 由于问题没有明确,我们假设字段都存在,不处理缺失情况。如果遇到缺失字段,我们抛出KeyError?或者,我们可以要用户确保字段存在。 在代码中,我们使用record[field]而不是record.get(field),这样如果字段不存在,会抛出KeyError。 因此,我们修改: value = record[field] # 如果字段不存在,抛出异常 这样,用户需要确保records中的每个字典都包含order_list中字段(转换后)的键。 最后,代码实现: 注意:由于包装类可能会被用于不同的类型,我们需要确保同一个字段的所有值都是可比较的(否则会抛出TypeError)。 完整代码: 我们将to_camel_case函数和dynamic_sort函数一起写出。</think>以下是一个高效的Python算法实现,用于按照动态顺列表对字典列表进行排序,支持键名转换为小驼峰格式和降排序: ```python def to_camel_case(s: str) -> str: """ 将下划线命名转换为小驼峰命名 例如: &#39;first_name&#39; -> &#39;firstName&#39;, &#39;user_id&#39; -> &#39;userId&#39; """ parts = s.split(&#39;_&#39;) return parts[0] + &#39;&#39;.join(part.capitalize() for part in parts[1:]) def dynamic_sort(records: list[dict], order_list: list[str]) -> list[dict]: """ 按动态顺列表对字典列表排序 :param records: 待排序字典列表 :param order_list: 排序列表,负号表示降 :return: 排序后的字典列表 """ # 解析排序指令 (字段名, 是否降) sort_orders = [] for field in order_list: if field.startswith(&#39;-&#39;): camel_field = to_camel_case(field[1:]) sort_orders.append((camel_field, True)) else: camel_field = to_camel_case(field) sort_orders.append((camel_field, False)) # 定义用于降排序的包装器 class Descending: __slots__ = (&#39;obj&#39;,) # 优化内存使用 def __init__(self, obj): self.obj = obj def __lt__(self, other): return self.obj > other.obj # 构建排序键函数 def key_func(record): key_parts = [] for field, reverse in sort_orders: value = record[field] if reverse: if isinstance(value, (int, float)) and not isinstance(value, bool): key_parts.append(-value) # 数值类型直接取负 else: key_parts.append(Descending(value)) # 非数值类型使用包装器 else: key_parts.append(value) return tuple(key_parts) return sorted(records, key=key_func) ``` ### 算法说明 1. **键名转换**: - 使用`to_camel_case`函数将下划线命名转换为小驼峰格式(如`first_name` → `firstName`) - 转换后键名直接用于字典查找 2. **排序方向处理**: - 字段前缀`-`表示降(如`&#39;-age&#39;`) - 数值类型通过取负实现高效降(无需额外对象) - 非数值类型使用`Descending`包装器反转比较逻辑 3. **多级排序**: - 通过返回元组实现多字段排序优先级 - 元组比较规则:按字段顺依次比较[^1] 4. **性能优化**: - `__slots__`减少包装器内存开销 - 避免不必要的数据复制 - 时间复杂度:O(n log n) ### 使用示例 ```python records = [ {&#39;firstName&#39;: &#39;Alice&#39;, &#39;userAge&#39;: 30, &#39;joinDate&#39;: &#39;2022-01-01&#39;}, {&#39;firstName&#39;: &#39;Bob&#39;, &#39;userAge&#39;: 25, &#39;joinDate&#39;: &#39;2023-05-15&#39;}, {&#39;firstName&#39;: &#39;Alice&#39;, &#39;userAge&#39;: 25, &#39;joinDate&#39;: &#39;2021-12-31&#39;} ] order_list = [&#39;first_name&#39;, &#39;-user_age&#39;] sorted_records = dynamic_sort(records, order_list) """ 结果: [ {&#39;firstName&#39;: &#39;Alice&#39;, &#39;userAge&#39;: 30, &#39;joinDate&#39;: &#39;2022-01-01&#39;}, {&#39;firstName&#39;: &#39;Alice&#39;, &#39;userAge&#39;: 25, &#39;joinDate&#39;: &#39;2021-12-31&#39;}, {&#39;firstName&#39;: &#39;Bob&#39;, &#39;userAge&#39;: 25, &#39;joinDate&#39;: &#39;2023-05-15&#39;} ] 解释: 1. 先按firstName升 (Alice < Bob) 2. 同firstName时按userAge降 (30 > 25) """ ``` ### 处理边界情况 1. **缺失字段处理**: ```python # 在key_func中增加默认值处理 value = record.get(field, None) # None作为默认值 ``` 2. **混合类型比较**: - 相同字段类型需一致(避免字符串与数字比较) - 可添加类型检查确保一致性 3. **None值处理策略**: ```python # 在key_func中定义None值排序行为 if value is None: value = float(&#39;-inf&#39;) if reverse else float(&#39;inf&#39;) # 空值置底 ``` ### 性能对比 | 方法 | 时间复杂度 | 适用场景 | |--------------------|------------|------------------------| | 本实现 | O(n log n) | 通用场景 | | 多次稳定排序 | O(kn log n)| 字段少(k<3)时适用 | | cmp_to_key | O(kn log n)| 兼容旧代码,效率较低 | 该实现通过组合数值取负和智能包装器,在保持代码简洁性的同时优化了排序性能[^2]。
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值