携手创作,共同成长!这是我参与「掘金日新计划 · 8 月更文挑战」的第25天,点击查看活动详情
一、前言
基数排序: 一种稳定的排序算法。它不是基于比较的算法,因此可以突破 O(n*log(n))
的下界。
它的工作原理是:
-
根据选取的基数,把整数键值分割成几个部分(
Redix=10
将整数按位数切割成不同的数字)。LSD
(Least Significant Digit
):从低位向高位进行处理。MSD
(Most Significant Digit
):从高位向低位进行处理。 -
依次以这几个部分所对应的整数作为键值,对原始序列进行多次计数排序或桶排序。
适用场景: 整数键值排序、较短的字符串键值排序(车牌号排序)。
基数排序 vs 计数排序 vs 桶排序:
- 基数排序:根据键值的每位数字来分配桶。
- 计数排序:每个桶只存储单一键值。
- 桶排序:每个桶存储一定范围的数值。
举个栗子:便于理解按十进制排序,[36, 9, 0, 25, 1, 49, 64, 16, 81, 4]
- 首先根据个位数的数值,按照个位置等于桶编号的方式,将它们分配至编号0到9的桶子中:
桶编号 | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 |
---|---|---|---|---|---|---|---|---|---|---|
0 | 1 | 64 | 25 | 36 | 9 | |||||
81 | 4 | 16 | 49 |
将这些数字按照桶以及桶内部的排序连接起来:[0, 1, 81, 64, 4, 25, 36, 16, 9, 49]
- 接着按照十位的数值,分别对号入座:
桶编号 | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 |
---|---|---|---|---|---|---|---|---|---|---|
0 | 16 | 25 | 36 | 49 | 64 | 81 | ||||
1 | ||||||||||
4 | ||||||||||
9 |
最后按照次序重现连接,完成排序:[0, 1, 4, 9, 16, 25, 36, 49, 64, 81]
**实现如下:**按二进制划分,再排序
public class RadixSort {
// 基数为 256,每次取 8 个二进制位作为一个部分进行处理,32 位整数需要处理 4 次。
// 每次取出的 8 个二进制位会作为计数排序的键值,去排序原始数据。
// 每次处理 8 个二进制位,是时间/空间上比较折衷的方法。
// 如果一次处理 16 个二进制位,速度会稍微快一些。但需要额外的空间是 2^16 = 65536,远大于每次处理 8 个二进制位所需空间。
// 如果一次只处理 4 个二进制位,速度则会慢很多。
public void sort4pass(int[] arr) {
sort(arr, 8, 0xff);
}
public void sort8pass(int[] arr) {
sort(arr, 4, 0x0f);
}
// Time: O(32/b * n), Space: O(n + 2^b)
private void sort(int[] arr, int bits, int mask) {
if (arr == null || arr.length == 0) return;
int n = arr.length, cnt = 32 / bits;
int[] tmp = new int[n];
int[] indexes = new int[1 << bits];
for (int d = 0; d < cnt; ++d) {
for (int num : arr) {
int idx = (num >> (bits * d)) & mask;
++indexes[idx];
}
--indexes[0];
for (int i = 1; i < indexes.length; ++i)
indexes[i] = indexes[i] + indexes[i - 1];
for (int i = n - 1; i >= 0; --i) {
int idx = (arr[i] >> (bits * d)) & mask;
tmp[indexes[idx]] = arr[i];
--indexes[idx];
}
Arrays.fill(indexes, 0);
int[] t = arr;
arr = tmp;
tmp = t;
}
// handle the negative number
// get the length of positive part
int len = 0;
for (; len < n; ++len)
if (arr[len] < 0) break;
System.arraycopy(arr, len, tmp, 0, n - len); // copy negative part to tmp
System.arraycopy(arr, 0, tmp, n - len, len); // copy positive part to tmp
System.arraycopy(tmp, 0, arr, 0, n); // copy back to arr
}
}
个人认为,可以先了解基数排序的思路,若工作中实际有用到,也能快速了解再深入。
二、题目
(1)排序数组(中)
题干分析
给你一个整数数组 nums
,请你将该数组升序排列。
示例 1:
输入:nums = [5,2,3,1]
输出:[1,2,3,5]
示例 2:
输入:nums = [5,1,1,2,0,0]
输出:[0,0,1,1,2,5]
思路解法
方法一:可以直接套上面的公式,可以满足负数。
上面是用二进制,这边使用 十进制,再来排序一遍。
// 基数排序-十进制排序, Faster: 74.74%
public int[] sortArrayRedixSort2(int[] nums) {
// RadixSort 基数排序
int n = nums.length;
// 预处理,让所有的数都大于等于0
for (int i = 0; i < n; ++i) {
nums[i] += 50000; // 50000为最小可能的数组大小
}
// 找出最大的数字,并获得其最大位数
int maxNum = nums[0];
for (int value : nums) {
if (value > maxNum) {
maxNum = value;
}
}
int num = maxNum, maxLen = 0;
while (num > 0) {
++maxLen;
num /= 10;
}
// 基数排序,低位优先
int divisor = 1;
int[] tmp = new int[n];
for (int i = 0; i < maxLen; ++i) {
radixSort(nums, tmp, divisor);
swap(tmp, nums);
divisor *= 10;
}
// 减去预处理量
for (int i = 0; i < n; ++i) {
nums[i] -= 50000;
}
return nums;
}
private void swap(int[] nums1, int[] nums2) {
for (int i = 0; i < nums1.length; ++i) {
int temp = nums1[i];
nums1[i] = nums2[i];
nums2[i] = temp;
}
}
private void radixSort(int[] nums, int[] tmp, int divisor) {
int n = nums.length;
int[] counts = new int[10];
// 统计个、十、百、千、万上对应 0 ~ 9 的出现次数
for (int num : nums) {
int x = (num / divisor) % 10;
++counts[x];
}
// 前缀和
for (int i = 1; i <= 9; ++i) {
counts[i] += counts[i - 1];
}
// 从后向前赋值
for (int i = n - 1; i >= 0; --i) {
int x = (nums[i] / divisor) % 10;
int index = counts[x] - 1;
tmp[index] = nums[i];
--counts[x];
}
}