线性时间排序
Counting sort(计数排序)
计数排序是一个非基于比较的排序算法,该算法于1954年由 Harold H. Seward 提出。它的优势在于在对一定范围内的整数排序时,它的复杂度为 O ( n + k ) Ο(n+k) O(n+k)(其中 k k k是整数的范围),快于任何比较排序算法。当然这是一种牺牲空间换取时间的做法,而且当 O ( k ) > O ( n ⋅ l o g ( n ) ) O(k)>O(n·log(n)) O(k)>O(n⋅log(n))的时候其效率反而不如基于比较的排序(基于比较的排序的时间复杂度在理论上的下限是O(n*log(n)), 如归并排序,堆排序)1,故适用于排序规模相对较小时( k < n ⋅ l g n k<n·lgn k<n⋅lgn)使用。
1.算法步骤
由于伪代码难以让人理解,我们以一个例子开始
1、我们有一个待排序数组
A
A
A,并在A下给予相应的编号(从1开始):
A
=
[
4
1
,
1
2
,
3
3
,
4
4
,
3
5
]
A=[4_1,1_2,3_3,4_4,3_5]
A=[41,12,33,44,35]
我们根据A的取值范围定义一个数组
C
C
C(长度为
m
a
x
(
C
)
max(C)
max(C)-
m
i
n
(
C
)
min(C)
min(C))
其中下标为对应
A
A
A的值的位置
C
=
[
0
1
,
0
2
,
0
3
,
0
4
]
C =[0_1,0_2,0_3,0_4]
C=[01,02,03,04]
2、我们根据编号从一开始,将
A
A
A遍历一遍,同时在
C
C
C中将相应的值每次加一。
换一种思路:
C
C
C对应的值为在
A
A
A中遍历到的位置的值
−
m
i
n
(
C
)
-min(C)
−min(C)即
C
[
A
[
当
前
位
置
]
−
1
]
C[A[当前位置]-1]
C[A[当前位置]−1]
则有:
第一个位置的数为
4
4
4,我们将
C
C
C中的
4
4
4位置对应的数
+
1
+1
+1,则变更以后的
C
C
C为:
C
=
[
0
1
,
0
2
,
0
3
,
1
4
]
C =[0_1,0_2,0_3,1_4]
C=[01,02,03,14]
同理,第二个数为
1
1
1,则我们将
2
2
2对应的数数
+
1
+1
+1,则变更以后的
C
C
C为:
C
=
[
1
1
,
0
2
,
0
3
,
1
4
]
C =[1_1,0_2,0_3,1_4]
C=[11,02,03,14]
最终我们得到的
C
C
C为:
C
=
[
1
,
0
,
2
,
2
]
C =[1,0,2,2]
C=[1,0,2,2]
3、我们对
C
C
C进行处理,将将
C
C
C从第一个位置开始,每一个位置的值为原来此位置的值加上前一个位置的值,我们将之叫为
C
′
C'
C′
即:
C
[
1
]
=
C
[
1
]
+
C
[
0
]
=
1
C[1] = C[1]+C[0] = 1
C[1]=C[1]+C[0]=1
C
[
2
]
=
C
[
2
]
+
C
[
1
]
=
3
C[2] = C[2]+C[1] = 3
C[2]=C[2]+C[1]=3
C
[
3
]
=
C
[
3
]
+
C
[
2
]
=
5
C[3] = C[3]+C[2] = 5
C[3]=C[3]+C[2]=5
C
′
=
[
1
,
1
,
3
,
5
]
C' = [1,1,3,5]
C′=[1,1,3,5] 为了节省空间我们可以直接在
C
C
C上操作,但为了易于辨别,我们将操作后的数组命名为
C
′
C'
C′。
4、我们新建一个数组,这个数组的长度与带排序的数组相同:
A
′
=
[
0
,
0
,
0
,
0
,
0
]
A'=[0,0,0,0,0]
A′=[0,0,0,0,0]A'中的值可以为空,仅需要长度与A相同即可
我们从A开始进行一次遍历,将每一个数字直接放在其应该在的位置上。从
A
A
A中的第一个数字开始,值为
4
4
4,在
C
′
C'
C′中,我们找到第
4
4
4列,即
4
4
4所对应
C
′
C'
C′中的列(我们是按照待排数值的范围确定
C
C
C的长度的,若以下标来讲,我们
C
′
C'
C′所对应的值为
C
′
[
A
[
当
前
位
置
]
−
m
i
n
(
C
)
]
C'[A[当前位置]-min(C)]
C′[A[当前位置]−min(C)]).
我们取到
C
′
[
4
]
=
5
C'[4] = 5
C′[4]=5
再将
A
′
A'
A′中的第
5
5
5列的值赋值为
4
4
4(待排数的值),将
C
′
C'
C′第
4
4
4列的值
−
1
-1
−1
新的数组为:
C
′
=
[
1
,
1
,
3
,
4
]
C' = [1,1,3,4]
C′=[1,1,3,4]
A
′
=
[
0
,
0
,
0
,
0
,
4
]
A'=[0,0,0,0,4]
A′=[0,0,0,0,4]
同理,
A
A
A中的第二个数为
1
1
1,在
C
′
C'
C′中第一列,值为
1
1
1,则我们将
A
′
A'
A′中的第
1
1
1个位置赋值为
1
1
1,将
C
′
C'
C′第
1
1
1列的值
−
1
-1
−1,则第
2
2
2个赋值之后新的数组为:
C
′
=
[
0
,
1
,
3
,
4
]
C' = [0,1,3,4]
C′=[0,1,3,4]
A
′
=
[
1
,
0
,
0
,
0
,
4
]
A'=[1,0,0,0,4]
A′=[1,0,0,0,4]
我们省去过程,将每一次的迭代结果给出:
第三次:
C
′
=
[
0
,
1
,
2
,
4
]
C' = [0,1,2,4]
C′=[0,1,2,4]
A
′
=
[
1
,
0
,
3
,
0
,
4
]
A'=[1,0,3,0,4]
A′=[1,0,3,0,4]
第四次:
C
′
=
[
0
,
1
,
2
,
3
]
C' = [0,1,2,3]
C′=[0,1,2,3]
A
′
=
[
1
,
0
,
3
,
4
,
4
]
A'=[1,0,3,4,4]
A′=[1,0,3,4,4]
第五次:
C
′
=
[
0
,
1
,
1
,
3
]
C' = [0,1,1,3]
C′=[0,1,1,3]
A
′
=
[
1
,
3
,
3
,
4
,
4
]
A'=[1,3,3,4,4]
A′=[1,3,3,4,4]
经过这一次的遍历,我们可直接得到
A
′
A'
A′这个排序完毕的数组。
多么美妙的排序过程啊!
然而太大的Cache占用过多的Cache访问会导致时间相较于Quicksort
还要长,但在开头的分析之下,排序速度依然是Counting sort
占优。所以更好的办法是我们将Quicksort
与Counting sort
结合使用,相关的内容及结合后的时间复杂度分析详见另一篇Blog。
Radix sort (基数排序)
1、算法步骤
作为一个古老的排序算法(大约出现在1890),我们依然已一个例子来作演示。
1、设我们的待排数组为
A
=
[
329
,
457
,
657
,
839
,
436
,
720
,
355
]
A=[329,457,657,839,436,720,355]
A=[329,457,657,839,436,720,355]
由于算法是从最后一位开始排序的,我们将他们竖着写
329
329
329
457
457
457
657
657
657
839
839
839
436
436
436
720
720
720
355
355
355
我们从待排数组的最后一位开始,从
0
−
9
0-9
0−9即由小到大排列
2、由于第一小的是
0
0
0则我们将
720
720
720放在第一个,第二小的为
5
5
5我们将
355
355
355放在
720
720
720后面,值得注意的是我们需要考虑排序的稳定性,即需要自上而下开始查看。
第二位与第一位暂时不需要考虑,则我们得到的数列为
720
720
720
355
355
355
436
436
436
457
457
457
657
657
657
329
329
329
839
839
839
3、同理可得对于倒是第二位的排序:
720
720
720
329
329
329
436
436
436
839
839
839
355
355
355
457
457
457
657
657
657
4、再对最后一位进行排序:
329
329
329
355
355
355
436
436
436
457
457
457
657
657
657
720
720
720
839
839
839
比较排序的时间复杂度下限证明详见两一篇blog,地址:比较算法时间复杂度最小值的证明。 ↩︎