线性排序
时间复杂度是线性的,所以我们把这类排序算法叫作线性排序(Linear sort),桶排序、计数排序、基数排序就是常见的线性排序,之所以能做到线性,他们不是通过比较完成的。
桶排序(Bucket sort)
桶排序对于排序数据的要求是非常苛刻的。
核心思想是将要排序的数据分到几个有序的桶里,每个桶里的数据再单独进行排序。桶内排完序之后,再把每个桶里的数据按照顺序依次取出,组成的序列就是有序的了。
如果要排序的数据有 n 个,我们把它们均匀地划分到 m 个桶内,每个桶里就有 k=n/m 个元素。每个桶内部使用快速排序,时间复杂度为 O(k * logk)。m 个桶排序的时间复杂度就是 O(m * k * logk),因为 k=n/m,所以整个桶排序的时间复杂度就是 O(n*log(n/m))。当桶的个数 m 接近数据个数 n 时,log(n/m) 就是一个非常小的常量,这个时候桶排序的时间复杂度接近 O(n)。
为什么苛刻呢?
- 要排序的数据需要很容易就能划分成 m 个桶,并且,桶与桶之间有着天然的大小顺序。这样每个桶内的数据都排序完之后,桶与桶之间的数据不需要再进行排序。
- 数据在各个桶之间的分布是比较均匀的。如果数据经过桶的划分之后,有些桶里的数据非常多,有些非常少,很不平均,那桶内数据排序的时间复杂度就不是常量级了。在极端情况下,如果数据都被划分到一个桶里,那就退化为 O(nlogn) 的排序算法了。
桶排序一般适用于外部排序,开发中基本用不到
计数排序(Counting sort)
计数排序其实是桶排序的一种特殊情况
上面的结论是桶排序在开发中基本不用,为什么还要学习计数排序呢?
当要排序的 n 个数据,所处的范围并不大的时候,比如最大值是 k,我们就可以把数据划分成 k 个桶。每个桶内的数据值都是相同的,省掉了桶内排序的时间。
高考查分数系统你还记得吗?我们查分数的时候,系统会显示我们的成绩以及所在省的排名。如果你所在的省有 50 万考生,如何通过成绩快速排序得出名次呢?考生的满分是 900 分,最小是 0 分,这个数据的范围很小,所以我们可以分成 901 个桶,对应分数从 0 分到 900 分。根据考生的成绩,我们将这 50 万考生划分到这 901 个桶里。桶内的数据都是分数相同的考生,所以并不需要再进行排序。我们只需要依次扫描每个桶,将桶内的考生依次输出到一个数组中,就实现了 50 万考生的排序。因为只涉及扫描遍历操作,所以时间复杂度是 O(n)。
计数排序的算法思想就是这么简单,跟桶排序非常类似,只是桶的大小粒度不一样。
它的核心点在于“计数”
通过一个例子来说明何为计数.
假设只有 8 个考生,分数在 0 到 5 分之间, 考生成绩如下:
记为:
//考生成绩
examineeScore = [1, 5, 2, 0, 1, 3, 0, 3]
然后我们把考生的成绩按照分数分为6个桶, 数组下标表示分数
score = [size=6]
比如: socre = [2,1]
下标为0: 则0分的有两人
此时我们遍历一遍考生成绩就可以得到成绩对应人个数的数组
for (i in examineeScore.indices) {
// socre[examineeScore[i]] = socre[examineeScore[i]] + 1
// 简化:
socre[examineeScore[i]]++
}
res:[2, 2, 1, 2, 0, 1]
个数有了,怎么进行排序呢?
这里有一个程式的技术点:
- 对 socre[6]数组顺序求和,socre[6]存储的数据就变成了下面这样子。socre[k]里存储小于等于分数 k 的考生个数。
[2, 4, 5, 7, 7, 8]
具体计算为:
2 + 2 + 1 + 2 + 0 + 1
2。 4。 5。 7。 7。 8。
比如socre[3] 里面的数据5,表示小于等于3分的人有5个 ([1, 5, 2, 0, 1, 3, 0, 3])
- 对带排序数组进行处理
比如,当扫描到 3 时,我们可以从数组 socre 中取出下标为 3 的值 7,也就是说,到目前为止,包括自己在内,分数小于等于 3 的考生有 7 个,也就是说 3 是排序后的数组中的第 7 个元素(也就是下标为 6 的位置)。当 3 放入到排序后的数组中后,小于等于 3 的元素就只剩下了 6 个了,所以相应的 socre[3]要减 1,变成 6。
执行流程如下:
注意红色箭头的变化,就是上述描述的(所以相应的 socre[3]要减 1,变成 6)部分
具体代码
private fun countSort(arr: IntArray, count: IntArray) {
for (i in arr.indices) {
count[arr[i]]++
}
var sum = 0
for (i in count.indices) {
print("${count[i]} \t")
sum += count[i]
count[i] = sum
}
println("计数数组 = ${Arrays.toString(count)}\n")
for (i in arr.size - 1 downTo 0) {
result[count[arr[i]] - 1] = arr[i]
//2,2,4,7,7,8
println(
"数组下标为${arr[i]}的值${count[arr[i]]} --> 也就是分数小于等于${arr[i]}的人有${count[arr[i]]}个 --> ${
Arrays.toString(
result
)
} \t"
)
count[arr[i]]--
println("count 变为 ${Arrays.toString(count)}")
}
}
计数排序只能用在数据范围不大的场景中,如果数据范围 k 比要排序的数据 n 大很多,就不适合用计数排序了。而且,计数排序只能给非负整数排序,如果要排序的数据是其他类型的,要将其在不改变相对大小的情况下,转化为非负整数.