10大排序算法之八:基数排序【稳定】,复杂度小,不常用基数排序,除非面试官特殊申明

10大排序算法之八:基数排序【稳定】,复杂度小,不常用基数排序,除非面试官特殊申明

提示:整个算法界,一共有十大排序算法,每一个算法都要熟悉,才算是算法入门

算法界的十大排序算法分别是:
选择排序、冒泡排序、插入排序、堆排序、希尔排序、归并排序、快速排序、桶排序、计数排序,基数排序
(1)选择排序:10大排序算法之一:选择排序【不稳定】,一般不用选择排序的
(2)冒泡排序:10大排序算法之二:冒泡排序【稳定的】,但复杂度高,一般不用冒泡排序的
(3)插入排序:10大排序算法之三:插入排序【稳定的】,复杂度高,系统常在数据少时用插入排序。
(4)归并排序:10大排序算法之四:归并排序【稳定的】,复杂度中,系统常用归并排序
(5)快速排序:10大排序算法之五:快速排序【不稳定】,复杂度中,系统常用快速排序
(6)堆排序:10大排序算法之六:堆排序【不稳定】,复杂度中,不常用堆排序,堆结构更重要
(7)计数排序:10大排序算法之七:计数排序【稳定】,复杂度小,不常用计数排序,除非面试官特殊申明
在这里插入图片描述
如果你想进互联网大厂,至少这上面的其中最重要的8种,是必须熟练掌握的:
去掉希尔排序,希尔排序是一种改进的插入排序算法——所以只需要掌握插入排序
桶排序中最重要的就是计数排序和基数排序,都是桶的思想
在这里插入图片描述
根据算法复杂度低一点的,又稳定的
咱们可以最常用的算法实际上就四种:
插入排序(o(n^2))【当数据量小时,这个方法简单】【稳定】、
堆排序o(nlog(n))【不稳定】、
归并排序o(nlog(n))【稳定】,
快速排序o(nlog(n))(虽然快排不稳定,但是很多不需要稳定情况下,快排非常快)
因此,o(n)的桶排序很少用,除非面试官特别申明,否则都用比较排序。
归并排序是最常用的,复杂度低,而且稳定,达到了一个非常好的折中。


题目

请你用基数排序算法,将arr排序为升序


一、审题

示例:arr = 101 100 102 103
让其最终变为:arr= 100 101 102 103


啥时候用基数排序?

arr纯10进制,而且位数还多,比如103 20004这种
范围太大,咱之前说的计数排序不适合,否则计数桶count的下标要到20004,太大了,浪费

当基数是10时候,数就是十进制呗,所以本算法就叫基数排序

注意:我们讲完本文,基数排序之后,我们的系列排序算法,总共8篇,讲了6个基于比较的算法,2个不基于比较的算法,就到此结束了。如果每一个算法,你都理解透了,那么进大厂的算法,你就算入门了。

基础知识:求数组arr中的最大值,并返回这个最大值的位数

求数组arr= 100 101 103 100 102 中的最大值max,并返回这个最大值max的位数【】有几位?
在这里插入图片描述
手撕代码:

//复习:找arr最大值max,求max有几位,统计一下?
    public static int maxBits(int[] arr){
        int max = arr[0];
        for (int i = 1; i < arr.length; i++) {
            max = Math.max(max, arr[i]);
        }
        int k = 0;//统计max的位数
        while (max != 0){
            k++;
            max /= 10;//每次抹掉个位数
        }
        
        return k;
    }

比如103:
在这里插入图片描述
这个方便我们整基数排序,排个位,十位,百位等,要排多少种情况
有3位,那就是个十百,三种情况。


基础知识:求一个数x,它在d位上的数字j

比如:求103,在d=1位,d=2位,d=3位上的数字j分别是:3,0,1
我们说的d是从右到左的
在这里插入图片描述
公式很简单:
在这里插入图片描述

代码:

//复习:求一个数x,它在d位上的数字j
    public static int getDigitReview(int x, int d){
        int a = (int)Math.pow(10, d - 1);//分母
        int tmp = x / a;
        return tmp % 10;
    }

基数排序的思想:也是桶排序的思想

基数排序的核心思想
每一次,从个位排序,十位排序,然后百位排序……
最后arr整个已经排序好了。——核心思想就这么简单。
比如:
在这里插入图片描述
在代码上怎么实现呢?
比较烦,但是,要理解一番,然后明白其中的道理。
先单独说按照个位怎么排,然后十位,百位的排法也就很明确了。

每次排特定位【个十百,比如个位】时,咱们将个位上数字相同的,放到0–9的桶里面【count】
看下图绿色那的计数
在这里插入图片描述
看上面图,咱们用桶count来统计,个位上
0–9的数有几个?0为个位的2个,count[0]=2
1做个位的数就1个,count[1]=1
2做个位的数就1个,count[2]=1
3做个位的数就1个,count[3]=1
桶计数记好了

然后看count的累加和c,你count整体计数为2 1 1 1 ,他们的累加和自然是N,因为你遍历arr就N次,每次+1,没啥可说的
我们为什么要关注这个累加和c,因为这涉及到倒桶的问题!!!
此处,我们用help来接count中倒出来的数,help暂存,help是N长度的,和arr一样长。

——逻辑上这么倒的:
看count的i=0–9每一个位置,0为个位的数,有2个,将100和100倒给help
1为个位的数,1个,将101倒给help
2为个位的数,1个,将102倒给help
3为个位的数,1个,将103倒给help

虽然逻辑上这么倒可以,但是工程实现代码的时候,它不好实现

我们用一个巧妙的count的累加和来实现
这里,先跟着我理解一波:
count,记录的是特定位(比如个位),arr的数在特定位上是0–9的个数
c是count的累加和,记录的是,连同x在内,arr如果排序好,arr的左边一共有c个数。

——实际上是这么倒的:【看不懂这个流程的话,结合我的代码看】
令c为count的前缀累加和数组
在这里插入图片描述
刚刚我们说过,c最大就是arr的长度N=5,这代表啥呢?代表,arr排序好之后,总共arr就5个数。没啥疑惑对吧?

如果我们倒桶的时候,从arrr 边开始遍历,从右到左遍历,依次将arr每个数倒入help中
现在:你看看
(1)看arr中最右边的4位置,这个x=103该倒在help的哪个位置?
x=103在的个位数字 j=3

你看看是不是应该放在help的 c[j]-1 位置,即c[3]-1=5-1=4位置。
【注意:arr的103是最大的,怎么排序都应该放在最后一个位置,也就是4位置】
这个4,是通过c[j]-1求出来的,c又是count的累加和,
count记录的是arr中 个位数字 是0–9的 数 ,有几个?——这很好理解
而,累加和c是count从左往右加的过程,也就是说:

其实,c这个累加和,就代表arr按照个位排序后,arr中某个数x,算上x自己,左边一共有c个数
这个数x,它就应该放在c-1的位置
看图:103,排序好后,连同103在内,左边一共有5个数——着很好理解吧!
在这里插入图片描述
是不是?
想想:x=103这个数,排序好之后,连同x=103自己,左边一定有5个数,确实如此。
自然x就应该在4位置【c[j]-1位置,j是x的个位数数字3】
所以这里一定要理解c这个累加和的含义!!

(2)咱们看看arr的3位置的101应该倒到help中哪个位置?
现在x=101,它的个位数j=1,咱们看看c[j]=c[1]=3,意味着什么?意味着连同101在内,arr排序后,左边一共有3个数,
自然,x=101应该在help的c[j]-1=3-1=2位置,不是吗?
很直白!

(3)咱们再看arr的2位置的100,应该倒到help中哪个位置?
现在x=100,它的个位数j=0,咱们看看c[j]=c[0]=2,意味着什么?意味着连同100在内,arr排序后,左边一个用2个数,
自然,x=100应该在help的1位置,【c[j]-1位置】对吧?因为100连同在内就2个,不就是0 1位置中的1位置吗
这次操作中,c[j]需要-1,这样代表咱们100已经倒出去1个了!!!
所以c[0]=2-1=1,还剩一个100

(4)咱们再看arr的1位置的100,应该倒到help中哪个位置?
个位数是j=0
根据(3)来看,目前c[j]=1,所以就剩1个100,放哪呢?自然是0位置【c[j]-1位置】

(5)咱们再看arr的0位置的102,应该倒到help中哪个位置?
现在x=102,它的个位数j=2,咱们看看c[j]=c[2]=4,意味着什么?意味着连同100在内,arr排序后,左边一个用4个数,
上面我们看到了确实有100 100 101 ,加上102,的确是4个
所以x=102应该倒入c[j]-1=4-1=3位置

现在从arr的右边4位置开始,找x,直到0位置,每一个数都倒入help了
大家看看个位数的位置,是不是已经排序好了。在help
在这里插入图片描述
然后将help转移到arr中,算arr的个位排序好了。
——按照上面的规则,咱们需要把十位,百位,也都这么排序一次
这样才能让arr最终有序。

这就是基数排序的核心思想,关键在理解count和其累加和数组c的代表含义:
count,记录的是特定位(比如个位),arr的数在特定位上是0–9的个数
c是count的累加和,记录的是,连同x在内,arr如果排序好,左边一共有c个数。
这样的话,我们从arr右边,往左,倒arr中的x时,就知道x它应该去help的哪个位置。

因此,我们再回顾一下基数排序的核心思想
(1)找到arr中最大值,max,计算max的位数maxK;
(2)每一次针对一个特定的位d,从d=个位排序,d=十位排序,然后d=百位排序……
(3)【中间针对特定的位,比如d=个位,排序时,
利用count统计0–9的x的个数,然后利用累加和c,把arr右–左的x倒入help,
然后转移help到arr中来】
(4)最后arr整个已经排序好了。
——核心思想就这么简单。
手撕基数排序的代码:

//基数排序,从L--R上做基数排序
    public static void radixSortReview(int[] arr, int L, int R){
        int N = R - L + 1;//一共这么长
        int[] help = new int[N];//每次排序每个位(个十百)都用help来转移

        int maxK = maxBits(arr);//arr最大值max的位数k

        int i = 0;//索引arr用
        int t = 0;//索引help用
        //下面从个位,十位,百位开始排序
        for (int d = 1; d <= maxK; d++) {
            //针对特定的d位,比如个位,十位,百位,下面的统计,装桶,倒桶,转移,都一样
            int[] count = new int[10];//以10进制数的基数,我们只统计0--9这些数字
            //去arr中统计d这个位,比如个位
            for (i = L; i <= R; i++) {
                //先拿到x的d位上的数j,比如个位数
                int j = getDigitReview(arr[i], d);
                count[j]++;//让桶,统计arr在d位上的j这个数字,有多少个?
            }
            //根据count,咱们整一个累加和数组,为了节约空间,让count复用一下
            for (i = 1; i < 10; i++) {
                count[i] += count[i - 1];
            }
            //此时count的含义,就变成了,如果arr排序好,连同x极其左边一共有多少个数

            //倒桶,让arr从右往左,把各个x倒入help
            for (i = R; i >= L; i--) {
                //先拿到d这个位上的数j
                int j = getDigitReview(arr[i], d);
                //然后看它该去help中的哪个位置,自然是c[j]-1位置
                help[count[j] - 1] = arr[i];//现在我们讨论的是arr[i],它就是x
                //你既然count[j]-1已经放了一个,那下次遇到相同的x,应该往左边放
                count[j]--;//此时这个词频统计,倒出去1个,自然要--
            }
            //倒完倒help后,自然help已经按照d位(比如个位)排序好了,需要转移到arr中
            //以便d+1之后,排d这个位(比如百位)
            for (i = L, t = 0; i <= R; i++, t++) {
                arr[i] = help[t];//从help的0开始转移到arr的L上。
            }
        }
        //等所有的位都这么干了之后,自然arr就井然有序了。
    }

测试一波:

//对数器之构建随机数组
    public static int[] createArray(int arrSize, int maxValue){
        int[] arr = new int[arrSize];
        for (int i = 0; i < arrSize; i++) {
            arr[i] = (int)(maxValue * Math.random());//0-N-1的随机数
        }
        return arr;
    }

    public static void checker(){
        //生成检验数组
        int[] arr = createArray(10000,750);
        int[] arr2 = new int[arr.length];//赋值同样一个数组arr
        for (int i = 0; i < arr.length; i++) {
            arr2[i] = arr[i];//copy即可
        }
        int[] arr3 = new int[arr.length];//赋值同样一个数组arr
        for (int i = 0; i < arr.length; i++) {
            arr3[i] = arr[i];//copy即可
        }

        //绝对的正确方法——暴力方法,或系统函数,操作arr
        Arrays.sort(arr);
        //优化方法,操作arr2
        radixSort(arr2);
        //复习优化方法,操作arr3
        radixSortReview(arr3, 0, arr3.length - 1);//从0--N-1范围内排

        //然后两个数组对位校验
        boolean isSame = true;
        for (int i = 0; i < arr.length; i++) {
            if(arr[i] != arr2[i]) {
                isSame = false;
                break;//只要有一个不等,不必继续判断了
            }
        }
        System.out.println(isSame == false ? "oops,wrong!" : "right!");
        isSame = true;
        for (int i = 0; i < arr3.length; i++) {
            if(arr[i] != arr3[i]) {
                isSame = false;
                break;//只要有一个不等,不必继续判断了
            }
        }
        System.out.println(isSame == false ? "oops,wrong!" : "right!");
    }

    public static void main(String[] args) {
        checker();
    }

结果:

right!
right!

总结

提示:重要经验:

1)基数排序的核心思想,就是利用特定位(比如个位)统计0–9的个数,然后用累加和数组c倒桶入help,最后转移到arr,完成排序
2)难,是难了点,但是这种桶排序的思想,就是这样,烦,面试官没有特殊要求,不要玩桶排序(基数排序和计数排序),我们只玩比较的排序。

  • 3
    点赞
  • 8
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

冰露可乐

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值