本节介绍了决策树模型。比较排序可以被抽象为一棵决策树。该模型证明了:对于包含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),它不是一个比较排序算法。
classCountSort:defsort(self, A, k):
C =[0]*(k +1)for j inrange(len(A)):# C[i] contains the number of elements equal to i
C[A[j]]= C[A[j]]+1for i inrange(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 inrange(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]]-=1for i inrange(len(A)):
A[i]= B[i]deftest():
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)。
classRadixSort:defsort(self, A, d):
temp = A.copy()for i inrange(d):
temp =[t //10for 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 %10for num in B]
C =[0]*(k +1)for j inrange(len(keys)):# C[i] contains the number of elements equal to i
C[keys[j]]= C[keys[j]]+1for i inrange(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 inrange(len(A)-1,-1,-1):# stable sort
tempA[C[keys[j]]-1]= A[j]
tempB[C[keys[j]]-1]= B[j]
C[keys[j]]-=1for i inrange(len(A)):
A[i]= tempA[i]
B[i]= tempB[i]deftest():
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)。即使输入数据不服从均匀分布,桶排序也仍然可以在线性时间内完成。只要输入数据满足下列性质:所有桶的大小的平方和与总的元素个数成线性关系。
classBucketSort:defsort(self, A):import math
n =len(A)
B =[]for i inrange(n):
B.append([])for i inrange(n):
B[math.floor(n*A[i])].append(A[i])for i inrange(n):
B[i].sort()# 可以使用插入排序
i =0for j inrange(n):for num in B[j]:
A[i], i = num, i+1deftest():
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()