算法练习(5)-计数排序法及优化

日常开发中,会遇到一些特定的排序场景:“待排序的值”范围很明细,比如:基金的星级排名,客服的好评星级排名,一般星级排名也就从1星到5星。这种情况下,有一个经典的“下标计数排序法”,可以用O(n)的时间复杂度完成排序:

static void sort1(int[] arr) {
        if (arr == null || arr.length < 2) {
            return;
        }
        int[] rates = new int[5];
        //统计每个元素出现的次数
        for (int i = 0; i < arr.length; i++) {
            rates[arr[i] - 1]++;
        }

        //辅助调试用
        System.out.println(Arrays.toString(rates));

        //根据计数结果,重新填充到原数组
        int cur = 0;
        for (int i = 0; i < rates.length; i++) {
            //如果该位置的统计值>0,说明出现了多个,依次填充即可
            //加上cur<arr.length是用于优化所有元素都是最小值的情况,后面的位置就不用看了
            for (int j = 0; j < rates[i] && cur < arr.length; j++) {
                arr[cur++] = i + 1;
            }
        }
    }

    public static void main(String[] args) {
        int[] arr = new int[]{5, 5, 4, 1, 1, 1};
        System.out.println(Arrays.toString(arr));
        sort1(arr);
        System.out.println(Arrays.toString(arr));
    }

输出:

[5, 5, 4, 1, 1, 1]
[3, 0, 0, 1, 2]
[1, 1, 1, 4, 5, 5]

但这是一个不稳定的排序算法,如果想改进稳定的算法,有一种比较巧妙的做法:

static int[] sort2(int[] arr) {
    if (arr == null || arr.length < 2) {
        return arr;
    }
    int[] buckets = new int[5];
    for (int i = 0; i < arr.length; i++) {
        buckets[arr[i] - 1]++;
    }
    //辅助调试用
    System.out.println("\tdebug=> " + Arrays.toString(buckets));
    //处理成"前缀和"
    for (int i = 1; i < buckets.length; i++) {
        buckets[i] += buckets[i - 1];
    }
    //辅助调试用
    System.out.println("\tdebug=> " + Arrays.toString(buckets));

    //根据计数结果,生成有序数组
    int[] result = new int[arr.length];
    for (int i = arr.length - 1; i >= 0; i--) {
        int t = arr[i];
        result[buckets[t - 1] - 1] = t;
        buckets[t - 1]--;
    }
    //辅助调试用
    System.out.println("\tdebug=> " + Arrays.toString(buckets));
    return result;
}

 写段测试,跑一下:

    int[] arr = new int[]{1, 4, 5, 2, 3, 4, 5, 5};
    System.out.println(Arrays.toString(arr));
    sort1(arr);
    System.out.println(Arrays.toString(arr));

    System.out.println("------------------------");
    arr = new int[]{1, 4, 5, 2, 3, 4, 5, 5};
    System.out.println(Arrays.toString(arr));
    int[] result = sort2(arr);
    System.out.println(Arrays.toString(result));

输出 :

[1, 4, 5, 2, 3, 4, 5, 5]
	debug=> [1, 1, 1, 2, 3]
[1, 2, 3, 4, 4, 5, 5, 5]
------------------------
[1, 4, 5, 2, 3, 4, 5, 5]
	debug=> [1, 1, 1, 2, 3]
	debug=> [1, 2, 3, 5, 8]
	debug=> [0, 1, 2, 3, 5]
[1, 2, 3, 4, 4, 5, 5, 5]

方法2,为啥能保证稳定呢? 关于在于"前缀和"这一步处理, 相当于把"计数+位置"这二种信息合成在一起了, 可能有点难理解. 

debug=> [1, 1, 1, 2, 3]
debug=> [1, 2, 3, 5, 8]

输出的这2行调试信息里:
第1行的[1, 1, 1, 2, 3]表示1出现1次, 2出现1次, 3次出1次, 4次出2次, 5出现3次. 然后把每1项,处理成前2项求和后, 变成
第2行的[1, 2, 3, 5, 8]表示<=1的元素个数为1, <=2的元素个数为2,  <=3的元素个数为3, <=4的元素个数为5, <=5的元素个数为8,相同的元素越在后面出现,这样累加的值就越大,所以相当于变相的用计数的大小,蕴含了元素的相对位置信息。建议初次接触此解法的同学,多断点调试几次,观察每1步各变量的情况。

 

这个方法看似巧妙,但个人觉得有点鸡肋,原因二点:

1、需要引入1个与原数组规模相同的额外数组存放最后的有序结果,额外空间又扩大了一倍

2、思路不易于理解 

既然都是空间换时间,还不如搞简单点,在每个槽位放一个list,相同元素依次放进该槽位的list(这样用list.size代替计数),由于list天然保证元素放入的顺序不变,所以能保证最终排序结果的稳定性:

static void sort2_2(int[] arr) {
    if (arr == null || arr.length < 2) {
        return;
    }
    List<List<Integer>> buckets = new ArrayList<>(5);
    for (int i = 0; i < 5; i++) {
        buckets.add(new ArrayList<>(arr.length));
    }

    //每个元素放入指定的槽位
    for (int i = 0; i < arr.length; i++) {
        buckets.get(arr[i] - 1).add(arr[i]);
    }

    //辅助调试用
    System.out.println("\tdebug=> " + buckets);

    //根据计数结果,重新填充到原数组
    int cur = 0;
    for (int i = 0; i < buckets.size(); i++) {
        //如果该位置的list非空,说明出现了多个,依次填充即可
        //加上cur<arr.length是用于优化所有元素都是最小值的情况,后面的位置就不用看了
        for (int j = 0; j < buckets.get(i).size() && cur < arr.length; j++) {
            arr[cur++] = buckets.get(i).get(j);
        }
    }
}

与刚才看似精妙的方法结果一样,但理解起来容易多了。跑测试的话,输出结果如下:

[1, 4, 5, 2, 3, 4, 5, 5]
	debug=> [[1], [2], [3], [4, 4], [5, 5, 5]]
[1, 2, 3, 4, 4, 5, 5, 5]

  

简单int[]类型的数组, 可能看不出稳定排序与非稳定排序的区别, 我们可以把数据类型弄复杂点:

[客服A:1, 客服B:4, 客服C:5, 客服D:2, 客服E:3, 客服F:4, 客服G:5, 客服H:5]
static class Score {
    public String name;
    public int val;

    public Score(String name, int val) {
        this.name = name;
        this.val = val;
    }

    @Override
    public String toString() {
        return this.name + ":" + this.val;
    }
}

按刚才的思路,要实现稳定的计数排序, 有2种写法:

static void sort3(Score[] arr) {
    if (arr == null || arr.length < 2) {
        return;
    }
    List<List<Score>> buckets = new ArrayList<>(5);
    for (int i = 0; i < 5; i++) {
        buckets.add(new ArrayList<>());
    }
    for (int i = 0; i < arr.length; i++) {
        buckets.get(arr[i].val - 1).add(arr[i]);
    }
    //辅助调试用
    System.out.println("\tdebug=> " + buckets);

    //根据计数结果,重新填充到原数组
    int cur = 0;
    for (int i = 0; i < buckets.size(); i++) {
        //如果该位置的统计值>0,说明出现了多个,依次填充即可
        //加上cur<arr.length是用于优化所有元素都是最小值的情况,后面的位置就不用看了
        for (int j = 0; j < buckets.get(i).size() && cur < arr.length; j++) {
            arr[cur++] = buckets.get(i).get(j);
        }
    }
}


static Score[] sort4(Score[] arr) {
    if (arr == null || arr.length < 2) {
        return arr;
    }
    int[] buckets = new int[5];
    for (int i = 0; i < arr.length; i++) {
        buckets[arr[i].val - 1]++;
    }
    //辅助调试用
    System.out.println("\tdebug=> " + Arrays.toString(buckets));

    //处理成前缀合
    for (int i = 1; i < buckets.length; i++) {
        buckets[i] += buckets[i - 1];
    }
    //辅助调试用
    System.out.println("\tdebug=> " + Arrays.toString(buckets));

    //根据计数结果,生成有序数组
    Score[] result = new Score[arr.length];
    for (int i = arr.length - 1; i >= 0; i--) {
        Score t = arr[i];
        result[buckets[t.val - 1] - 1] = t;
        buckets[t.val - 1]--;
    }
    //辅助调试用
    System.out.println("\tdebug=> " + Arrays.toString(buckets));
    return result;
}

测试代码:

public static void main(String[] args) {
    Score[] arr3 = new Score[]{
            new Score("客服A", 1),
            new Score("客服B", 4),
            new Score("客服C", 5),
            new Score("客服D", 2),
            new Score("客服E", 3),
            new Score("客服F", 4),
            new Score("客服G", 5),
            new Score("客服H", 5)
    };
    System.out.println(Arrays.toString(arr3));
    sort3(arr3);
    System.out.println(Arrays.toString(arr3));
    System.out.println("------------------------");

    Score[] arr4 = new Score[]{
            new Score("客服A", 1),
            new Score("客服B", 4),
            new Score("客服C", 5),
            new Score("客服D", 2),
            new Score("客服E", 3),
            new Score("客服F", 4),
            new Score("客服G", 5),
            new Score("客服H", 5)
    };
    System.out.println(Arrays.toString(arr4));
    Score[] result = sort4(arr4);
    System.out.println(Arrays.toString(result));
}

输出:

[客服A:1, 客服B:4, 客服C:5, 客服D:2, 客服E:3, 客服F:4, 客服G:5, 客服H:5]
	debug=> [[客服A:1], [客服D:2], [客服E:3], [客服B:4, 客服F:4], [客服C:5, 客服G:5, 客服H:5]]
[客服A:1, 客服D:2, 客服E:3, 客服B:4, 客服F:4, 客服C:5, 客服G:5, 客服H:5]
------------------------
[客服A:1, 客服B:4, 客服C:5, 客服D:2, 客服E:3, 客服F:4, 客服G:5, 客服H:5]
	debug=> [1, 1, 1, 2, 3]
	debug=> [1, 2, 3, 5, 8]
	debug=> [0, 1, 2, 3, 5]
[客服A:1, 客服D:2, 客服E:3, 客服B:4, 客服F:4, 客服C:5, 客服G:5, 客服H:5]
posted @ 2021-03-25 20:04  菩提树下的杨过  阅读(197)  评论(0编辑  收藏  举报