算法原理系列:木桶排序

 算法原理系列:木桶排序

木桶排序是一种用标记来替代比较操作的排序手段,适用范围较窄,但效率极高,时间复杂度为 O(n) ,在生活中,我们也经常能看到一些木桶排序的实际案例,比如扑克牌排序时,我们把它平摊在空间中,这种记录相对位置的排序方法是最直观的木桶排序。

缘由

先来看看,在计算机视角中,如何利用相对位置进行排序操作。给出数据集:

nums = [9,2,1,4,7,8,6]

这样的数据集有明显的特点,nums在指定范围内,所以我们可以建立一个map来映射nums的值和相对位置关系。

map
index: 0 1 2 3 4 5 6 7 8 9
value: 0 1 1 0 1 0 1 1 1 1

扫描value中非零元素,得到的排序就是最终的排序结果

代码如下:

    public static void bucketSortWithNoDuplicate(int[] data){
        int n = data.length;
        int[] aux = new int[n];
        int[] map = new int[4 * n]; //开辟足够大的空间
        for (int i = 0; i < n; i++){
            map[data[i]]++; 
        }

        for (int i = 0, k = 0; i < 4 * n; i++){
            if (map[i] == 0) continue;
            aux[k++] = i;
        }

        //回写
        for (int i = 0; i < n; i++){
            data[i] = aux[i];
        }
    }

这是最简单的思路,但该排序方法只针对非重复元素的排序,遇到重复元素就不起作用。

如:

nums = 2 5 5 6 8 5 2 6 1 1 6 5

map
index : 0 1 2 3 4 5 6 7 8 9
value : 0 2 2 0 0 4 3 0 1 0

元素1出现的次数为2次
元素2出现的次数为2次
...

做点变形:map[data[i]+1]++;
map
index : 0 1 2 3 4 5 6 7 8 9 10
value : 0 0 2 2 0 0 4 3 0 1 0

累加:
map
index : 0  1  2  3  4  5  6  7  8  9  10
value : 0  0  2  4  4  4  8  11 11 12 0

物理含义:
元素1出现的下标为1
元素2出现的下标为2
...

这样我们就充分利用了map统计的频次,并决定了元素的起始位置在何方。

最终排序时,借助aux数组:
for (int i = 0; i < n; i++){
    aux[map[data[i]]++] = data[i];
}

此时,注意map在取映射时并未+1

这就好比在扑克排序时,预先统计了每张排出现的频次是多少,如2出现3次,3出现2次,则预先给2留三个空位,再给3留5个空位,塞满元素后,也就排序完成。

代码如下:

public static void bucketSortWithDuplicate(int[] data){
        int n = data.length;
        int[] map = new int[11];
        int[] aux = new int[n];

        //计算出现频率
        for (int i = 0; i < n; i++){
            map[data[i]+1]++;
        }
        //预留空位
        for (int i = 1; i < map.length; i++){
            map[i] = map[i-1] + map[i];
        }
        //排序
        for (int i = 0; i < n; i++){
            aux[map[data[i]]++] = data[i];
        }
        //回写
        for (int i = 0; i < n; i++){
            data[i] = aux[i];
        }
    }

低位优先排序

在上述基础上,我们可以实现固定位数的radix排序,如:

nums:
791372584
783336045
313758717
165485572
536175826
619392783
231528160
309593813
830298494
219957039

思路:
从右到左依次扫描每一位,用木桶排序,因为在针对高位排序时,上述排序稳定,所以不会影响相对位置,这样当遍历到最后一位时,就自然整体有序。

代码如下:

public static void radixSort(long[] data){
        int n = data.length;
        int exp = 1;
        int radix = 10;
        long max = data[0];

        while (max/exp != 0){
            int[] map = new int[11];
            long[] aux = new long[n];

            for (int i = 0; i < n; i++){
                map[(int)(data[i] / exp % 10) + 1]++;
            }

            for (int i = 1; i < n; i++){
                map[i] += map[i-1];
            }

            for (int i = 0; i < n; i++){
                aux[map[(int)(data[i] / exp % 10)]++] = data[i];
            }

            for (int i = 0; i < n; i++){
                data[i] = aux[i];
            }
            exp *= radix;
        }
    }

针对不同位数的数组排序,低位优先只要明确了数组中最大数的最长位数,同样能达到排序效果。

高位优先排序

再介绍一种高位优先排序,如,给定字符数组:

输入:
she
sells
seashells
by
the
seashore
the
shells
she
sells
are
surely
seashells

高位排序,先按第一个字符进行木桶排序,所以有:
0 : are
  ----------
1 : by
  ----------
2 : she
3 : sells
4 : seashells
5 : sea
6 : shore
7 : shells
8 : she
9 : sells
10: surely
11: seashells
  ----------
12: the
13: the

所以经过一轮木桶排序后,我们只要能够得到分组后的头index和尾index,便能递归的进行排序操作了。

递归,无非就是当首字母相同时,如index(2~11)之间的元素,进行第二轮木桶排序。

代码如下:

    private static int R = 256;
    private static final int M = 0;
    private static String[] aux;

    private static int charAt(String s, int d){
        if (d < s.length()) return s.charAt(d); else return -1;
    }

    public static void sort(String[] data){
        int N = data.length;
        aux = new String[N];
        sort(data, 0, N - 1, 0);
    }

    private static void sort(String[] data, int lo, int hi, int d){

        if (hi <= lo + M){
            insertionSort(data, lo, hi, d);
            return;
        }

        int[] count = new int[R + 2];

        for (int i = lo; i <= hi; i++){
            count[charAt(data[i], d) + 2] ++;
        }

        for (int i = 1; i < count.length; i++){
            count[i] += count[i-1];
        }

        for (int i = lo; i <= hi; i++){
            aux[count[charAt(data[i], d) + 1] ++] = data[i];
        }

        for (int i = lo; i <= hi; i++){
            data[i] = aux[i - lo];
        }

        for (int r = 0; r < R; r++){
            sort(data,lo + count[r], lo + count[r + 1] - 1, d+1);
        }
    }

    public static void insertionSort(String[] elements, int lo, int hi, int d){
        for (int i = lo + 1; i <= hi; i++){
            for (int j = i; j > lo && elements[j].charAt(d) < elements[j-1].charAt(d); j--){
                swap(elements, j, j-1);
            }
        }
    }

    private static void swap(String[] ele, int i, int j){
        String tmp = ele[i];
        ele[i] = ele[j];
        ele[j] = tmp;
    }

有些细节很有趣,比如在进行每一轮的木桶排序时,count频率计数是+2开始的,主要原因在于,count[1]这个位置被用来收集字符串末尾的(count[-1+2]),而在数据分类时,遇到末尾的情况直接把顺序照搬至aux即可。

练习:164. Maximum Gap

顺便来道练习题:

Given an unsorted array, find the maximum difference between the successive elements in its sorted form.

Try to solve it in linear time/space.

Return 0 if the array contains less than 2 elements.

You may assume all elements in the array are non-negative integers and fit in the 32-bit signed integer range.

非负整数,所以我们可以采用基数排序来,对数组进行排序,所需要的时间复杂度为 O(n)

代码如下:

public int maximumGap(int[] nums) {
        if (nums.length <= 1)
            return 0;
        radixSort(nums);
        int max = 0;
        for (int i = 1; i < nums.length; i++) {
            max = Math.max(nums[i] - nums[i - 1], max);
        }
        return max;
    }

    private static void radixSort(int[] nums) {
        int exp = 1;
        int radix = 10;

        //最大的一定是位数最长的!
        int max = 0;
        for (int i = 0; i < nums.length; i++) {
            max = Math.max(max, nums[i]);
        }

        while (max / exp != 0) {
            int[] count = new int[radix + 1];
            int[] aux = new int[nums.length];

            for (int i = 0; i < nums.length; i++) {
                count[nums[i] / exp % 10 + 1]++;
            }

            for (int i = 1; i < count.length; i++){
                count[i] += count[i-1];
            }

            for (int i = 0; i < nums.length; i++){
                aux[count[nums[i] / exp % 10]++] = nums[i];
            }

            for (int i = 0; i < nums.length; i++){
                nums[i] = aux[i];
            }

            exp *= 10;
        }
    }
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值