基数排序
- 在常见排序算法中,如冒泡,选中,插入,希尔,归并(详情:juejin.cn/post/703107… ),堆排,快排(详情: juejin.cn/post/703145…) 都是基于比较的排序,在决定两个数前后位置的时候,都是需要把两个数进行大小比较来决定。
- 基数排序是一种非比较型 整数 排序算法,其原理是将整数按位数切割成不同的数字,然后按每个位数分别比较。
- 基数排序的应用范围有限,基数排序对有负数和0的数列难以进行排序,基于比较的排序更为通用。
- 基数排序的时间复杂度可以做到O(N),即有限次数循环可以完成排序。
桶排序(基数排序的一种)实现思路
数组:[51, 102, 15, 12, 13, 1325, 6, 23, 45],用基数排序对其排序:
步骤:
-
遍历数组,获取值最大的数,得到它的进制位数,如最大值1325 一共 4位(个,十,百,千)
-
按照进制的长度定义桶的大小,如:十进制的桶长度即为10, bucket = Array(10)
-
外循环最大的进位,如循环4次,依次对每个数个位,十位,百位,千位···进行出入桶的操作
-
循环数组,对数组每项求得当前进位数,存入桶中统计词频。如: 个位数的词频统计
bucket个位数词频 key:索引 0 1 2 3 4 5 6 7 8 9 value:词频 0 1 2 2 0 3 1 0 0 0 -
循环桶,对词频求前缀和 bucket[i] = bucket[i -1] + bucket[i]。bucket[4] = 8,表示,有8个数组项的个位 <=4。
bucket个位数词频 key:索引 0 1 2 3 4 5 6 7 8 9 value:前缀求和 0 1 3 5 8 9 9 9 9 9 -
逆序遍历数组,将arr中的每一项加入到辅助数组(存储arr按照个位数排序的结果)中。先求得arr[i]的个位数X,通过bucket[X]可以获得按照个位数排序的,arr[i]应该所在的位置。随即加入到help数组中,help[bucket[X] - 1] = arr[i],然后要将词频和减1,即bucket[X]--。
-
拷贝辅助数组到原数组中,至此,arr已经完成个位的排序。后面依次循环十位,百位,千位···,最后完成整个数组的排序。
这里的第6步有点绕,详细说明一下:
- 首先在内循环第一步统计词频的时候,是正序循环arr的。
- 内循环第二步得到词频前缀求和的结果,这里举例上面的数组,如数组项23个位数是3(原来数组最后一个个位数是3的项),桶中的前缀和bucket[3] = 5, 表示按照个位数排序的结果([51, 102, 12, 13, 23, 15, 1325, 35, 6]),23应为第五项,前面有5个项的个位数<= 3。
- 内循环第三步,一定要逆序遍历arr,因为统计的是的时候是正序遍历的,后统计的项应先取出,维持对之前排序结果的结果的不改变。
代码如下:
function bucketSort(arr, decimal = 10) {
// 根据传入的进位digit,获取num上的指定进制的值,如:
// (1325, 1) => 5 取个位数,
// (1325, 3) => 3取百分位数
function getDigitNum(num, digit) {
let value = 0;
while (digit) {
value = num % 10;
num = num / 10 | 0;
digit--;
}
return value;
}
// 获取数组中最大的数,它的进制位数,如1325 一共 4位
function getMaxDigitLength(arr) {
let max = Number.MIN_SAFE_INTEGER;
let index = 0;
while (index < arr.length) {
if (max < arr[index]) {
max = arr[index];
}
index++;
}
let maxDigitLength = 0;
while (max) {
max = max / 10 | 0;
maxDigitLength++;
}
return maxDigitLength
}
const maxDigitLength = getMaxDigitLength(arr)
let digit = 1;
// 按照进制的长度定义桶,十进制的桶长度即为10
let bucket = Array(decimal).fill(0);
// 辅助数组,长度同原始数组长度
let help = Array(arr.length);
let index = 0
// 循环最大的进位,依次对每个数个位,十位,百位,千位···进行出入桶的操作
for (; digit <= maxDigitLength; digit++) {
bucket = bucket.fill(0);
index = 0;
// 循环数组,对数组每项求得当前进位数,存入桶中统计词频。如:
// digit = 2时,即arr每项数字十分位上进行词频统计,最终得到的数组中,如:
// bucket[1] = 9表示,有9个数组项的十分位是1。
for (; index < arr.length; index++) {
bucket[getDigitNum(arr[index], digit)]++;
}
// 对桶中的词频进行前缀求和,最终得到的数组中。
// 如digit = 2时,bucket[4] = 14,表示,有14个数组项的十分位 <=4。
index = 1;
for (; index < bucket.length; index++) {
bucket[index] = bucket[index - 1] + bucket[index];
}
// 逆序遍历数组,将arr中的每一项加入到辅助数组中
index = arr.length - 1;
for (; index >= 0; index--) {
help[--bucket[getDigitNum(arr[index], digit)]] = arr[index];
}
// 遍历辅助数组,赋值给arr。至此本轮的排序结束,如digit = 2时,arr每项的十分位至此已经按照大小顺序排好
index = 0;
for (; index < arr.length; index++) {
arr[index] = help[index];
}
}
}
排序算法总结
基于比较的算法总结:
| 排序算法 | 时间复杂度 | 空间复杂度 | 稳定性 |
|---|---|---|---|
| 选择排序 | O(N²) | O(1) | 不能 |
| 冒泡排序 | O(N²) | O(1) | 能 |
| 插入排序 | O(N²) | O(1) | 能 |
| 归并排序 | O(N * logN) | O(N) | 不能 |
| 堆排序 | O(N * logN) | O(1) | 能 |
| 快速排序 | O(N * logN) | O(logN) | 能 |
- 稳定性的定义是:若经过排序,这些记录的相对次序保持不变,即在原序列中,r[i]=r[j],且r[i]在r[j]之前,而在排序后的序列中,r[i]仍在r[j]之前,则称这种排序算法是稳定的;否则称为不稳定的。
- 基于比较的排序算法中,目前没有时间复杂度维持在O(N * logN)级,空间复杂度维持O(1),还能保持稳定性
- 对于大数据量的排序,快排的效率最高,因为在快排的时间复杂度常数项最小。如果小数据量,如60以下的排序,插入排序的效率反而更高,所以我们可以优化快排(递归过程判断小数据量的时候,使用插入排序),可以达到提高速度的目的。
- 系统实现的Array.sort实现,v8 引擎 sort 排序策略是在数组长度小于 10 时使用
InsertionSort(插入排序),在大于 10 时使用In-place QuickSort(原地分区版的快速排序,即使用较少的空间实现的快速排序)(www.dazhuanlan.com/mingrong/to…