数据结构与算法之美学习总结,这一课讲了三个线性排序,这三种排序时间复杂度都是 O ( n ) O(n) O(n) 。
1. 桶排序(Bucket sort)
把 n n n 个数据分到 m m m 个桶内,每个桶会有 k = n / m k=n/m k=n/m 个元素,每个桶内使用快速排序。桶排序的时间复杂度为 O ( m k l o g k ) O(mklogk) O(mklogk),如果桶的数量越接近 n n n,就会退变成 O ( n ) O(n) O(n) 。
桶排序对数据的要求很高:
- 桶与桶之间有天然大小顺序;
- 数据在桶之间的分布均匀。
桶排序适合用于外部排序: 数据存在外部磁盘中,数据量大而内存小,无法将数据全部加载到内存。
如果对 10 GB 的订单按照金额排序,但是内存又不够大,就可以对其进行桶排序。一般情况下 1 ~ 1000 的金额会比较多,那就对这部分再进行桶排序,直到内存可以装得下。
2. 计数排序(Counting sort)
计数排序可以看做桶排序的特殊情况。 但是桶的大小粒度不一样,一般来说,计数排序的桶会偏大但偏少。所以当数据的范围 k 比要排序的数据 n 大得多,就不适合用计数排序。
计数排序只能给非负整数排序。 特殊情况的处理:
- 如果数据范围是 [-1000, 1000],那么我们给每一个数据加 1000,范围就变为 [0, 2000];
- 如果数据精度是小数点后一位如,[0.1, 9.8],我们可以给每个数据乘以 10,就变为 [1, 98]。
计数的体现:
如上图:以数组 A = [2 5 3 0 2 3 0 3] 为例,说说计数排序的过程:
- 因为数据的范围在 [0, 5],所以把数据分为 6 个桶,并存放在 C 数组中(C 的下标代表数据,对应位置存放的是下标对应数据的数量);
- 然后对 C 中每个元素都求其前面元素与当前元素的和,即 C[i] = C[i - 1] + C[i];
- 请求一个存放排序好的数据的数组,与 A 同样大小;
- 从右往左对 A 排序,重复一下步骤:
- R[C[A[n]] - 1] = A[n]
- n = n - 1
计数排序的 Python 实现:
# copyright (c) strongnine
def countingSort(a):
n = len(a)
if n <= 1:
return
max = a[0]
for i in range(1, n):
if max < a[i]:
max = a[i]
c = [0 for i in range(max + 1)]
for i in range(n):
c[a[i]] += 1
for i in range(1, max + 1):
c[i] = c[i - 1] + c[i]
r = [0 for i in range(n)]
for i in range(n - 1, -1, -1):
index = c[a[i]] - 1
r[index] = a[i]
c[a[i]] -= 1
return r
if __name__ == '__main__':
a = [2, 5, 3, 0, 2, 3, 0, 3]
r = countingSort(a)
print(r)
3. 基数排序(Radix sort)
类似于对手机号的排序,数据的范围很大,所以要分桶就不太行,所以不适合上面两种方式。
过程: 先按照最后一位使用稳定的线性排序来排序手机号码,再按照倒数第二位排序,直到弄完全部 11 位。注意使用的排序算法一定要是稳定的。
对于不一样长的数据,例如对单词的排序,可以在每个单词后面都补零,再排序。
基数排序的要求:
- 数据的「位」需要可以独立分割出来比较;
- 位之间有递进关系,即高位比低位高,就可以忽略更低位的大小。
4. 线性排序的局限
线性排序算法对于数据的要求都比较严苛,应用并不是很广泛,但是如果数据的特征很适合使用线性排序,那么用起来就会很高效。