快速排序

题目

快速排序

快速排序(英语:Quicksort),又称划分交换排序(partition-exchange sort),简称快排,一种排序算法,最早由东尼·霍尔提出。在平均状况下,排序{\displaystyle n}n个项目要{\displaystyle \ O(n\log n)}大O符号)次比较。在最坏状况下则需要次比较,但这种状况并不常见。事实上,快速排序通常明显比其他算法更快,因为它的内部循环(inner loop)可以在大部分的架构上很有效率地达成。

快速排序使用分治法(Divide and conquer)策略来把一个序列(list)分为较小和较大的2个子序列,然后递归地排序两个子序列。

步骤为:

  1. 挑选基准值:从数列中挑出一个元素,称为“基准”(pivot),
  2. 分割:重新排序数列,所有比基准值小的元素摆放在基准前面,所有比基准值大的元素摆在基准后面(与基准值相等的数可以到任何一边)。在这个分割结束之后,对基准值的排序就已经完成,
  3. 递归排序子序列:递归地将小于基准值元素的子序列和大于基准值元素的子序列排序。

递归到最底部的判断条件是数列的大小是零或一,此时该数列显然已经有序。

快速排序图示

选取基准值有数种具体方法,此选取方法对排序的时间性能有决定性影响。

在简单的伪代码中,此算法可以被表示为:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
function quicksort(q)
{
var list less, pivotList, greater
if length(q) ≤ 1
return q
else
{
select a pivot value pivot from q
for each x in q except the pivot element
{
if x < pivot then add x to less
if x ≥ pivot then add x to greater
}
add pivot to pivotList
return concatenate(quicksort(less), pivotList, quicksort(greater))
}
}

原地分割版本

上面简单版本的缺点是,它需要的额外存储空间,也就跟归并排序一样不好。额外需要的存储器空间配置,在实际上的实现,也会极度影响速度和缓存的性能。有一个比较复杂使用原地(in-place)分割算法的版本,且在好的基准选择上,平均可以达到{\displaystyle O(\log n)}空间的使用复杂度。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
function partition(a, left, right, pivotIndex)
{
pivotValue = a[pivotIndex]
swap(a[pivotIndex], a[right]) // 把pivot移到結尾
storeIndex = left
for i from left to right-1
{
if a[i] <= pivotValue
{
swap(a[storeIndex], a[i])
storeIndex = storeIndex + 1
}
}
swap(a[right], a[storeIndex]) // 把pivot移到它最後的地方
return storeIndex
}

这是原地分割算法,它分割了标示为”左边(left)”和”右边(right)”的序列部分,借由移动小于a[pivotIndex]的所有元素到子序列的开头,留下所有大于或等于的元素接在他们后面。在这个过程它也为基准元素找寻最后摆放的位置,也就是它回传的值。它暂时地把基准元素移到子序列的结尾,而不会被前述方式影响到。由于算法只使用交换,因此最后的数列与原先的数列拥有一样的元素。要注意的是,一个元素在到达它的最后位置前,可能会被交换很多次。

一旦我们有了这个分割算法,要写快速排列本身就很容易:

1
2
3
4
5
6
procedure quicksort(a, left, right)
if right > left
select a pivot value a[pivotIndex]
pivotNewIndex := partition(a, left, right, pivotIndex)
quicksort(a, left, pivotNewIndex-1)
quicksort(a, pivotNewIndex+1, right)

举例

下面以一个实际的例子说明。

以 47、29、71、99、78、19、24、47 的待排序的数列为例进行排序,为了方便区分两个 47,我们对后面的 47 增加一个下画线,即待排序的数列为 47、29、71、99、78、19、24、47

首先我们需要在数列中选择一个基准数,我们一般会选择中间的一个数或者头尾的数,这里直接选择第 1 个数 47 作为基准数,接着把比 47 小的数字移动到左边,把比 47 大的数字移动到右边,对于相等的数字不做移动。所以实际上我们需要找到中间的某个位置 k,这样 k 左边的值全部比 k 上的值小,k 右边的值全部比 k 上的值大。

接下来开始移动元素。怎么移动呢?其实冒泡排序也涉及对元素的移动,但是那样移动起来很累,比如把最后一个元素移动到第 1 个,就需要比较 n-1 次,同时交换 n-1 次,效率很低。其实,只需把第 1 个元素和最后一个元素交换就好了,这种思想是不是在排序时可以借鉴呢?之前说快速排序就是对冒泡排序的一个改进,就是这个原因。

快速排序的操作是这样的:首先从数列的右边开始往左边找,我们设这个下标为 i,也就是进行减减操作(i–),找到第 1 个比基准数小的值,让它与基准值交换;接着从左边开始往右边找,设这个下标为 j,然后执行加加操作(j++),找到第 1 个比基准数大的值,让它与基准值交换;然后继续寻找,直到 i 与 j 相遇时结束,最后基准值所在的位置即 k 的位置,也就是说 k 左边的值均比 k 上的值小,而 k 右边的值都比 k 上的值大。

所以对于上面的数列 47、29、71、99、78、19、24、47,进行第 1 趟第 1 个交换的排序情况如下,第 1 次的操作情况如下图所示。

第一次交换后

交换之后,j 移动到了下标为 6 的位置,对 i 继续扫描:

第一次交换后

此时交换后的数列变为 24、29、47、99、78、19、71、47。接下来我们继续对 i、j 进行操作,如图 3 所示,继续进行 i– 及 j++ 的比较操作:

第一次交换后

进行了这两次 i、j 的移动、比较、交换之后,我们最终得到的数列是 24、29、19、47、78、99、71、47。接下来我们继续进行 i– 的操作,发现在 i 为 4 时比 47 大不用交换,在 i 为 3 时与 j 相遇,这时就不需要继续移动、比较了,已经找到 k 了,并且 k 的值为 3。我们可以确认一下当前的数列是不是 k 左边的值都比 47 小,而 k 右边的值都比 47 大(由于要保持相对位置不变,所以 47 同样在基准值 47 的右边)。

47 这个值已经落到了它该在的位置,第 1 趟排序完成了。接下来就是以 k 为基准,分为两部分,然后在左右两部分分别执行上述排序操作,最后数据会分为 4 部分;接着对每部分进行操作,直到每部分都只有一个值为止。

接下来进行第 2 趟排序,现在左边部分为 24、29、19,我们选择第 1 个数 24 作为基准数,接着进行 i–、j++ 的操作,我们发现 i 最初的值为 19,比 24 这个基准值小,所以与基准值进行交换,得到的数列为 19、29、24;当 j 为 1 时,我们发现 29 比 24 大,所以与基准值进行交换,得到的数列 19、24、29,此时 i 为 2,j 为 1;继续 i– 时发现 i 为 1,与 j 相遇,左边部分的数列的 k 为 1,并且左右两部分分别只有一个元素,此时第 2 轮排序的左边部分的排序结束,同时左边部分的所有数据都排序完成。

我们接着看右边部分的排序,待排序的数列为 78、99、71、47,我们同样选择第 1 个值 78 为基准值,接下来进行 i 与 j 的移动与比较,发现 47 比 78 小,进行交换,得到的数列 47、99、71、78;从左往右发现 99 比基准值 78 大,进行交换,得到的数列为 47、78、71、99;继续从右向左看,发现 71 比基准值 78 小,进行交换,得到的数列为 47、71、78、99。此时 i 在整体数组中的下标为 6,j 为 5,若继续 j++ 则与 i 相遇,所以完成此轮排序。

此时右边数列的 k 为 6,一般会是相遇的位置,也就是基准值所在的位置,这时数列又被分为两部分,左边是 47、71,右边是 99,需要继续对左边部分的数据进行排序,虽然只有两个数据,但我们还是继续按照快速排序的思想操作一下,选择 47 作为基准数,将i进行从右向左的移动、比较,发现 i 与 j 相等时没有产生移动,完成第 2 轮排序。

至此,所有排序都已经完成,最终数列的结果是 19、24、29、47、47、71、78、99,怎么样,快速排序是不是非常简单地完成了所有的排序呢?虽然本次快速排序没有改变相同值的元素的顺序,但是由于快速排序需要对数列中的元素来回移动,有时还是会改变相对顺序的(比如 47 在第 1 轮的移动过程中就被移动到 47 的右边了),所以快速排序并不是一个稳定的算法。

快速排序实现

实现一:递归

java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
public int[] sortArray(int[] nums) {
quickSort(nums, 0, nums.length - 1);
return nums;
}

private int partition(int[] nums, int left, int right) {
int pivot = nums[left];
while (left < right) {
while (left < right && nums[right] >= pivot) {
right--;
}
nums[left] = nums[right];

while (left < right && nums[left] <= pivot) {
left++;
}
nums[right] = nums[left];
}
nums[left] = pivot;
return left;
}

private void quickSort(int[] nums, int left, int right) {
if (left < right) {
int index = partition(nums, left, right);
quickSort(nums, left, index - 1);
quickSort(nums, index + 1, right);
}
}

参考资料

维基百科-快速排序

0%