快速排序(QuickSort)

来源:互联网 发布:sql 2005 sp4 x64 编辑:程序博客网 时间:2024/05/21 07:55

快速排序(QuickSort)

在维基百科中,这样说道:
Quicksort (sometimes called partition-exchange sort) is an efficient sorting algorithm, serving as a systematic method for placing the elements of an array in order. Developed by Tony Hoare in 1959,[1] with his work published in 1961,[2] it is still a commonly used algorithm for sorting. When implemented well, it can be about two or three times faster than its main competitors, merge sort and heapsort.[3]
快速排序(有时候被称为分区交换排序)是一种高效的排序算法,作为排序数组元素的系统的方法。由Tony Hoare在1959年开发,1961年公开发表。快速排序认识一个常用的排序算法。如果实施得当,它要比归并排序和堆排序快2~3倍。
Quicksort is a comparison sort, meaning that it can sort items of any type for which a “less-than” relation (formally, a total order) is defined. In efficient implementations it is not a stable sort, meaning that the relative order of equal sort items is not preserved. Quicksort can operate in-place on an array, requiring small additional amounts of memory to perform the sorting.
快速排序是一种比较排序,这意味着,只要可以用小于关系定义的任何事物,都可以使用快速排序(正式的说,是整体的次序)。在高效的实现中, 快速排序并不是稳定的排序,也就是说,如果两项相等,不能保证排序后的相对位置与排序前的相对位置相同。快速排序只需要很小的附加内存,就可以在数组上执行“就地”排序。
Mathematical analysis of quicksort shows that, on average, the algorithm takes O(n log n) comparisons to sort n items. In the worst case, it makes O(n2) comparisons, though this behavior is rare.
数学分析显示,如果需要对n项进行排序,快速排序平均需要O(nlogn)次比较。在最坏情况下,需要O(n2)次比较,不过最坏情况很少发生。
(2009年,Yaroslavskiy提出了新的双基准快速排序的实现。并被选入Oracle 的Java 7运行时库)

算法(Algorithm):

Quicksort is a divide and conquer algorithm. Quicksort first divides a large array into two smaller sub-arrays: the low elements and the high elements. Quicksort can then recursively sort the sub-arrays.
快速排序是一种分而治之的算法。快速排序首先将一个大的数组切分成两个较小的子数组:低元素和高元素。快速排序可以递归地排序这些子数组。
The steps are:

  1. Pick an element, called a pivot, from the array.
  2. Reorder the array so that all elements with values less than the pivot come before the pivot, while all elements with values greater than the pivot come after it (equal values can go either way). After this partitioning, the pivot is in its final position. This is called the partition operation.
  3. Recursively apply the above steps to the sub-array of elements with smaller values and separately to the sub-array of elements with greater values.
    The base case of the recursion is arrays of size zero or one, which never need to be sorted.

步骤如下:
1. 从数组中选择一个基准(pivot)元素。
2. 重新排序数组,使得小于基准的所有元素放在基准前,大于基准的所有元素放在基准后面(相等的元素,可以放在基准的前面,也可以放在基准的后面)。在这次划分(partition)之后,基准就被放到了它最终应该到的位置。
3. 对划分后的较小元素子数组和较大的子数组分别的递归的实施上面的步骤。
递归退出条件:当数组中有0个或一个元素时,它意味着,不再需要排序。

快速排序之所以比冒泡排序的效率高得多,是因为交换的元素相距很远,使得我们需要较少得到交换次数就可以把一个元素移动到正确位置。

算法示例

对于《数据结构与算法分析—-C++语言描述 Narry Nyhoff》一书中,给定例子:
75, 70 , 65 , 84 , 98 , 78 , 100 , 93 , 55 , 61 , 81 , 68

选取一个基准:选取最左边的一个元素75。
一个从列表右端出发,寻找小于或等于(≤)基准75的元素 —- 68;
另一个从列表左端出发,寻找大于(>)基准75的元素 —- 84。
75, 70 , 65 , 84 , 98 , 78 , 100 , 93 , 55 , 61 , 81 , 68
交换这两个元素:
75, 70 , 65 , 68 , 98 , 78 , 100 , 93 , 55 , 61 , 81 , 84

继续查找,从右端寻找小于或等于(≤)基准75的元素 —- 61;
从左端寻找大于(>)基准75的元素 —- 98
75, 70 , 65 , 68 , 98 , 78 , 100 , 93 , 55 , 61 , 81 , 84
交换这两个元素
75, 70 , 65 , 68 , 61, 78 , 100 , 93 , 55 , 98, 81 , 84

继续查找,从右端寻找小于或等于(≤)基准75的元素 —- 55;
从左端寻找大于(>)基准75的元素 —- 78
75, 70 , 65 , 68 , 61, 78 , 100 , 93 , 55 , 98, 81 , 84
交换这两个元素:
75, 70 , 65 , 68 , 61, 55, 100 , 93 , 78 , 98, 81 , 84

继续查找,从右端寻找小于或等于(≤)基准75的元素 —- 55;此时发现,从右向左 与 从左向右 相遇。因此,查找结束。此时,只需要将55与基准75交换
75, 70 , 65 , 68 , 61, 55, 100 , 93 , 78 , 98, 81 , 84
得到:
55, 70 , 65 , 68 , 61, 75, 100 , 93 , 78 , 98, 81 , 84
自此,基准75左侧的元素,均比75小;基准75右侧的元素,均比75大。于是,75就将一个完整的列表分割成左右两个自列表(≤75,>75)。这两个列表就是:

55, 70 , 65 , 68 , 61;
100 , 93 , 78 , 98, 81 , 84

然后就可以重复的使用上述方法,对这两个自列表进行排序。

霍尔分区方案(Hoare partition scheme)

The original partition scheme described by C.A.R. Hoare uses two indices that start at the ends of the array being partitioned, then move toward each other, until they detect an inversion: a pair of elements, one greater than the pivot, one smaller, that are in the wrong order relative to each other. The inverted elements are then swapped.[16] When the indices meet, the algorithm stops and returns the final index.
使用两个索引(一个在需要被分割的数组的开始,另一个在将要被分割的数组的末尾),将两个索引相向移动,直到检测到了错位(一个大于基准,另一个小于基准,他们的相对位置是被错放的)。此时,需要把这两个错位(颠倒)的元素互换位置。当索引相遇,算法停止并且返回最终的索引(相遇时,将基准处的值与该位置的值互换,分割结束)。该方法是C.A.R Hoare最开始的分割方法。
There are many variants of this algorithm, for example, selecting pivot from A[hi] instead of A[lo]. Hoare’s scheme is more efficient than Lomuto’s partition scheme because it does three times fewer swaps on average, and it creates efficient partitions even when all values are equal.[9][self-published source?] Like Lomuto’s partition scheme, Hoare partitioning also causes Quicksort to degrade to O(n2) when the input array is already sorted; it also doesn’t produce a stable sort. Note that in this scheme, the pivot’s final location is not necessarily at the index that was returned, and the next two segments that the main algorithm recurs on are [lo..p] and (p..hi] as opposed to [lo..p) and (p..hi] as in Lomuto’s scheme. In pseudocode,[14]
当然这份算法也有许多的变种,比如:选择最后一个元素A[hi]作为基准,而不是A[lo]。Hoare的方法比Lomuto的分割方法更加高效(原因是什么,以及Lomuto分割方案又是什么参见Wikipedia中Quicksort的Lomuto partition scheme部分)。就像Lomuto的分割方案,当输入的数组已经排好序,Hoar的分割方案会导致快速排序退化(degrade)为O(n2)。当然,也不会产生一个稳定的排序。
那么,快速排序适合什么样的数组呢?The QuickSort Algorithm中这样说道it is said that the more random the arrangement of the array, the faster the Quicksort Algorithm finishes.数组越随机,Quicksort算法完成的越快。

分治法:
Divide: Partition S[p..r] into two subarrays S[p..q-1] and S[q+1..r] such that each element of S[p..q-1] is less than or equal to S[q], which is, in turn, less than or equal to each element of S[q+1..r]. Compute the index q as part of this partitioning procedure
分:S[p…q-1] , S[q] , S[q+1…r];
大小关系:S[p…q-1] ≤ S[q] ≤ S[q+1…r]。

Conquer: Sort the two subarrays S[p…q-1] and S[q+1..r] by recursive calls to quicksort.
治:递归调用quicksort来排序子数组S[p…q-1]和S[q+1..r]。

Combine: Since the subarrays are sorted in place, no work is needed to combing them: the entire array S is now sorted.
合:因为子数组是就地排好序的,因此,不需要将这些子数组合并:整个数组S是排好的。

算法伪代码如下

QuickSort伪代码(S为整个数组,p为起始,r为终止;调用形式为:QUICKSORT(S, 1, length[A]) ):

QUICKSORT(S, p, r)  If p < r    then q <- PARTITION(S, p, r)        QUICKSORT(S, p, q-1)        QUICKSORT(S, q+1, r)

Partition分割伪代码:

PARTITION(S, p, r)  pivot <- S[p]  i <- p  j <- r  while i < j do    while pivot <  S[j]      do j--    while i < j &&  S[i] <= pivot      do i--    if i< j      swap S[i] <-> S[j]  swap pivot <-> S[j]  return j

{上面的伪代码不一定对,具体参见代码部分}

调用方式:

quicksort(A, 0, length(A)-1)

完整代码如下:

#include <iostream>using namespace std;template <typename T>int partition1(T x[] , int first, int last){    int pivot = x[first];    int left = first,    right = last;    while(left < right){        while(pivot<x[right]){    // x[right] <= left;            right --;        }        while(left<right  &&              (x[left] < pivot || x[left] == pivot )  )    // x[left] > pivot        {            left++;        }        if(left < right){            swap(x[left], x[right]);        }    }    // while循环退出的条件: left == right(但是要确保传入的first<= last)    x[first] = x[right];    // 交换两个值    x[right] = pivot;    return right;}// QuitSorttemplate <typename T>void quicksort(T arr[], int first, int last){    int position;    if(first < last){        position = partition1(arr , first, last);        quicksort(arr,first,position-1);        quicksort(arr, position+1, last);    }}int main(){    int arr[] = {75,70,65,68,61,55,100,93,78,98,81,84};   // 这个数组最好使用随机数rand产生    int arrLength = sizeof(arr) / sizeof(int);    cout<< "original array: ";    for(int i=0; i< arrLength; i++){        cout << arr[i] << "  ";    }    cout<<endl;    quicksort(arr,0, arrLength-1);    cout<< "sorted array: ";    for(int i=0; i< arrLength-1; i++){        cout << arr[i] << "  ";    }    cout<<endl;    return 0;}

程序输出:

original array:75  70  65  68  61  55  100  93  78  98  81  84sorted array:55  61  65  68  70  75  78  81  84  93  98

在QuickSort中说道:

Quicksort的运行时间依赖于partition例程 —- 平衡、非平衡。这取决于基准的选取。如果分割后的子数组是不平衡的,快速排序回合插入排序一样慢;如果是平衡的,该算法和归并算法一样快。因此,选择最好的基准pivot对算法有着重要的影响。(平衡可以理解为:分割后左右两个子序列大小是相同的。比如:11个元素,分割后,左边为5个,右边为5个,这就是分割平衡)

错误的方法:大多数选取pivot选取的是第一个元素,要知道,只有输入的元素都是随机的这种方法才可行。但是,如果输入是预先排好序的,或者是相反的顺序(reverse order),那么选取第一个元素作为基准将会是一个糟糕的决定 —- 分割不平衡。(所以出于这个原因,在有的教材或者网站上,你看到被选取的基准是数组的最后一个元素。相应的伪代码就为:

PARTITION(S, p, r)
x <- S[r]
i <- p-1
for j <- p to r-1
do if S[j] <= x
then i <- i+1
swap S[i] <-> S[j]
swap S[i+1] <-> S[r]
return i+1

),伪代码不一定正确,形式仅供参考。

保险的方式: 选择基准较为保险的方式是 随机的选择一个。因为在排序过程中,随机基准不可能总是为我们提供不好的划分。
三数取中的方式: 当分割产生两个大小基本相等的子问题时 —- 一个大小为 [n/2]、另一个大小为[n/2]-1 ,则是最好的分割情况。为了得到这样的分割,支点必须是整个输入的中位数(median of the entire input)。不幸的是,中位数很难计算,而且会消耗大量的时间,明显(considerably)降低算法的执行效率。一个折衷的办法是:随机地选取3个数,然后用他们的平均数作为基准。

注:中位数(又称中值,英语:Median)对于有限的数集,可以通过把所有观察值高低排序后找出正中间的一个作为中位数。如果观察值有偶数个,通常取最中间的两个数值的平均数作为中位数。

使用保险的方式—-随机选取基准,示例(Short Example of a Quicksort Routine (Pivots chosen “randomly”)):

Input: [13 81 92 65 43 31 57 26 75 0]Pivot: 65Partition:  [13 0 26 43 31 57]  65  [ 92 75 81]Pivot: 31  81  Partition: [13 0 26]  31  [43 57]  65  [75]  81  [92]Pivot: 13Partition: [0]  13  [26]  31  [43 57]  65  [75]  81  [92]Combine: [0 13 26]  31 [43 57]  65  [75 81 92]Combine: [0 13 26 31 43 57]  65  [75 81 92]Combine: [0 13 26 31 43 57 65 75 81 92]

Summary
Quicksort is a relatively simple sorting algorithm using the divide-and-conquer recursive procedure. It is the quickest comparison-based sorting algorithm in practice with an average running time of O(n log(n)). Crucial to quicksort’s speed is a balanced partition decided by a well chosen pivot. Quicksort has the advantage of sorting in place, and it works well even in virtual memory environments.

总结:

快速排序是一个简单的排序算法。它使用了分而治之的递归过程算法。在实践中,它是基于比较的最快的排序算法。平均运行时间为$O(n log n)。
最快速排序速度影响较大的是:一个好的pivot对分割是否平衡有着决定性的因素。快速排序有着就地排序的优势(就地排序:sorting in place,只要很少的辅助空间就可以交换元素)。在虚拟内存环境中,运行的很好。
更多内容参见:普林斯顿大学算法 第四版 快速排序

0 0
原创粉丝点击