排序算法之 计数排序 桶排序 基数排序

1.计数排序:Counting Sorthtml

计数排序是一个非基于比较的排序算法,该算法于1954年由 Harold H. Seward 提出,它的优点在于在对于较小范围内的整数排序。它的复杂度为Ο(n+k)(其中k是待排序数的最大值),快于任何比较排序算法,缺点就是很是消耗空间。很明显,若是并且当O(k)>O(n*log(n))的时候其效率反而不如基于比较的排序,好比堆排序和归并排序和快速排序。算法

算法原理: 
基本思想是对于给定的输入序列中的每个元素x,肯定该序列中值小于x的元素的个数。一旦有了这个信息,就能够将x直接存放到最终的输出序列的正确位置上。例如,若是输入序列中只有17个元素的值小于x的值,则x能够直接存放在输出序列的第18个位置上。固然,若是有多个元素具备相同的值时,咱们不能将这些元素放在输出序列的同一个位置上,在代码中做适当的修改便可。数组

用待排序的数做为计数数组的下标,统计每一个数字的个数。而后依次输出便可获得有序序列。ui

算法步骤: 
(1)找出待排序的数组中最大的元素; 
(2)统计数组中每一个值为i的元素出现的次数,存入数组C的第i项; 
(3)对全部的计数累加(从C中的第一个元素开始,每一项和前一项相加); 
(4)反向填充目标数组:将每一个元素i放在新数组的第C(i)项,每放一个元素就将C(i)减去1。code

时间复杂度:Ο(n+k)。htm

空间复杂度:Ο(k)。blog

要求:待排序数中最大数值不能太大。最大值肯定而且不大,必须是正整数。 排序

稳定性:稳定。递归

 

计数排序须要占用大量空间,它仅适用于数据比较集中的状况。好比 [0~100],[10000~19999] 这样的数据。索引

计数排序的基本思想是:对每个输入的元素arr[i],肯定小于 arr[i] 的元素个数
因此能够直接把 arr[i] 放到它输出数组中的位置上。假设有5个数小于 arr[i],因此 arr[i] 应该放在数组的第6个位置上。

 

计数排序用到一个额外的计数数组C,根据数组C来将原数组A中的元素排到正确的位置。

  通俗地理解,例若有10个年龄不一样的人,假如统计出有8我的的年龄不比小明大(即小于等于小明的年龄,这里也包括了小明),那么小明的年龄就排在第8位,经过这种思想能够肯定每一个人的位置,也就排好了序。固然,年龄同样时须要特殊处理(保证稳定性):经过反向填充目标数组,填充完毕后将对应的数字统计递减,能够确保计数排序的稳定性。

 

计数排序属于线性排序,它的时间复杂度远远大于经常使用的比较排序。(计数是O(n),而比较排序不会超过O(nlog2nJ))。

其实计数排序大部分很好理解的,惟一理解起来很蛋疼的是为了保证算法稳定性而作的数据累加,你们听我说说就知道了:

一、首先,先取出要排序数组的最大值,假如咱们的数组是int[] arrayData = { 2, 4, 1, 5, 6, 7, 4, 65, 42 };,那么最大值就是65.(代码17-21行就是在查找最大值)

二、而后建立一个计数数组,计数数组的长度就是咱们的待排序数组长度+1。即65+1=66。计数数组的做用就是用来存储待排序数组中,数字出现的频次。 例如,4出现了两次,那么计数数组arrayCount[4]=2。 OK,如今应该明白为何计数数组长度为何是66而不是65了吧? 由于为了存储0

    而后再建立一个存储返回结果的数组,数组长度与咱们的原始数据长度是相同的。(24和26行)

三、进行计数(代码29至31行)

四、将计数数组进行数量累计,即arrayCount[i]+=arrayCount[i-1](代码35行至代码37行)。 

   目的是为了数据的稳定性, 这块我其实看了许久才看懂的…再次证实个人资质真的不好劲。 我来尽力解释一下:

     其实这个与后边那步结合着看理解起来应该更容易些。

     例如咱们计数数组分别是 1 2 1 2 1 的话,那么就表明0出现了一次,1出现了两次,2出现了一次,3出现了两次。

  这个是很容易理解的。 那咱们再换个角度来看这个问题。

  咱们能够根据这个计数数组获得每一个数字出现的索引位置,即数字0出现的位置是索引0,数字1出现的问题是索引1,2;数字2出现的位置是索引3,数字4出现的位置是索引4,5。。。。

  OK,你们能够看到,这个索引位置是累加的,因此咱们须要arrayCount[i]+=arrayCount[i-1]来存储每一个数字的索引最大值。 这样为了后边的输出

五、最后,把原始数据从后往前输出;而后每一个数字都能找到计数器的最后实现索引。  而后将数字存储在实际索引的结果数组中。 而后计数数组的索引--, 结果就出来了。

PS:计数排序实际上是特别吃内存的

 

时间复杂度:

O(n+k)  

请对照下方代码:由于有n的循环,也有k的循环,因此时间复杂度是n+k

空间复杂度:

O(n+k)  

请对照下方代码:须要一个k+1长度的计数数组,须要一个n长度的结果数组,因此空间复杂度是n+k

public static void main(String[] args) {

        int[] arrayData = { 2, 3, 1, 5, 6, 7, 4, 65, 42 };

        int[] arrayResult = CountintSort(arrayData);

    }
public static int[] CountintSort(int[] arrayData) {
    int maxNum = 0;
    // 取出最大值
    for (int i : arrayData) {
        if (i > maxNum) {
            maxNum = i;
        }
    }

    // 计数数组
    int[] arrayCount = new int[maxNum + 1];
    // 结果数组
    int[] arrayResult = new int[arrayData.length];

    // 开始计数
    for (int i : arrayData) {
        arrayCount[i]++;
    }

    // 对于计数数组进行 i=i+(i-1)
    // 目的是为了保证数据的稳定性
    for (int i = 1; i < arrayCount.length; i++) {
        arrayCount[i] = arrayCount[i] + arrayCount[i - 1];
    }

    for (int i = arrayData.length - 1; i >= 0; i--) {
        arrayResult[arrayCount[arrayData[i]] - 1] = arrayData[i];
        arrayCount[arrayData[i]]--;
    }

    return arrayResult;

}

 

算法分析

        主要思想:根据array数组元素的值进行排序,而后统计大于某元素的元素个数,最后就能够获得某元素的合适位置;好比:array[4] = 9;统计下小于array[4]的元素个数为:8;因此array[4] = 9 应该放在元素的第8个位置;

        主要步骤:

       一、根据array数组,把相应的元素值对应到tmpArray的位置上;

     二、而后根据tmpArray数组元素进行统计大于array数组各个元素的个数;

     三、最后根据上一步统计到的元素,为array元素找到合适的位置,暂时存放到tmp数组中;

 

        以下图所示:array 是待排序的数组;tmpArray 是至关于桶的概念; tmp 是临时数组,保存array排好序的数组;

        

        注意:计数排序对输入元素有严格要求,由于array元素值被用来看成tmpArray数组的下标,因此若是array的元素值为100的话,那么tmpArray数组就要申请101(包括0,也就是 mix - min + 1)。

 

 

时间复杂度

        时间复杂度能够很好的看出了就是:O( n );

 

空间复杂度

        空间复杂度也能够很好的看出来:O( n );

 

总结

        计数排序的时间复杂度和空间复杂度都是很是有效的,可是该算法对输入的元素有限制要求,因此并非全部的排序都使用该算法;最好的是0~9之间的数值差不会很大的数据元素间比较;有人会说这个没多大用,可是在后面的基数排序中会看到,这能够算是基数排序中的一个基础;

 

 

 

 

基数排序(Radix Sort)

 

  基数排序的发明能够追溯到1887年赫尔曼·何乐礼在打孔卡片制表机上的贡献。它是这样实现的:将全部待比较正整数统一为一样的数位长度,数位较短的数前面补零。而后,从最低位开始进行基数为10的计数排序,一直到最高位计数排序完后,数列就变成一个有序序列(利用了计数排序的稳定性)。

        基数排序的时间复杂度是O(n * dn),其中n是待排序元素个数,dn是数字位数。这个时间复杂度不必定优于O(n log n),dn的大小取决于数字位的选择(好比比特位数),和待排序数据所属数据类型的全集的大小;dn决定了进行多少轮处理,而n是每轮处理的操做数目。

  若是考虑和比较排序进行对照,基数排序的形式复杂度虽然不必定更小,但因为不进行比较,所以其基本操做的代价较小,并且若是适当的选择基数,dn通常不大于log n,因此基数排序通常要快过基于比较的排序,好比快速排序。因为整数也能够表达字符串(好比名字或日期)和特定格式的浮点数,因此基数排序并非只能用于整数排序。

 

基数排序已经再也不是一种常规的排序方式,它更多地像一种排序方法的应用,基数排序必须依赖于另外的排序方法。基数排序的整体思路就是将待排序数据拆分红多个关键字进行排序,也就是说,基数排序的实质是多关键字排序。

若是按照习惯思惟,会先比较百位,百位大的数据大,百位相同的再比较十位,十位大的数据大;最后再比较个位。人得习惯思惟是最高位优先方式。但一旦这样,当开始比较十位时,程序还须要判断它们的百位是否相同--这就认为地增长了难度,计算机一般会选择最低位优先法。

基数排序方法对任一子关键字排序时必须借助于另外一种排序方法,并且这种排序方法必须是稳定的。对于多关键字拆分出来的子关键字,它们必定位于0-9这个可枚举的范围内,这个范围不大,所以用桶式排序效率很是好。对于多关键字排序来讲,程序将待排数据拆分红多个子关键字后,对子关键字排序既可使用桶式排序,也可使用任何一种稳定的排序方法。

 

基数排序(radix sort)属于“分配式排序”(distribution sort),又称“桶子法”(bucket sort)或bin sort,顾名思义,它是透过键值的部份资讯,将要排序的元素分配至某些“桶”中,藉以达到排序的做用,基数排序法是属于稳定性的排序,其时间复杂度为O (nlog(r)m),其中r为所采起的基数,而m为堆数,在某些时候,基数排序法的效率高于其它的稳定性排序法。

 

算法分析

 

     主要思想:

        基数排序的实现虽然有不少,可是基本思想就是把元素从个位排好序,而后再从十位排好序,,,,一直到元素中最大数的最高位排好序,那么整个元素就排好序了。

                好比:2,22,31,1221,90,85,105

        个位排序:90,31,1221,2,22,85,105

        十位排序:2,105,1221,22,31,85,90

        百位排序:2,22,31,85,90,105,1221

        千位排序:2,22,31,85,90,105,1221

         注意:每次排序都是在上次排序的基础上进行排序的,也就是说这次排序的位数上他们相对时,就不移动元素(即顺序参数上一个位数的排序顺序)

 

    主要步骤:

        一、把全部元素都分配到相应的桶中

       二、把全部桶中的元素都集合起来放回到数组中

       三、依次循环上面两步,循环次数为最大元素最高位数

 

    代码分析:

        参考下图

     一、竖  0~9:表示桶个数(每一个位数上的数字都在0到9之间);

     二、行 0~length:0 表示在某个桶内有多少个元素;

     三、好比:全部元素中个位为5的有两个元素,5 , 95;那么在下图中存放,分别是:(5,0) = 2;(5,1) = 5;(5,2)= 95;

 

基数排序是基于计数排序的,因此看这个以前要先看一下计数排序对于理解基数排序是颇有帮助的(发现计数和基数的音节几乎一致啊)。

所谓基数排序,其实就是分别对数字的个位,十位,百位,百位。。。。分别进行计数排序。

固然能够从个位往上进行计数排序,也能够从高位往个数计数排序,这里咱们使用个位往上计数排序的方法。

 

先从与计数排序的区别提及吧,区别在于计数排序是直接对数字进行排序。而基数排序是分别对个位,十位,百位。。。进行排序的。

而后,每一个位数中,都有0至9共10个数字(即个数时,其实就是10个数字作排序;十数时,其实也是对10个数字作排序),接着咱们对每一个数字中的数字进行计数排序(好绕口,意思就是说,当进行个数排序时,个位为1时,因此个位为1的数字进行计排,例如11,21,31,221,411等等)。

因此咱们申请的是二维数组int[][] radixBucket = new int[10][length]; (代码27行)  第一维的10存储的就是咱们每次都是对10个数分别进行计排。第二维存储的就是对应的要排序的数字啦  

同时,由于咱们要保证数字的稳定性,当咱们把低位的数字进行计排后,要把低位数字输出至原始数组中,而后再进行高位排序。

 

public class RadixSort {
    public static void main(String[] args) {
        int[] arrayData = { 2, 3, 1, 5, 6, 7, 4, 65, 42 };
        RadixSortMethod(arrayData, 100);
        for (int integer : arrayData) {
            System.out.print(integer);
            System.out.print(" ");
        }
    }

    /*
     * arrayData - 要排序的数据 height - 要排序的步长 若是100,则只排序个位十位
     */
    public static void RadixSortMethod(int[] arrayData, int height) {
        int maxNum = 0; // 最大值,用于存储桶数据临时数组空间大小
        for (int data : arrayData) {
            if (data > maxNum) {
                maxNum = data;
            }
        }

        int step = 1;
        int length = arrayData.length;
        int[][] radixBucket = new int[10][length]; // 二维数组,排序的容器
        int[] arrayTemp = new int[maxNum + 1]; // 这个是每一个桶中的数字个数
        int num;
        int index = 0;
        while (step < height) {
            for (int data : arrayData) {
                // 当step=1时统计个数,这时取出个位的数字。
                // 当step=10时,统计十数,这时取出十位的数字
                num = data / step % 10;
                radixBucket[num][arrayTemp[num]] = data;
                arrayTemp[num]++;
            }

            for (int i = 0; i < 10; i++) {
                if (arrayTemp.length > i && arrayTemp[i] != 0) {
                    for (int j = 0; j < arrayTemp[i]; j++) {
                        arrayData[index] = radixBucket[i][j];
                        index++;
                    }
                    arrayTemp[i] = 0; // 将当前数字个数重置为0,用于下次的统计
                }
            }

            step *= 10;
            index = 0;
        }
    }
}

 

时间复杂度:

假设步长是s,待排序数组长度是n,数字最大值是m

那么时间复杂度就是O(s(n+(10*m)))=O(s(m+n))

 

空间复杂度:

待排序数组长度是n,数字最大值是m。

那么空间复杂度就是O(10*n+(m+1))=O(m+n)

 

稳定性:是稳定的

 

应用场景:

针对最大值相对比较小的正整数。

 

时间复杂度

        该算法所花的时间基本是在把元素分配到桶里和把元素从桶里串起来;把元素分配到桶里:循环 length 次;

       把元素从桶里串起来:这个计算有点麻烦,看似两个循环,其实第二循环是根据桶里面的元素而定的,能够表示为:k×buckerCount;其中 k 表示某个桶中的元素个数,buckerCount  则表示存放元素的桶个数;

       有几种特殊状况:

       第1、全部的元素都存放在一个桶内:k = length,buckerCount = 1;

       第2、全部的元素平均分配到每一个桶中:k = length/ bukerCount,buckerCount = 10;(这里已经固定了10个桶)

       因此平均状况下收集部分所花的时间为:length (也就是元素长度 n)

 

       综上所述:

       时间复杂度为:posCount * (length  + length) ;其中 posCount 为数组中最大元素的最高位数;简化下得:O( k*n ) ;其中k为常数,n为元素个数;

 

空间复杂度

        该算法的空间复杂度就是在分配元素时,使用的桶空间;因此空间复杂度为:O(10 × length)= O (length)

 

 

 

 

桶排序(Bucket Sort)

 

  桶排序也叫箱排序。工做的原理是将数组元素映射到有限数量个桶里,利用计数排序能够定位桶的边界,每一个桶再各自进行桶内排序(使用其它排序算法或以递归方式继续使用桶排序)。

桶排序可用于最大最小值相差较大的数据状况,好比[9012,19702,39867,68957,83556,102456]。
但桶排序要求数据的分布必须均匀,不然可能致使数据都集中到一个桶中。好比[104,150,123,132,20000], 这种数据会致使前4个数都集中到同一个桶中。致使桶排序失效。

桶排序的基本思想是:把数组 arr 划分为n个大小相同子区间(桶),每一个子区间各自排序,最后合并
计数排序是桶排序的一种特殊状况,能够把计数排序当成每一个桶里只有一个元素的状况。

1.找出待排序数组中的最大值max、最小值min
2.咱们使用 动态数组ArrayList 做为桶,桶里放的元素也用 ArrayList 存储。桶的数量为(max-min)/arr.length+1
3.遍历数组 arr,计算每一个元素 arr[i] 放的桶
4.每一个桶各自排序
5.遍历桶数组,把排序好的元素放进输出数组

 

桶排序的逻辑其实特别好理解,它是一种纯粹的分而治之的排序方法。

举个例子简单说一下你们就知道精髓了。

假如对11,4,2,13,22,24,20 进行排序。

那么,咱们将4和2放在一块儿,将11,13放在一块儿,将22,24,20放在一块儿。  而后将这三部分分别排序(能够根据实现状况任意选择排序方式,个人代码中使用的是快排),将子数组排序后,再顺序输出就是最终排序结果了(大概应该明白了,咱们是根据数字大小进行分组的,故而顺序输出便可)

/*
 * 桶排序
 */
public class BucketSort {
    public static void main(String[] args) {
        int[] arrayData = { 22, 33, 57, 55, 58, 77, 44, 65, 42 };
        BucketSortMethod(arrayData, 10);
        for (int integer : arrayData) {
            System.out.print(integer);
            System.out.print(" ");
        }
    }

    /*
     * buckenCount - 桶的数量
     */
    public static void BucketSortMethod(int[] arrayData, int buckenCount) {
        int[][] arrayBucket = new int[buckenCount][arrayData.length]; // 桶容器

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

        int[] arrayLength = new int[arrayData.length];
        int num;

        // 将数据分桶
        for (int i = 0; i < arrayData.length; i++) {
            // 根据结果来肯定是存在在哪一个桶中
            num = arrayData[i] / buckenCount;
            num = 10 - num; // 这是为了降序

            // System.out.println(num);
            arrayBucket[num][arrayLength[num]] = arrayData[i];
            arrayLength[num]++;
        }

        // 将桶内数据进行排序,这里使用的是快排
        for (int i = 0; i < arrayBucket.length; i++) {
            QuickSort.QuickSortMethod(arrayBucket[i]);
        }

        int resultIndex = 0;
        // 对于桶内的数据进行输出
        for (int i = 0; i < arrayBucket.length - 1; i++) {
            if (arrayLength[i] > 0) {
                for (int j = 0; j < arrayBucket[i].length; j++) {
                    if (arrayBucket[i][j] != -1) {
                        arrayData[resultIndex++] = arrayBucket[i][j];
                    }
                }
            }
        }
    }
}

 

 

时间复杂度:

时间复杂度主要仍是取决于子数组的排序时间复杂度。 子数组的排序复杂度平均是O(n*log2n),而后分桶这块的的空间复杂度是O(n)

即O(n+n*log2n)

 

空间复杂度:

假设桶的数量是b,待排序数组的长度是n。

那么O(b*n)=O(n)

 

稳定性:稳定性主要取决于子数组中的排序(即44行调用的快排),子数组中使用的排序方法是稳定的,那么桶排序就是稳定的。

 

.