Python中的快速排序算法
就像合并排序一样,快速排序算法采用分治法的原理将输入数组分为两个列表,第一个包含小项目,第二个包含大项目。然后,该算法将对两个列表进行递归排序,直到对结果列表进行完全排序为止。
划分输入列表称为对列表进行分区。快速排序首先选择一个主元素并围绕这个主元素对列表进行分区,将每个较小的元素放入一个低数组,将每个较大的元素放入一个高数组。
将低链表中的每个元素放到主链表的左边,将高链表中的每个元素放到主链表的右边,使主链表精确地位于排序后的主链表中所需要的位置。这意味着该函数现在可以递归地对low和high应用相同的过程,直到整个列表被排序。
在Python中实现Quicksort
这是quicksort的一个相当紧凑的实现:
以下是代码摘要:
如果数组包含的元素少于两个,第6行将停止递归函数。第12行从列表中随机选择主元素,然后对列表进行分区。第19行和第20行将所有小于pivot的元素放入名为low的列表中。第21行和第22行把每一个等于主元素的元素放到这个叫做相同的列表中。第23行和第24行将每个大于pivot的元素放入名为high的列表中。第28行递归地对低链表和高链表进行排序,并将它们与同一链表的内容组合在一起。下面是快速排序对数组进行排序的步骤的说明[8, 2, 6, 4, 5]:
黄线表示阵列的划分成三个列表:low,same,和high。绿线表示排序并将这些列表放在一起。以下是步骤的简要说明:
主元是随机选择的。在这种情况下,主元是 6第一个传递分区输入数组,low包含[2,4,5],同样包含[6],high包含[8]。然后递归地调用quicksort(),以low作为输入。这将选择一个随机的主元,并将数组分为[2]低、[4]相同和[5]高。这个过程还在继续,但是在这一点上,low和high都有两个以下的项。这就结束了递归,函数将数组重新组合在一起。将排序后的low和high分别添加到同一个列表的任意一端,生成[2、4、5]。另一方面,包含[8]的高链表有少于两个元素,因此算法返回排序后的低数组,它现在是[2,4,5]。将其与same([6])和high([8])合并将生成最终的排序列表。选择主元
为什么上面的实现会随机选择主元素?一致地选择输入列表的第一个或最后一个元素不是同样的吗?
由于快速排序算法的工作方式,递归层的数量取决于主元在每个分区中的位置。在最佳情况下,算法始终选择中位数元素作为轴心。这将使每个生成的子问题的大小正好是前一个问题的一半,导致最多达到log2n级别。
另一方面,如果算法始终选择数组中最小或最大的元素作为主元素,那么生成的分区将尽可能不相等,导致n-1递归级别。这是快速排序最坏的情况。
正如您所看到的,快速排序的效率通常取决于主选择。如果输入数组是未排序的,那么使用第一个或最后一个元素作为主元素将与随机元素一样工作。但是,如果输入数组是排序的或几乎是排序的,使用第一个或最后一个元素作为主元素可能会导致最坏的情况。随机选择支点使得快速排序更有可能选择一个更接近中位数的值,并且完成得更快。
选择主元的另一种方法是找到数组的中值,并强制算法使用它作为主元。这可以在O(n)时间内完成。虽然这个过程有点复杂,但是使用中间值作为快速排序的主元素可以确保您得到最好的大O场景。
衡量Quicksort的大复杂性
使用快速排序,输入列表在线性时间O(n)内进行分区,这个过程递归地重复平均log2n次。这就导致了O(n log2n)的最终复杂度。
也就是说,还记得关于支点的选择如何影响算法运行时的讨论吗?O(n)最佳情况发生在所选的主元接近数组的中位数时,O(n2)情况发生在主元是数组的最小值或最大值时。
从理论上讲,如果算法首先集中于寻找中值,然后将其用作主元素,那么最坏情况复杂度将降至O(n log2n)。数组的中位数可以在线性时间内找到,使用它作为轴心可以保证代码的快速排序部分将在O(n log2n)中执行。
通过使用中值作为主元,最终得到O(n) + O(n log2n)的运行时。可以化简为O(n log2n)因为对数部分的增长速度比线性部分快得多。
定时实施Quicksort
到目前为止,您已经熟悉了算法运行时间的计时过程。只需在第8行中更改算法的名称即可:
您可以像以前一样执行脚本:
Shell
$ python sorting.py
Algorithm: quicksort. Minimum execution time: 0.11675417600002902
快速排序不仅在不到一秒内完成,而且比合并排序快得多(0.11秒对0.61秒)。将ARRAY_LENGTH指定的元素数量从10,000增加到1,000,000并再次运行脚本,最终会在97秒内完成合并排序,而快速排序仅在10秒内对列表进行排序。
分析快速排序的优缺点
顾名思义,快速排序非常快。虽然理论上最坏的情况是O(n2),但实际上,快速排序的良好实现胜过大多数其他排序实现。另外,就像归并排序一样,快速排序也可以直接并行化。
快速排序的主要缺点之一是不能保证它将达到平均运行时复杂度。尽管最坏的情况很少出现,但某些应用程序不能冒性能差的风险,因此它们会选择O(n log2n)以内的算法,而不管输入是什么。
就像归并排序一样,快速排序也需要用内存空间来换取速度。这可能会成为对较大列表进行排序的限制。
对10个元素的列表进行快速排序,可以得到以下结果:
Shell
Algorithm: bubble_sort. Minimum execution time: 0.0000909000000000014
Algorithm: insertion_sort. Minimum execution time: 0.00006681900000000268
Algorithm: quicksort. Minimum execution time: 0.0001319930000000004
结果表明,当列表足够小时,快速排序也要付出递归的代价,完成时间比插入排序和冒泡排序都要长。