桶排序原理及实现
Posted 大前端百科
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了桶排序原理及实现相关的知识,希望对你有一定的参考价值。
预备知识
桶排序、计数排序、基数排序 三种排序算法的时间复杂度是 O(n) 。因为这些排序算法的时间复杂度是线性的,所以我们把这类排序算法叫作线性排序(Linear sort)。之所以能做到线性的时间复杂度,主要原因是,这三个算法是非基于比较的排序算法,都不涉及元素之间的比较操作。
桶排序
基本思想
桶排序是一种用空间换取时间的排序,桶排序重要的是它的思想,而不是具体实现,时间复杂度最好可能是线性O(n),桶排序不是基于比较的排序而是一种分配式的。
桶排序从字面的意思上看:
若干个桶,说明此类排序将数据放入若干个桶中。
每个桶有容量,桶是有一定容积的容器,所以每个桶中可能有多个元素。
从整体来看,整个排序更希望桶能够更匀称,即既不溢出(太多)又不太少。
桶排序的思想为:「将待排序的序列分到若干个有序的桶中,每个桶内的元素再进行个别排序。」 当然桶排序选择的方案跟具体的数据有关系,桶排序是一个比较广泛的概念,并且计数排序是一种特殊的桶排序,基数排序也是建立在桶排序的基础上。在数据分布均匀且每个桶元素趋近一个时间复杂度能达到O(n), 但是如果数据范围较大且相对集中就不太适合使用桶排序。
问题思考
桶排序看起来很优秀,那它是不是可以替代我们之前讲的排序算法呢?
答案当然是否定的。实际上,桶排序对要排序数据的要求是非常苛刻的。
首先,要排序的数据需要很容易就能划分成 m 个桶,
并且,桶与桶之间有着天然的大小顺序。这样每个桶内的数据都排序完之后,桶与桶之间的数据不需要再进行排序。
其次,数据在各个桶之间的分布是比较均匀的。如果数据经过桶的划分之后,有些桶里的数据非常多,有些非常少,很不平均,那桶内数据排序的时间复杂度就不是常量级了。在极端情况下,如果数据都被划分到一个桶里,那就退化为 O(nlogn) 的排序算法了
桶排序比较适合用在外部排序中。
所谓的外部排序就是数据存储在外部磁盘中,数据量比较大,内存有限,无法将数据全部加载到内存中。
代码实现
一下代码只是桶排序的一种实现,具体会根据认为设置的桶的个数,和桶内元素个数不同而有所变化。
function bucketSort(arr){
// 获取最大值,最小值
let min = arr[0];
let max = arr[0];
for(let i = 1; i < arr.length; i++) {
if(arr[i] < min) {
min = arr[i];
}
if(arr[i] > max) {
max = arr[i];
}
}
// 设置桶的个数
let buckets = [];
for(let i =0;i< Math.floor((max-min)/10 + 1); i++){
buckets.push([]);
}
for(let i = 0; i < arr.length; i++) {
buckets[Math.floor(arr[i]/10)].push(arr[i]);
}
arr.length = 0;
for(let i=0;i<buckets.length;i++) {
// 这里只是一种模拟
buckets[i].sort();
// 将各个桶中的数据收集
arr.push(...buckets[i]);
}
}
let arr = [1,8,7,44,42,46,38,34,33,17,15,16,27,28,24];
bucketSort(arr);
注意事项
桶排序对要排序数据的要求是非常苛刻的。如果数据不符合桶排序的要求,性能就会比较差。
时间复杂度
如果要排序的数据有 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)。
计数排序
计数排序的核心在于将输入的数据值转化为键存储在额外开辟的数组空间中。作为一种线性时间复杂度的排序,计数排序要求输入的数据必须是有确定范围的整数。
计数排序算法不是基于元素比较,而是利用数组下标来确定元素的正确位置。
它的优势在于在对一定范围内的整数排序时,它的复杂度为Ο(n+k),快于任何比较排序算法。
当然这是一种牺牲空间换取时间的做法,而且当O(k)>O(n*log(n))
的时候其效率反而不如基于比较的排序。
计数排序是一种特殊的桶排序,每个桶的大小为1。
基本思想
在「设计具体算法的时候」,先找到最小值min,再找最大值max。然后创建这个区间大小的数组,从min的位置开始计数,这样就可以最大程度的压缩空间,提高空间的使用效率。
代码实现
function countSort(arr){
let min = arr[0];
let max = arr[0];
for(let i=1;i<arr.length;i++) {
if(arr[i] < min) {
min = arr[i];
}
if(arr[i] > max) {
max = arr[i];
}
}
let buckets = new Array(max-min+1).fill(0);
for(let i=0;i<arr.length;i++) {
buckets[arr[i]-min]++;
}
let index = 0;
for(let i=0;i<buckets.length;i++) {
while(buckets[i]-- > 0) {
arr[index++] = i+min;
}
}
}
let arr = [10001,10010,10003,10001,10002,10020,10008];
countSort(arr);
复杂度和稳定度
注意事项
计数排序只能为正整数?!
基数排序
基数排序是一种很容易理解但是比较难实现(优化)的算法。基数排序也称为卡片排序。
基本思想
基数排序的原理就是多次利用计数排序(计数排序是一种特殊的桶排序),但是和前面的普通桶排序和计数排序有所区别的是,「基数排序并不是将一个整体分配到一个桶中」,而是将自身拆分成一个个组成的元素,每个元素分别顺序分配放入桶中、顺序收集,当从前往后或者从后往前每个位置都进行过这样顺序的分配、收集后,就获得了一个有序的数列。
图示过程
假设是数字类型排序(也可以是字符类型),就拿 934,241,3366,4399这几个数字进行基数排序的一趟过程来看,第一次会根据各位进行分配、收集。
分配和收集都是有序的,第二次会根据十位进行分配、收集,此次是在第一次个位分配、收集基础上进行的,所以所有数字单看个位十位是有序的。
而第三次就是对百位进行分配收集,此次完成之后百位及其以下是有序的。
而最后一次的时候进行处理的时候,千位有的数字需要补零,这次完毕后后千位及以后都有序,即整个序列排序完成。
注意事项
基数排序还有字符串等长、不等长、一维数组优化等各种实现需要需学习。
代码实现
function radixSort(arr){
let buckets = new Array(10).fill(0).map(() => []);
let max = arr[0];
for(let i=1;i<arr.length;i++) {
if(arr[i] > max) {
max = arr[i];
}
}
let divideNum = 1;
while(max > 0) {
// 放入不同的桶中
for(let i=0;i<arr.length;i++) {
buckets[(Math.floor(arr[i]/divideNum))%10].push(arr[i]);
}
divideNum *= 10;
max = Math.floor(max/10);
// 收集数据
let index = 0;
for(let i=0;i<buckets.length;i++){
for(let j=0;j<buckets[i].length;j++) {
arr[index++] = buckets[i][j];
}
// 收集完数据,清空桶
buckets[i] = [];
}
}
}
let arr = [634,5567,336];
radixSort(arr);
以上是关于桶排序原理及实现的主要内容,如果未能解决你的问题,请参考以下文章