基数排序详解

基数排序详解


摘要:基数排序是一种代码量比较复杂,但是时间复杂度比较低的排序,其时间复杂度和数组规模以及使用到的桶的个数相关,基数排序和计数排序、桶排序有很大的相关性,我们在学习排序的时候一般会成套的学习这三种排序,基数排序是这三种使用到“桶”的排序中时间复杂度比较高的一个,但是它的最好情况与最坏情况也比较稳定。接下来我们学习基数排序

1.基数排序算法图解

​ 基数排序需要根据数组中所有元素的具体情况构建一个桶,并不断地重复将数组中元素放入桶中并不断拿出来的这个过程,根据其内部相关机制,最后这个数组就会变得有序,接下来我们使用图解的方式对这个算法过程进行深入理解。

​ 基数排序的应用场景一般都是数组中元素存在多位数,否则其将发生退化的现象,这个问题在我们了解了基数排序算法之后,就会有更深刻的理解,首先我们得到了一个待排序的数组:

​ 现在我们创建“”,什么是桶?桶就是一个根据某种分类规则分类存放数据元素的结构,也就是根据事物的某种特征对一堆混在一起的事物进行分类,并根据分类规则及结果将元素们分别放在相互隔离的存储空间里,这就叫桶。打个比方,我们现在有篮球,足球,网球,棒球,乒乓球,排球几种运动器材,它们现在混在一起堆成一堆,我们拿来了六个大桶,根据它们外形的不同,或者直接就是根据它们球种类的不同,分门别类的放进这六个大桶中,让这些球同类的放在一个桶中,不同类型的球被自己的桶相互隔离,这就是排序时用到的桶的作用。简而言之:桶就是分类存放某种数据的一个数据结构。接下来我们看看排序用到的桶具体是什么样子的,这样能够进一步加深理解:

​ 在这里我们使用到的桶是这样的,为什么是这样呢?基数排序中的桶的分类规则又是什么呢?且听我细细道来:在基数排序中,数字会被多次放入到桶中,每次数字被放入到桶中时,都是按照其某一个数位进行分类的,简而言之,基数排序中的桶分类数据的规则是按照数字中某一位的值为多少进行分类的,也就是按照数字们的某个位如个位,十位,百位...上的具体数字进行分类,数字相同的将被分在一个桶中,如:

147
68
6
1376
167
139
27
这个桶会按照这些数字的个位,十位,百位进行分类,它是按照数字们同量级位上的数字进行分类的,在这里,这个桶会将这些数组按照个位分成:{6,1376},{147,167,27},{68},{139}四组。

​ 那么这个桶的具体构造又怎么解释呢?按照这里的桶的分类规则,也就是按照十位数字的每一个位上边的数字进行分类这个规则,我们可以推断出,这个桶的数量,一定超不过10,这是因为在十进制中,每一个数位上的值只有0~9这10个数字,因此在基数排序中的所有桶的列数,都是10,而行数,我们则需要考虑到数组元素中的值的个数,如果我们桶结构的行数只有5行,数组元素有10个,那么桶结构能够存储的数字就是5*10,也就是50个,看上去存储数组这10个值完全是绰绰有余的,然而,当数组中的这十个值的个位或者十位(反正是有这么一位)都相同的话,那么在某一个时刻,这十个值一定会聚集到这一排桶中的其中一个桶中去,而每一个桶的上限就是行数,也就是5,那么这时就会产生栈溢出,因此,我们为了保险起见,将桶的行数要设定为数组中的元素值的个数,以防出现这种情况,因此在这里我们的数组又十个值,这个桶结构就是有十行十列的,十列代表桶结构有十个桶,十行代表每个桶的容量都是10。

​ 我们在基数排序的过程中,要不断地将数组中的元素放进桶中并拿出来,我们将元素放进桶中的话,那必须要有一个变量来记住每个桶中已经放入元素的数量,这样我们在往外取的时候才能正常取出,否则就会两眼一抹黑,取不到正确的数量,因此我们还需要额外的设计一个新的数组结构,来记录每个桶中的元素数量,因为有十个桶,所以我们要声明一个长度为十的数组,这个数组中的每一位都会和每一个桶相对应,这个对应关系时基于数学原理的,因为桶的列下标就等于桶中存放的位数值,并等于记录数组的下标,它们三个是完全自然对应的。每当一个桶中新加入了一个值,记录数组的相应元素就要自增1,如图:

​ 至此我们讲解了基数排序中通的基本定义与分类原则,至于是如何按照位数分类的,具体怎样操作,我们即将以一个从小到大的基数排序例子来详细解说基数排序的过程:

​ 我们首先要根据个位上的数字,把这个数组中的元素全部放入到桶结构中,进而进行一次分类,首先我们遍历整个数组,将数组中的值放入桶中,在放入的时候要根据每个数字的个位进行放置:

​ 首先我们获取到了第一个元素,并且知道了它的个位数字为4,因此我们将其放到下标为4的桶中去,并将下标为4的桶的长度记录变量加1,这个长度记录变量的另一层含义也是这个桶的头指针,如当前这个桶的长度记录变量为1,既说明现在桶中有一个元素,也让其指向了桶中下一个为空的位置,下一次放入时直接放入这个位置再让它自增1,表示当前有两个元素,并且它又指向了下一个新的空位置,这里的数学思想非常好。现在我们跳过整个放数字的繁琐过程,直接展现数字全都放好之后的状态:

​ 如图所示,当前是所有数组元素全部放入桶中后的桶的状态,现在我们需要按照顺序,将桶中元素依次再放入数组中,这个顺序当然就是根据下标(注意下标和数位值是相等的)从小到大的顺序放回,在放回过程中,我们要遍历桶的记录变量数组,也就是蓝色填充的数组,我们遍历这个数组,如果发现其值不等于0,那么说明它对应的桶里是存在数字的,我们就要将这个数字取出来,放到数组中,现在我们详细进行这个过程:

​ 首先我们检测到了1位置,里边只有一个数字21,我们将其放到数组的0下标出,之后继续遍历,并将原数组的位置指针向后移动一个单位。

​ 之后是个位为2的数字,我们也将其放入到了原数组中,之后我们又检测到了个位为3的位置,也存在一个数值,我们将其取回并放入到原数组中:

​ 之后我们有检测到了个位为4的数字,有两个,我们依次将其取回,如图:

​ 之后我们又检测到了个位为6的数字,我们将其取出并放在原数组中:

​ 之后我们又检测到了个位为7的数字三个,我们依次将它们取出并放在原数组中:

​ 最后我们发现个位为9的数字也有一个,我们将其取出并放到数组中去,至此第一轮循环宣告完成:

​ 现在我们按照所有元素的个位进行了第一轮排序,可见现在的数组仍然是一个无序状态,但是现在我们可以发现它们的个位上的数字实际上已经按照从小到大的顺序排好了。之后,我们还要再进行一轮排序,此时我们就不会再需要使用个位上的数字进行排序了,而是使用十位上的数字,那么问题来了,既然我们将要使用十位上的数字,那么个位数怎么办呢?在代码中,我们使用的是除以10^n之后再对10求余的方式获取每一位的,例如:

对于9和98,我们在取得它的个位数时,是先除以1*10^0之后,在对10求余,这样一来9通过运算可以得到9,而98则会得到8
当我们取得它的十位数时,我们要先除以1*10^1之后,在对10求余,此时9在除1*10^1的时候,就已经变成了0,之后再对10求余的结果还是0
而98则不同,它在除以1*10^1之后,变成了9,再对10求余,结果就成了9,这样一来就成功取到了98的十位上的数字9

​ 因此我们在第二轮循环的时候,之前的个位数会统统被按照之前的顺序被依次放置到下标为0的桶上去,也就是十位数为0的桶,因为第一轮循环是按照个位数字进行的排序,因此实际上在第一轮循环中个位数已经被排好序了,因此这样被依次放置到下标为0的桶上去,实际上是一次对个位数的分类与整合,让个位数都进到了第一个桶中去,这样一来在下一次的桶中取数过程中,我们就会首先按照顺序的从第一个桶中取到所有的个位数并放到最前边,这样实际上就是已经完成了个位数的排序,而其他的十位数百位数则将继续像第一轮循环中那样被放在之后的桶中,并继续按照十位上的数字进行排序,现在我们直接展现第二轮循环的结果:

​ 如图所示这是我们按照十位上的数字放置到桶中的状态,之前的个位数因为十位上数字被计算为0,而依次被排布到了下标为0的桶上去,我们现在发现这个桶上的各位数字其实已经被排好序了,之后我们按照之前的规则依次将桶中的数字放回到数组中去,现在我们直接展示这个直接结果:

​ 我们现在可以发现除了个位数已经按照顺序的被排布在了最前边,其余数字也是按照十位数从小到大的顺序被排布好了,与此同时我们发现了一个神奇的规律,这是百位数虽然穿插在不符合位置的地方,但是整个数组中的十位数的大小顺序,实际上已经符合从小到大的规律了,如果你细心一点可以发现,早在第一轮循环结束后,个位数的从前到后顺序就已经是正确的了,尽管十位数和百位数穿插在它们期间,但是在第二轮循环中,它们就会因为将要研究十位,而它们的十位上是0这一共同点,被有序的收集到了下标为0的桶中去,进而连接在了一起,成为了一个有序的子数组。

​ 这时这个待遇将轮到十位数享受了,现在我们即将开启第三轮循环,第三轮循环我们研究的是百位上的数字,当我们研究百位上的数字的时候,根据上文中提到的位数获取算法,我们就会发现,在第三轮循环中,十位数字也会被有序的存放在下标为0的桶中,因为这个存放顺序是从前到后的,因此肯定是个位数字在前边,十位数字在后边,在第三轮循环中放入桶中操作结束之后,我们发现当前桶的结构是这样的:

​ 我们发现个位数和十位数已经被有序的存放到下标为零的桶中去了,在下标为1的桶中有157,下标为4的桶中有457,接下来我们进行取数放入桶中的操作,在按照顺序取数并放回桶中之后,我们发现了现在数组变成了这样:

​ 这个数组已经排好序了。

2.基数排序的解读

​ 在我们进行排序时,肯定是个位数小于十位数小于百位数,基数排序的思想就是先对个位数进行排序,然后对十位数进行排序,最后对百位数进行排序。整体思想上是使用了桶的思想。整个桶的思想,我们可以理解为,排序+收集。先排个位,再排十位,再排百位...总体这样下来,一轮轮就有序了。

​ 细化来讲,在第一轮基数排序的时候,我们会先研究所有数字的个位,并按照个位上的数组排好序,在经过第一轮的排序之后,所有数字便已经按照个位排好序了,而之后我们再按照十位数进行排序,而这时数组中的拥有十位数字的数肯定是比只拥有个位数字的个位数大,不管它们怎么排,我们都知道它们必定会比各位数字大,因此这时我们完全可以心安理得的将个位数按照第一轮的顺序排在数组的最前边,然后在后边对拥有十位数值的大数们进行排序,同理在研究百位的时候,我们也会这样对待之前的仅有十位和个位的十位数们,这样依次排序,我们就能将这些数字排好序。

3.代码

    public static void ridix(int[] arr) {// 基数排序
        int max = arr[0];
        for (int i = 1; i < arr.length; i++) {
            if (max < arr[i])
                max = arr[i];
        }
        int maxSize = (max + "").length();
        for (int i = 1, n = 1; i <= maxSize; i++, n *= 10) {
            int[][] bucket = new int[10][arr.length];
            int[] bucketElement = new int[10];
            for (int j = 0; j < arr.length; j++) {
                int element = arr[j] / n % 10;
                bucket[element][bucketElement[element]] = arr[j];
                bucketElement[element]++;
            }

            for (int x = 0, index = 0; x < bucketElement.length; x++) {
                if (bucketElement[x] != 0) {
                    for (int z = 0; z < bucketElement[x]; z++) {
                        arr[index] = bucket[x][z];
                        index++;
                    }
                }
                bucketElement[x] = 0;
            }

        }
        System.out.println(Arrays.toString(arr));
    }

posted @ 2022-03-25 11:21  云杉木屋  阅读(1986)  评论(0编辑  收藏  举报