排序是日常编程处理中最常用的功能之一。python 排序有两层内容含义,一层是 python 本身提供的排序函数,一层是用 python 去实现各种排序演算法。

本文主要探讨 python 自身提供的排序函数,涉及函数用法介绍、对比差异、实际场景中的使用范例(主要是面向复杂数据结构排序时如何使用的问题),以及稍微展开来了解一些排序函数底层的实现。很多文章都会涉及其中的一部分,但从实用角度,不管是 Coding 还是面试,综合和深入是更看重的。限于篇幅可能一篇介绍不完,希望能抛砖引玉,大家共同分析完善。

首先我们简单回顾几个函数及其用法范例。

Python 中有 3 个排序函数,一个是 list 内置的 sort() 方法,另一个是全局的 sorted() 方法,而 argsort() 函数,是 numpy 库中的函数。

sorted() 和列表方法 sort() 对数据进行排序,要求参与排序的数据支持关系运算符,也就是这些数据本身是可以比较大小,除非使用 key 参数明确指定了特别的排序规则。对于绝大多数内置类型的对象而言,同类型的对象之间基本上都是支持关系运算符的。

在进入详细介绍之前,我们先简单说说 sort() 、sorted() 和 argsort() 的区别:

  • sort() 是 List 的一个方法,既然是 List 自身的方法,所以对列表本身的顺序是会改变的,返回值为空。
  • sorted() 是全局方法,所以不会对原数据结构做改变,而是返回一个已排序的新列表。
  • Argsort() 函数返回的则是数组值从小到大的索引值(此说法有点模糊,下面详细说)。

所以很多问题来了。包括但不限于如下一些疑问(如果我要是面试官,我肯定会这样问):

问:两个列表,sort 其中一个,什么情况下会导致另外一个也有变化?

问:sort() 的对象可以是元组吗?可以是字典吗?

问:sorted() 里面的排序函数怎么定义使用?

问:复杂数据结构,例如列表中嵌套元组、字典时,如何排序?

这些使用上的问题搞清楚了,「方显英雄本色」,不会在面试时卡壳,让你拥有在面试官面前滔滔不绝的「能量」。也会让你在解决问题时拥有更多技巧。

先来说 sort()。这个方法相对简单。

list.sort(key=None,reverse=False)

#将 list 自身进行排序,不返回新的 list 对象,默认升序; reverse:-True 降序 -False正序

需要注意,这个排序执行后是直接改变原有列表的。适用场景自然是需要直接改变原有顺序的场景

sorted<iterable,key=None,reverse=False>

#返回排好序的新列表,不改变对象本身,默认升序; reverse:-True 降序 -False 正序。对所有可迭代的对象均有效

#参数 iterable 可以是 list 或者 iterator;

argsort<a, axis=-1,kind=quicksort,order=None>

#axis是列编号

Argsort() 是 Numpy 模块中的函数,作用是将输入数据结构中的元素从小到大排列,提取其对应的 index (索引),然后返回。我看到有的文章说 argsort() 函数返回的是数组值从小到大的索引值。这个说法很容易让人误解。

针对元组、字典或复杂 List 的排序

大部分文章,包括本文前面介绍的用法都是演示性质的,科普一下而已。真正到实际的应用环境中,不可能都是对几个整数的来回排序。很有可能是一堆复杂数据结构组成的列表排序,来解决多个对象之间的顺序问题。这一小节我们就试著来介绍一下。

先说元组,在默认情况下 sort() 和 sorted() 函数接收的参数是元组时,它将会先按元组的第一个元素进行排序再按第二个元素进行排序,再按第三个、第四个…依次排序。

我们通过一个简单的例子来了解它,以下面这个 list 为例:

src_data=[<1,A>,<2,C>,<2,B>,<0,b>,<0,a>]

我们通过 sorted() 对它进行排序

>>> src_data=[<1,A>,<2,C>,<2,B>,<0,b>,<0,a>]
>>> result=sorted<src_data>
>>> print<result>
[<0,b>,<1,A>,<2,B>,<2,C>,<2,a>]

会发现排序后的结果中 (2, 『B』) 在 (2, 『C』) 的前面,接下来是 (2,』a』)。排序执行的顺序是先按元组第一个元素排好之后,将 (2, 『B』), (2, 『C』), (2, 『a』)) 再按第二个元素进行排序,而 』B』 的 ASCII 编码最小,所以 (2, 『B』) 就排在几个的最前面了。

问题:想要让它排序时不分大小写如何操作?

这就要用 sort() 方法和 sorted() 方法里的key参数。例子如下:

src_data=[<1,A>,<2,C>,<2,B>,<0,b>,<0,a>]

#利用参数 key

>>> result=sorted<src_data,key=lambda src_data:<src_data[0],src_data[1].lower<>>>
>>> print<result>
[<0,b>,<1,A>,<2,a>,<2,B>,<2,C>]

其中的 lambda srtc_data:(src_data[0],src_data[1].lower() 可以理解为一个匿名函数;其中的 src_data 其实也不一定必须叫这个名字,你可以随便叫 xyz 都可以,保持前后一致即可。

问题:如果想要以字母作为第一排序规则,该如何实现?

这就要用到之前所讲到的在默认情况下 sort() 和 sorted() 函数接收的参数是元组时,它将会先按元组的第一个元素进行排序再按第二个元素进行排序,再按第三个以至于第 N 个依次排序。

再配合 lambda 返回一个自定义元组;

src_data=[<1,A>,<2,C>,<2,B>,<0,b>,<0,a>]

#将 src_data[1].lower() 作为返回元组里的首元素,按照 sorted 的排序规律,就会先按字母排序,再按数字排序。

>>> result=sorted<src_data,key=lambda src_data:<src_data[1].lower<>,src_data[0]>>
>>> print<result>
[<1,A>,<1,a>,<0,b>,<2,B>,<2,C>]

我们再来看以 dict 作为 list 的元素需要如何使用排序函数。以下面为例:

src_data=[{name:T,wgt:60},{name:K,wgt:55},{name:J,wgt:65}]

#将 src_data[wgt] 作为返回 dict 的首元素

>>> src_data=[{name:T,wgt:60},{name:K,wgt:55},{name:J,wgt:65}]
>>> result=sorted<src_data,key=lambda src_data:<src_data[wgt],src_data[name,1]>>
>>> print<result>
[{name:T,wgt:60},{name:K,wgt:55},{name:J,wgt:65}]

几个排序演算法的底层原理背景及执行效率

说到排序不得不提到两个词「时间复杂度」和「演算法的稳定性」。时间复杂度大家聊的比较多,排序演算法的稳定性略显高深。通俗地讲就是能保证排序前 2 个相等的数其在序列的前后位置顺序和排序后它们两个的前后位置顺序相同。在简单形式化一下,如果 Ai = Aj,Ai 原来在位置前,排序后 Ai 还是要在 Aj 位置前。再通俗一点,意思是就算两个元素比不出大小,在每次排序的结果里他们的相对位置是固定的。比如冒泡排序和插入排序就是稳定的,而快速排序和希尔排序就不是稳定的。

划重点:从 python2.2 开始,排序被保证为稳定的。意思是说多个元素如果有相同的 key,则排序前后他们的先后顺序不变。

对于 python 的 sort() 和 sorted(),我没有看过源码,自然没有权威的说法。但多方查找资料,基本指向是 sorted() 是使用了 Timesort 演算法,这是一种稳定演算法。而 sort()有说是 Timesort,另一说采用的是混合(hybrid)排序,规模小的时候采用 binary insertion,规模大的时候采用 sample sort。

既然 Timesort 是高频的,我们就稍微介绍一下它。 Timsort 是结合了归并排序和插入排序而得到的排序演算法,它有较好的效率。Tim Peters 在 2002 年设计了该演算法并在 Python 中使用。该演算法找到数据中已经排好序的块-分区,每个分区叫 run,然后按规则合并这些 run。

Timesort 演算法本身我们就不详细介绍了,作为背景知识点到为止。我们可以试著考虑一下 sort() 和 sorted() 哪个执行效率高的问题。这在大规模数据处理的情况下会有一定的差异性。至于怎么做,大家可以一起试试。无非有两种验证办法,一种是构建大规模数据,两种演算法执行计时;一种是从理论上去分析二者的差异性,包括带不同的返回值这种细微差异都要考虑。最终很可能是二者的结合验证。本文限于篇幅暂不展开,后续另成文来试著分析。

技术交流群:QQ1群:365534424 QQ2群:238757010


推荐阅读:
相关文章