归并排序和快速排序都使用了“分治”策略(divide-and-conquer)。对于数组A[p..r],归并排序先把数组从中间分开,形成两个具有(p+r)/2个元素的子数组(divide);然后,分别对这两个子数组递归地进行归并排序(conquer),当子数组只包含一个元素时到达递归出口;最后,将两个排好序的子数组合并起来,形成有序数组(combine)。
归并排序算法如下:
MERGE-SORT(A, p, r)
if p < r
q = (p + r) / 2
MERGE-SORT(A, p, q)
MERGE-SORT(A, q + 1, r)
MERGE(A, p, q, r)
MERGE(A, p, q, r)
n1 = q - p + 1
n2 = r - q
let L[1..n1 + 1] and R[1..n2 + 1] be new arrays
for i = 1 to n1
L[i] = A[p + i - 1]
for j = 1 to n2
R[j] = A[q + j]
L[n1 + 1] = inf
R[n2 + 1] = inf
i = 1
j = 1
for k = p to r
if L[i] <= R[j]
A[k] = L[i]
i = i + 1
else
A[k] = R[j]
j = j + 1
一些问题可以在归并排序的基础上得到解决,典型的有“计算数组的逆序数”等。
给定包含n个不同元素的数组A[1..n],如果存在i < j且A[i] > A[j],则(i, j)称为数组A的一个逆序。数组包含所有逆序的数量成为数组的逆序数。例如,对于数组[2, 3, 8, 6, 1],它的逆序有(2, 1), (3, 1), (8, 6), (8, 1), (6, 1),所以该数组的逆序数为5。
如果对数组进行插入排序,会发现每次“插入”都能消除若干逆序。插入排序算法如下:
INSERTION-SORT(A)
for j = 2 to A.length
key = A[j]
i = j - 1
while i > 0 and A[i] > key
A[i + 1] = A[i]
i = i - 1
A[i + 1] = key
可以看出,消除逆序的操作发生在while循环中:每执行一次循环体都会消除一个逆序。当所有逆序都被消除后,就完成了插入排序。所以,可以通过在插入排序中加入计数器的方法计算数组逆序数。这样做的时间复杂度为 O(n2) 。
既然排序的过程就是消除逆序的过程,我们可以考虑修改归并排序来计算数组逆序数:首先,将数组A[p..r]分成两个分别具有(p+r)/2个元素的子数组;然后,分别计算两个子数组中的逆序数。需要注意的是,简单相加两个子数组的逆序数并不能得出原数组的逆序数,因为原数组还有一部分逆序存在于合并两个子数组的过程中,所以原数组的逆序数是两个子数组逆序数之和加上合并过程中消除的逆序数。
对于已经从小到大排好序的子数组A[p..q]和A[q+1..r],令 n1 和 n2 分别表示两个子数组的长度。对于 i∈[p,q],j∈[q+1,r] ,如果 A[j]<A[i] ,则对于所有 k∈[i+1,q] ,均有 A[j]<A[k] ,于是逆序数需增加 n1−i+1 。根据这一思路,计算数组逆序数的算法如下:
COUNT-INVERSIONS(A, p, r)
if p < r
q = (p + r) / 2
return COUNT-INVERSIONS(A, p, q)
+ COUNT-INVERSIONS(A, q + 1, r)
+ MERGE-INVERSIONS(A, p, q, r)
MERGE-INVERSIONS(A, p, q, r)
n1 = q - p + 1
n2 = r - q
let L[1..n1 + 1] and R[1..n2 + 1] be new arrays
for i = 1 to n1
L[i] = A[p + i - 1]
for j = 1 to n2
R[j] = A[q + j]
L[n1 + 1] = inf
R[n2 + 1] = inf
i = 1
j = 1
count = 0
for k = p to r
if L[i] <= R[j]
A[k] = L[i]
i = i + 1
else
A[k] = R[j]
j = j + 1
count = count + n1 - p + 1
return count
与归并排序类似,快速排序的第一步也是把数组A[p..r]划分成两个子数组。快速排序希望其中一个子数组中的所有元素都比另一个子数组中的元素小,于是采用了如下划分(partition)方式:先从数组A[p..r]中选出一个元素作为“轴”(pivot),所有大于轴的元素作为一个子数组,把它们放到A[p..q-1]位置上,所有不大于轴的元素作为另一个子数组,把它们放到A[q+1..r]位置上,最后把轴元素放到A[q]位置上,这样就完成了一次划分,可以看出,轴元素已经被固定在了A[q]位置上,在后续的排序过程中,位置不再改变;然后,分别对A[p..q-1]和A[q+1..r]递归地进行快速排序。由于A[p..q-1]中的所有元素均小于A[q+1..r]中的元素,所以这两个子数组分别排好序后,排序过程就结束了,不必像归并排序那样再做一次合并操作。
快速排序算法如下:
QUICKSORT(A, p, r)
if p < r
q = PARTITION(A, p, r)
QUICKSORT(A, p, q - 1)
QUICKSORT(A, q + 1, r)
PARTITION(A, p, r)
x = A[r]
i = p - 1
for j = p to r
if A[j] <= x
i = i + 1
exchange(A[i], A[j])
exchange(A[i + 1], A[r])
return i + 1
众所周知,快速排序的平均时间复杂度为 O(nlogn) ,最坏时间复杂度为 O(n2) 。最坏情况发生在每次划分(partition)都极度不平衡的时候,即对于一个n元素数组,每次划分都将数组分为长度分别为1和n-1的两个子数组,这时,需要进行 O(n) 次递归调用,导致了 O(n2) 的时间复杂度。
为了减少最坏情况的发生,可以在每次排序前随机选择一个元素作为轴,然后将这个元素与A[r]交换,这样,数组中每个元素被选为轴的概率相等。于是,就有了随机版的快速排序算法:
RANDOMIZED-QUICKSORT(A, p, r)
if p < r
q = RANDOMIZED-PARTITION(A, p, r)
RANDOMIZED-QUICKSORT(A, p, q - 1)
RANDOMIZED-QUICKSORT(A, q + 1, r)
RANDOMIZED-PARTITION(A, p, r)
i = RANDOM(p, r)
exchange(A[i], A[r])
return PARTITION(A, p, r)
一些问题可以在快速排序的基础上得到解决,典型的有“查找第k小的元素”等。
给定一个数组,可以利用分治法找到数组中第i小的元素。具体思路是:将数组划分成两个子数组,使其中一个子数组中的所有元素均小于另一个子数组中的元素(divide);然后,判断第k小的元素位于哪个子数组中,并在那个子数组中继续进行划分和判断,直到找到第k小的元素(conquer)。显然,划分过程可以利用快速排序的RANDOMIZED-PARTITION方法。与快速排序不同的是,每完成一次划分,只需在包含第i小元素的子数组中进行后续操作,不再对另一个子数组进行操作。这样,平均算法复杂度为 O(n) 。查找第i小元素的算法如下:
RANDOMIZED-SELECT(A, p, r, i)
if p == r
return A[p]
q = RANDOMIZED-PARTITION(A, p, r)
k = q - p + 1
if k == i
return A[q]
elseif i < k
return RANDOMIZED-SELECT(A, p, q - 1, i)
else
return RANDOMIZED-SELECT(A, q + 1, r, i - k)
这个问题的一个变体是“查找数组中最小的i个元素”。实际上,只要通过反复调用RANDOMIZED-PARTITION方法找到第i小元素,该元素左侧的子数组就是数组中最小的i个元素。
这个问题的另一个变体是“查找数组中出现次数超过一半的元素”。显然,如果一个元素在数组中出现的次数超过一半,那么,只要将数组排序,这个元素必然出现在排序后的数组的中间位置。所以,该问题等价于“查找第n/2小的元素”,其中n是数组元素个数。