📅  最后修改于: 2020-09-05 10:58:12             🧑  作者: Mango
Quicksort是一种流行的排序算法,经常与Merge Sort一起使用。这是高效排序算法的一个很好的例子,平均复杂度为O(nlogn)。其受欢迎程度的部分原因还在于易于实施。
我们将在本文的第一部分中使用简单的整数,但是将给出一个示例,说明如何更改此算法以对自定义类的对象进行排序。
快速排序是三种类型的排序算法的代表:分而治之,就地,和不稳定的。
当您对对象而不是原始类型进行排序时,这一点变得很重要。例如,假设您有几个Person
具有相同对象的对象age
,即Dave年龄为21岁,Mike年龄为21岁。如果要对同时包含Dave和Mike的集合(按年龄排序)使用Quicksort,则不能保证Dave会出现每次您运行算法时都要先于Mike,反之亦然。
该算法的基本版本执行以下操作:
通过使用伪随机元素并将其用作支点,将集合分为两个(大致相等)部分。
小于枢轴的元素将移至枢轴的左侧,大于枢轴的元素将移至枢轴的右侧。
对枢轴左侧的集合以及枢轴右侧的元素数组重复此过程,直到对整个数组进行排序为止。
当我们将元素描述为比另一个元素“大”或“小”时-不一定意味着更大或更小的整数,我们可以按选择的任何属性进行排序。
如果我们有一个自定义类Person
,并且每个人都有一个name
和age
,则可以按name
(按字典顺序)或按年龄(按升序或降序)排序。
Quicksort通常不会将数组分成相等的部分。这是因为整个过程取决于我们如何选择支点。我们需要选择一个枢轴,使其大约比元素的一半大,因此比元素的另一半小。虽然看起来很直观,但是很难做到。
仔细考虑一下-如何为阵列选择合适的支点?Quicksort的历史上已经提出了许多有关如何选择枢轴的想法-随机选择一个元素,这是行不通的,因为选择一个随机元素的“昂贵”方式虽然不能保证良好的枢轴选择,但它是无效的。从中间挑选一个元素;选择第一个,中间和最后一个元素的中位数;甚至更复杂的递归公式。
最简单的方法是简单地选择第一个(或最后一个)元素。具有讽刺意味的是,这导致Quicksort在已排序(或几乎已排序)的阵列上表现很差。
这就是大多数人选择实施Quicksort的方式,并且由于它很简单并且选择枢轴的这种方式是非常有效的操作(并且我们将需要重复执行),这正是我们将要做的。
现在我们已经选择了一个支点-我们将如何处理它?同样,有几种方法可以进行分区本身。我们将有一个指向枢轴的“指针”,一个指向“较小”元素的指针和一个指向“较大”元素的指针。
目标是移动元素,以便所有小于枢轴的元素都在其左侧,而所有较大元素都在其右侧。越来越大的元素并不一定要排序,我们只希望它们位于轴的正确一侧。然后,我们递归地遍历枢轴的左侧和右侧。
逐步查看我们计划做的事情将有助于说明该过程。使用下面显示的数组,我们选择了第一个元素作为枢轴(29),紧随其后的是指向较小元素的指针(称为“低”),而指向较大元素的指针(称为“高”)从结尾开始。
29 | 99(低),27,41,66,28,44,78,87,19,31,76,58,88,83,97,12,21,44 (高)
high
向左移动,直到找到一个低于中心点的值。29 | 99(低),27,41,66,28,44,78,87,19,31,76,58,88,83,97,12,21 (高),44
29 | 21(低),27,41,66,28,44,78,87,19,31,76,58,88,83,97,12,99 (高),44
这个过程一直持续到低指针和高指针最终在单个元素中相遇为止:
29 | 21,27,12,19,28 (低/高),44,78,87,66,31,76,58,88,83,97,41,99,44
28,21,27,12,19,29,44,78,87,66,31,76,58,88,83,97,41,99,44
如您所见,我们已经实现了所有小于29的值现在都在29的左侧,所有大于29的值都在右侧。
然后,算法对28,21,27,12,19(左侧)集合和44,78,87,66,31,76,58,88,83,97,41,99,44做相同的操作(右侧)集合。
Quicksort是一种自然递归算法-将输入数组划分为较小的数组,将元素移至枢轴的适当一侧,然后重复。
让我们看一下一些递归调用的外观:
话虽如此,我们将利用两个函数- partition()
和quick_sort()
。该quick_sort()
函数将首先partition()
收集集合,然后在分割的部分上递归调用自身。
让我们从partition()
函数开始:
def partition(array, start, end):
pivot = array[start]
low = start + 1
high = end
while True:
# If the current value we're looking at is larger than the pivot
# it's in the right place (right side of pivot) and we can move left,
# to the next element.
# We also need to make sure we haven't surpassed the low pointer, since that
# indicates we have already moved all the elements to their correct side of the pivot
while low <= high and array[high] >= pivot:
high = high - 1
# Opposite process of the one above
while low <= high and array[low] <= pivot:
low = low + 1
# We either found a value for both high and low that is out of order
# or low is higher than high, in which case we exit the loop
if low <= high:
array[low], array[high] = array[high], array[low]
# The loop continues
else:
# We exit out of the loop
break
array[start], array[high] = array[high], array[start]
return high
最后,让我们实现quick_sort()
功能:
def quick_sort(array, start, end):
if start >= end:
return
p = partition(array, start, end)
quick_sort(array, start, p-1)
quick_sort(array, p+1, end)
将它们都实现后,我们可以quick_sort()
在一个简单的数组上运行:
array = [29,99,27,41,66,28,44,78,87,19,31,76,58,88,83,97,12,21,44]
quick_sort(array, 0, len(array) - 1)
print(array)
输出:
[12, 19, 21, 27, 28, 29, 31, 41, 44, 44, 58, 66, 76, 78, 83, 87, 88, 97, 99]
由于该算法不稳定,因此无法保证这两个44彼此是按照此顺序排列的。也许原来是切换了-尽管这在整数数组中意义不大。
您可以通过几种方法重写此算法以对Python中的自定义对象进行排序。一个非常Python化的方式是实行比较运营商给定类,这意味着我们实际上并不需要改变,因为该算法的实现>
,==
,<=
等会我们的类对象上也工作。
另一个选择是允许调用者向我们的算法提供一种方法,然后该方法将用于执行对象的实际比较。以这种方式重写算法以用于自定义对象非常简单。但是请记住,该算法不稳定。
让我们从一个Person
类开始:
class Person:
def __init__(self, name, age):
self.name = name
self.age = age
def __str__(self):
return self.name
这是一个非常基本的类,只有两个属性,name
和age
。我们希望将其age
用作排序键,这将通过为排序算法提供自定义lambda函数来完成。
但首先,让我们看看如何在算法中使用此提供的函数。与其直接与<=
or >=
运算符不做直接比较,我们调用函数来告诉is哪个Person
年龄更大:
def partition(array, start, end, compare_func):
pivot = array[start]
low = start + 1
high = end
while True:
while low <= high and compare_func(array[high], pivot):
high = high - 1
while low <= high and not compare_func(array[low], pivot):
low = low + 1
if low <= high:
array[low], array[high] = array[high], array[low]
else:
break
array[start], array[high] = array[high], array[start]
return high
def quick_sort(array, start, end, compare_func):
if start >= end:
return
p = partition(array, start, end, compare_func)
quick_sort(array, start, p-1, compare_func)
quick_sort(array, p+1, end, compare_func)
现在,让我们对这些对象的集合进行排序。您可以看到对象比较是quick_sort
通过lambda 提供给调用的,该lambda可以对age
属性进行实际比较:
p1 = Person("Dave", 21)
p2 = Person("Jane", 58)
p3 = Person("Matthew", 43)
p4 = Person("Mike", 21)
p5 = Person("Tim", 10)
array = [p1,p2,p3,p4,p5]
quick_sort(array, 0, len(array) - 1, lambda x, y: x.age < y.age)
for person in array:
print(person)
输出为:
Tim
Dave
Mike
Matthew
Jane
通过以这种方式实现算法,只要我们提供适当的比较功能,它就可以与我们选择的任何自定义对象一起使用。
鉴于Quicksort可以对给定数组的“一半”进行独立排序,因此对并行化非常方便。我们可以有一个单独的线程来对数组的每个“一半”进行排序,并且理想情况下,我们可以将排序所需的时间减半。
但是,如果我们在选择数据透视表时特别不走运,那么Quicksort可能具有非常深的递归调用堆栈,并且并行化不如合并排序有效。
建议使用简单的非递归算法对小数组进行排序。即使是像插入排序这样的简单操作,在小型阵列上也比Quicksort更有效。因此,理想情况下,我们可以检查子数组是否只有少量元素(大多数建议说的是大约10个或更少),如果是,则可以使用Insertion Sort对其进行排序。
Quicksort的一个流行变体是Multi-pivot Quicksort,它使用n-1个枢轴将原始数组分解为n个较小的数组。但是,大多数时候只使用两个枢轴,而不是更多。
有趣的事实:Java 7的排序实现中使用了Dual-pivot Quicksort,以及用于较小数组的插入排序。
如前所述,Quicksort的效率在很大程度上取决于数据透视的选择-它可以“制造或破坏”算法的时间(和堆栈空间)复杂性。使用自定义对象时,算法的不Solidity也可能会破坏交易。
然而,尽管如此,Quicksort的平均时间复杂度O(n * log n)以及相对较低的空间使用量和简单的实现,使其成为一种非常有效且流行的算法。