目录
第8章 线性时间排序
8.1 排序算法的下界
- 归并排序、堆排序和快速排序能在 O ( n l g n ) O(nlgn) O(nlgn)时间内排序 n n n个数。归并排序和堆排序在最坏情况下就能够达到该时间,快速排序在平均情况达到该时间(快速排序最坏情况下是 O ( n 2 ) O(n^2) O(n2))。
- 如果在排序中各元素的次序依赖于它们之间的比较,就把该类排序算法称为比较排序。上述三种排序算法和冒泡排序、插入排序、选择排序等都是比较排序。
- 本节介绍了决策树模型。比较排序可以被抽象为一棵决策树。该模型证明了:对于包含n个元素的输入序列来说,在最坏情况下,任何比较排序都需要做 Ω ( n l g n ) \Omega(nlgn) Ω(nlgn)次比较。因此,归并排序和堆排序是渐近最优的比较排序算法。
- 8.2节、8.3节和8.4节介绍了三种线性时间复杂度的排序算法:计数排序、基数排序和桶排序。这些算法是用运算而不是比较来确定排序顺序的。因此,下界 Ω ( n l g n ) \Omega(nlgn) Ω(nlgn)对它们是不适用的。
8.2 计数排序
- 计数排序(counting sort)假设 n n n个元素中的每一个都是在 0 0 0到 k k k区间内的一个正数,其中 k k k为某个整数。当 k = O ( n ) k=O(n) k=O(n)时,排序的运行时间为 Θ ( n ) \Theta(n) Θ(n)。
- 计数排序的基本思想是:对每一个输入元素 x x x,确定小于 x x x的元素个数。利用这一信息,就可以直接把 x x x放到它在输出数组中的位置上了。当有几个元素相同时,这一方案就要略作修改,因为不能把它们放在同一个输出位置上。
- 在计数排序算法的代码中,假设输入是一个数组 A [ 1… n ] A[1…n] A[1...n], A . l e n g t h = n A.length=n A.length=n。另外还需要两个数组: B [ 1… n ] B[1…n] B[1...n]存放排序的输出, C [ 0… k ] C[0…k] C[0...k]提供临时存储空间。
- 计数排序的时间代价为 Θ ( k ) + Θ ( n ) = Θ ( k + n ) \Theta(k)+\Theta(n)=\Theta(k+n) Θ(k)+Θ(n)=Θ(k+n)。当 k = O ( n ) k=O(n) k=O(n)时,我们一般会采用计数排序,此时的运行时间为 Θ ( n ) \Theta(n) Θ(n)。计数排序的下界优于8.1节中证明的 O ( n l g n ) O(nlgn) O(nlgn),它不是一个比较排序算法。
- 如下的代码能保证计数排序是稳定的,因为第12行表示从数组最后一个元素逐一向前开始排序。如果换成第13行中注释的语句,表示从数组第一个元素逐一向后排序,则计数排序是不稳定的。
- 保证计数排序的稳定性的另一个原因是:计数排序经常会被用作8.2节中基数排序算法的一个子过程。为了使基数排序正确运行,计数排序必须是稳定的。
class CountSort:
def sort(self, A, k):
C = [0] * (k + 1)
for j in range(len(A)):
# C[i] contains the number of elements equal to i
C[A[j]] = C[A[j]] + 1
for i in range(1, k+1):
# C[i] contains the number of elements less than or equal to i
C[i] = C[i] + C[i-1]
B = [0] * len(A)
for j in range(len(A)-1, -1, -1): # stable sort
# for j in range(len(A)): # unstable sort
B[C[A[j]]-1] = A[j]
C[A[j]] -= 1
for i in range(len(A)):
A[i] = B[i]
def test():
cs = CountSort()
A = [2, 5, 3, 0, 2, 3, 0, 3] # P109例子
cs.sort(A, 5)
print(A)
if __name__ == '__main__':
test()
8.3 基数排序
- 基数排序(radix sort)用于对 n n n张卡片上的 d d d位数进行排序。与人们直观感受相悖的是,基数排序是先按最低有效位进行排序来解决卡片排序问题的。然后将所有卡片合成一叠,用同样的方法按次低有效位对所有卡片进行排序,并把排好的卡片再次合成一叠。重复这一过程 d d d轮( d d d位数字)后,卡片可以排好序。
- 为了保证基数排序的稳定性,选择计数排序作为卡片的排序方法。
- 基数排序的代码是非常直观的,使用一个稳定排序方法从最低位开始到最高位进行排序。假设输入是一个数组 A [ 1… n ] A[1…n] A[1...n],其中每一个元素都是一个 d d d位数。第1位表示最低位,第 d d d位表示最高位。
- 稍微修改了计数排序,数组 B B B表示数组 A A A中各元素的每一位,简化了计算每一轮排序后数组 A A A的有效位。
- 给定 n n n个 d d d位数,其中每一个数位有 k k k个不同的取值。如果基数排序使用的稳定排序方法(计数排序)耗时 Θ ( n + k ) \Theta(n+k) Θ(n+k),那么它就可以在 Θ ( d ( n + k ) ) \Theta(d(n+k)) Θ(d(n+k))时间内将这些数排好序。当 d d d为常数且 k = Θ ( n ) k=\Theta(n) k=Θ(n)时,基数排序具有线性的时间代价 O ( n ) O(n) O(n)。
class RadixSort:
def sort(self, A, d):
temp = A.copy()
for i in range(d):
temp = [t // 10 for t in temp]
self.__count_sort(A, 9, temp)
def __count_sort(self, A, k, B):
''' modified count_sort A and B have equal length. And B is used to compute every column of A. '''
keys = [num % 10 for num in B]
C = [0] * (k + 1)
for j in range(len(keys)):
# C[i] contains the number of elements equal to i
C[keys[j]] = C[keys[j]] + 1
for i in range(1, k+1):
# C[i] contains the number of elements less than or equal to i
C[i] = C[i] + C[i-1]
# sort A and B
tempA = [0] * len(A)
tempB = [0] * len(B)
for j in range(len(A)-1, -1, -1): # stable sort
tempA[C[keys[j]]-1] = A[j]
tempB[C[keys[j]]-1] = B[j]
C[keys[j]] -= 1
for i in range(len(A)):
A[i] = tempA[i]
B[i] = tempB[i]
def test():
rs = RadixSort()
A = [329, 457, 657, 839, 436, 720, 355] # P110例子
rs.sort(A, 3)
print(A)
if __name__ == '__main__':
test()
8.4 桶排序
- 桶排序(bucket sort)假设输入数据服从均匀分布,平均情况下它的时间代价为 O ( n ) O(n) O(n)。与计数排序类似,因为对输入数据作了某种假设,桶排序的速度也很快。具体来说,计数排序假设输入数据都属于一个小区间内的整数,而桶排序则假设输入数据由一个随机过程产生,该过程将元素均匀、独立地分布在 [ 0 , 1 ) [0,1) [0,1)区间内。
- 桶排序将 [ 0 , 1 ) [0,1) [0,1)区间划分为 n n n个相同大选哦的子区间,或称为桶。然后将 n n n个数字分别放到各个桶中。因为输入数据均匀分布,所以一般不会出现很多数落在同一个桶中的情况。为了得到输出结果,我们先对每个桶中的数进行排序,然后遍历每个桶,按照次序把各个桶中的元素列出来即可。
- 在桶排序的代码中,我们假设输入是一个包含 n n n个元素的数组 A A A,每个元素位于 [ 0 , 1 ) [0,1) [0,1)区间。此外,算法中需要一个临时数组 B [ 0… n − 1 ] B[0…n-1] B[0...n−1]来存放桶。桶可以用数组或链表来实现。
- 桶排序的期望运行时间是 Θ ( n ) \Theta(n) Θ(n)。即使输入数据不服从均匀分布,桶排序也仍然可以在线性时间内完成。只要输入数据满足下列性质:所有桶的大小的平方和与总的元素个数成线性关系。
class BucketSort:
def sort(self, A):
import math
n = len(A)
B = []
for i in range(n):
B.append([])
for i in range(n):
B[math.floor(n*A[i])].append(A[i])
for i in range(n):
B[i].sort() # 可以使用插入排序
i = 0
for j in range(n):
for num in B[j]:
A[i], i = num, i+1
def test():
bs = BucketSort()
A = [0.78, 0.17, 0.39, 0.26, 0.72, 0.94, 0.21, 0.12, 0.23, 0.68] # P112例子
bs.sort(A)
print(A)
if __name__ == '__main__':
test()