基数排序
- 算法描述
非比较排序,「基」指的是数的位,例如十进制数123,共有百十个位,共3个位。基数排序 按数字的位进行循环,每一***作都是对当前位(基数)的计数排序,使得输出到arr后所有数字在截止到当前位上(即去掉未考察的位后)是排序状态,考察完最大位后完成排序。具体过程如下:
遍历待排序数组arr,找到最大值,计算其位数,例如arr中最大数为123,则maxDigitLen = 3。
数组的数字为n进制,就创建大小为n的计数数组countArr,也可以称为n个桶。
开始「位」的for循环,循环次数等于maxDigitLen,每一轮对 当前所有数字的当前位 执行一次 计数排序。
每次计数排序结束后将结果写回arr。
for循环结束后返回排序结果arr。
如下动图演示{6674, 1560, 5884, 2977, 2922, 4127, 5390, 7870, 1193, 7163}的基数排序过程。
也可以不使用计数排序,而是创建一个二维数组(可看作19个桶)保存每次遍历的中间结果,此写法耗费的空间较大(每个桶的大小都要等于arr大小+1,空间复杂度为O(19n)),是稳定排序,不详细说明,可以参考后续给出的代码实现。
以计数排序为基础的基数排序,每一位循环时都对所有数做该位的计数排序。
不以计数排序为基础的基数排序,每一位循环时都将所有数按顺序放入相应的桶中。
- 稳定性:取决于计数排序的稳定性。
两个同值的元素位数相同,按位循环,若采用稳定版本的计数排序则稳定,否则不稳定。
- 优化
处理负数优化:若存在负数,可以先找到最小值(负数),对arr中的每个数,都加上此最小值的绝对值,排序完成后再减回去。但加法可能使得 数字越界,一种更好的办法是计数排序时 将countArr的大小扩展为 19,以[0, 19]对应可能出现的[-9, 9]。因此在每轮求当前基数时,要在原基数结果上 +9 以对应countArr的下标。
后续代码给出利用计数排序的应用此优化的版本。
-
复杂度分析
时间复杂度:d为绝对值最大的元素位数,总共进行d轮计数排序,O(n + k)O(n+k) 是计数排序的复杂度,其中k是位的取值范围,如果是非负数,则 k = 10 (0~9),如果包含负数,则 k = 19 (-9~9)。所以总的时间复杂度为 O(d(n + k))O(d(n+k))。 -
空间复杂度:
利用计数排序的基数排序,空间复杂度与计数排序相同,为 O(n + k)O(n+k)。
不利用计数排序的计数排序,将以19个(应用处理负数优化)「桶」的二维数组作为排序过程中的存储空间,故空间复杂度为 O(19n)O(19n),当n显著大于19时也可认为其空间复杂度为 O(n)O(n)。
代码
- 以计数排序为基础
public int[] radixSort(int[] arr) {
if (arr.length < 2) return arr;
int max = Math.abs(arr[0]); // 找到arr中绝对值最大者
for (int i = 1; i < arr.length; i++) {
max = Math.max(max, Math.abs(arr[i]));
}
int maxDigitLen = 0, base = 10; // 最大位数 & 基(几进制就是几)
while (max != 0) {
maxDigitLen++;
max /= base;
}
// 在接下来的for中,每一轮都对当前位(基数)执行一次计数排序
int[] sortedArr = new int[arr.length];
for (int i = 0; i < maxDigitLen; i++) {
int[] countArr = new int[19]; // 处理负数优化
// 根据每一个数字当前位的数字,累计相应位置的计数
for (int j = 0; j < arr.length; j++) {
// 此步处理要注意,当base大于10时,例如base=100时,1234%100=34
// 还需要再除以(base/10),得到的3,然后再+9(考虑负数)才是本次的bucketIdx
int bucketIdx = (arr[j] % base) / (base / 10) + 9;
countArr[bucketIdx]++;
}
// countArr变形,得到每个下标所代表的arr中的数的当前位在arr中的最大位置(从1开始)
for (int j = 1; j < countArr.length; j++) {
countArr[j] += countArr[j - 1];
}
// 逆序输出保持稳定性
for (int j = arr.length - 1; j >= 0; j--) {
int thisBase = (arr[j] % base) / (base / 10) + 9;
// countArr[thisBase]得到的从1开始计算的位置,转成下标要-1
sortedArr[countArr[thisBase] - 1] = arr[j];
countArr[thisBase]--;
}
// 完成当前位的计数排序后将排序结果拷贝回原数组
arr = Arrays.copyOf(sortedArr, sortedArr.length);
// base进一位,准备下一轮对下一位的计数排序
base *= 10;
}
return arr;
}
不以计数排序为基础
public int[] radixSort(int[] arr) {
if (arr.length < 2) return arr;
// 找到arr中绝对值最大者
int max = Math.abs(arr[0]);
for (int i = 1; i < arr.length; i++) {
max = Math.max(max, Math.abs(arr[i]));
}
int maxDigitLen = 0, base = 10; // 最大位数 & 基
while (max != 0) {
maxDigitLen++;
max /= base;
}
// arr.length + 1的作用是令每个桶的第0位保存该桶的元素个数。
int[][] buckets = new int[19][arr.length + 1]; // 处理负数优化
// 在每一位上将数组中所有具有该位的数字装入对应桶中
for (int i = 0; i < maxDigitLen; i++) {
for (int j = 0; j < arr.length; j++) {
// 此步处理要注意,当base大于10时,例如base=100时,1234%100=34
// 还需要再除以(base/10),得到的3才是本次的bucketIndex
int bucketIdx = (arr[j] % base) / (base / 10) + 9; // +9使其可以处理负数
int currentBucketQuantity = buckets[bucketIdx][0];
buckets[bucketIdx][currentBucketQuantity + 1] = arr[j];
buckets[bucketIdx][0]++;
}
// 将当前所有桶的数按桶序,桶内按低到高输出为本轮排序结果
int arrIdx = 0;
for (int j = 0; j < buckets.length; j++) {
for (int k = 1; k <= buckets[j][0]; k++) {
arr[arrIdx++] = buckets[j][k];
}
}
// 每一轮过后将桶计数归零
for (int[] bucket : buckets) bucket[0] = 0;
base *= 10; // 调整base
}
return arr;
}