查找与排序:计数排序
计数排序
计数排序有限制条件,序列里的元素必须是有限偏序集,如整数,如英文字母。那么它的性能为O(n+k),其中n为元素个数,k为有限偏序集大小,如个位整数,k最大就是10,如英文字母,k最大就是26。
计数排序相当于利用偏序集的特点,对元素进行分组,分组数为偏序集的基数,举例子来说,一个长度为n的随机英文字母ABCDE序列,我们要排序,只需要将序列中的每个字母分组到自己那个组中,然后把组按次序串起来就行了,因为是偏序集,所以组之间本来就是有次序的。演示如下:
序列为S=”BAACCBEECCDDBA”,我们按照字母次序分组如下:
0 |
1 |
2 |
3 |
4 |
全部A字母 |
全部B字母 |
全部C字母 |
全部D字母 |
全部E字母 |
我们扫描序列S,把字母一个一个放到组中:
0 |
1 |
2 |
3 |
4 |
AAA |
BBB |
CCCC |
DD |
EE |
这样自然排好序了。
我们现在看程序的思路,我们用一个新的序列,将原序列中的字母放到正确的位置来。为了算出这个正确的位置,我们需要知道序列中每一个字母出现的次数。
我们从头到尾扫描串S,总共有14个字母,计一下数,得到如下字母次数数组:
0(A) |
1(B) |
2(C) |
3(D) |
4(E) |
3 |
3 |
4 |
2 |
2 |
然后我们根据次数数组,就能算出字母的输出位置,数组如下:
0(A) |
1(B) |
2(C) |
3(D) |
4(E) |
3 |
6 |
10 |
12 |
14 |
这个输出位置就是前一格的值加上自己这个字母的值,也就是截止位置。如对字母C来讲,前面两个字母占用了6个位置,本身C又有四个,所以C的截止位置为10。
接下来,我们从尾到头反过来扫描序列S:”BAACCBEECCDDBA”。首先碰到的字母为A,我们检查计数表,对应A的值为3,所以我们把它放在第3的位置:
A |
然后把计数表中对应的值减1,这样计数表成了:
0(A) |
1(B) |
2(C) |
3(D) |
4(E) |
2 |
6 |
10 |
12 |
14 |
下一次从序列中读取的字母为B,我们检查计数表,对应B的值为6,所以我们把它放在第6的位置。
A |
B |
然后把计数表中对应的值减1,这样计数表成了:
0(A) |
1(B) |
2(C) |
3(D) |
4(E) |
2 |
5 |
10 |
12 |
14 |
一直这么循环。
代码如下:
def CountingSort(S,BASE):
S0=['']*len(S) #return array
POS=[0]*len(BASE) #counting array
#counting
for c in S:
POS[ord(c)-ord('A')] += 1
#position
for i in range(1,len(POS)):
POS[i] += POS[i-1]
#generate new array
for i in range(len(S)-1,-1,-1): #desc order
idx=POS[ord(S[i])-ord('A')]-1
S0[idx]=S[i]
POS[ord(S[i])-ord('A')] -= 1
return S0
测试一下:
S=['B','A','A','C','C','B','E','E','C','C','D','D','B','A']
BASE=['A','B','C','D','E']
print(CountingSort(S,BASE))
运行结果:
['A', 'A', 'A', 'B', 'B', 'B', 'C', 'C', 'C', 'C', 'D', 'D', 'E', 'E']
我们看到,计数排序只扫描了原序列2遍,还使用了一个长度更小的数组POS,所以整个排序性能为O(n)。
有的人爱动脑筋,觉得不需要在新序列中算位置,就用一个二维数组,第一维为ABCDE五个位置,第二维为序列S中的哪些对应的字母;甚至有人进一步想到了数组+链表结构。这样都是可以的,事实上,这种算法还有一种变形叫做桶排序,所谓桶就是你们想到的第一维数组,桶这个词太贴切了。
点击“阅读原文”,可进入原创书籍,阅读目录