【算法】基数排序

134 阅读3分钟

携手创作,共同成长!这是我参与「掘金日新计划 · 8 月更文挑战」的第25天,点击查看活动详情

一、前言

基数排序: 一种稳定的排序算法。它不是基于比较的算法,因此可以突破 O(n*log(n)) 的下界。

它的工作原理是:

  1. 根据选取的基数,把整数键值分割成几个部分(Redix=10将整数按位数切割成不同的数字)。

    LSDLeast Significant Digit):从低位向高位进行处理。

    MSDMost Significant Digit):从高位向低位进行处理。

  2. 依次以这几个部分所对应的整数作为键值,对原始序列进行多次计数排序或桶排序。

radixSort.gif

适用场景: 整数键值排序、较短的字符串键值排序(车牌号排序)。

基数排序 vs 计数排序 vs 桶排序:

  • 基数排序:根据键值的每位数字来分配桶。
  • 计数排序:每个桶只存储单一键值。
  • 桶排序:每个桶存储一定范围的数值。

举个栗子:便于理解按十进制排序,[36, 9, 0, 25, 1, 49, 64, 16, 81, 4]

  1. 首先根据个位数的数值,按照个位置等于桶编号的方式,将它们分配至编号0到9的桶子中:
桶编号0123456789
016425369
8141649

将这些数字按照桶以及桶内部的排序连接起来:[0, 1, 81, 64, 4, 25, 36, 16, 9, 49]

  1. 接着按照十位的数值,分别对号入座:
桶编号0123456789
0162536496481
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)排序数组(中)

LeetCode 912

题干分析

给你一个整数数组 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];
    }
}