常用排序算法(五)基数排序、桶排序以及计数排序

这是三种线性时间复杂度的排序算法,它们是用运算而不是比较来确定排序顺序的

一、基数排序

1.简介

它一种与其他排序算法完全不同的排序方法,其他的排序算法都是通过关键字之间的比较和移动来完成的,而它是采用一种多关键字的思想。

多关键字的思想:给定一组数据,我可以先按个位的大小对所有数进行排序,然后再按十位进行排序,一直到最高位,这样就可以使整组数据变得有效,这样从最低位开始的方法称为最低位优先

2.图解过程

常用排序算法(五)基数排序、桶排序以及计数排序

经过一次放置和回收,得到的序列已经按个位有序了,接下来按照次低位再次进行放置和回收。

常用排序算法(五)基数排序、桶排序以及计数排序

由此可以看出,如果一组序列中最大的数为两位数,则需要两次的分配和收集,整个分配收集的次数与最大数的位数有关

基数排序需要两个辅助空间,一个是0~9号桶,另一个是计算定位的数组,定位数组是干什么的呢?就是记录每个桶中的数据待会要放回原数组的哪个位置。

常用排序算法(五)基数排序、桶排序以及计数排序

def radixSort(number: Array[Int], d: Int): Unit = { //d表示最大的数有多少位
    var k = 0
    var n, m = 1
    //控制键值排序依据在哪一位
    val temp = Array.ofDim[Int](10, number.length)
    //数组的第一维表示可能的余数0-9
    val order = new Array[Int](10) //数组orderp[i]用来表示该位是i的数的个数
    while (m <= d) {
      for (i <- number.indices) {
        val lsd = (number(i) / n) % 10
        temp(lsd)(order(lsd)) = number(i)
        order(lsd) += 1
      }
      var i = 0
      while (i < 10) {
        if (order(i) != 0) {
          var j = 0
          while (j < order(i)) {
            number(k) = temp(i)(j)
            k += 1
            j += 1
          }
        }
        order(i) = 0
        i += 1
      }
      n *= 10
      k = 0
      m += 1
    }
  }

基数排序的时间复杂度可以理解为O(d*n),d为序列中最大的位数,适用于n值很大,但是关键字较小的序列。

二、桶排序

1.概念

桶排序将[0,1)区间划分为n个相同的大小的子区间,这些子区间被称为桶。然后将n个输入元素分别放入各自的桶中。因为输入时均匀独立的,所以一般不会有很多数同时落在一个桶中的情况。这样,我们想对各个桶中的数据进行排序,然后遍历每个桶,按照次序把各个桶中的元素列出来即可

2.特性说明

1》. 桶排序的时间复杂度通常是O(N+N*logM),其中,N表示桶的个数,M表示桶内元素的个数(这里,M取的是一个大概的平均数,这也说明,为何桶内的元素尽量不要出现有的很多,有的很少这种分布不均的事情,分布不均的话,算法的性能优势就不能最大发挥)。

2》. 桶排序是稳定的(是可以做到平衡排序的)。

3》. 桶排序,在内存方面消耗是比较大的,可以说其时间性能优势是由牺牲空间换来的。

桶排序,在大数据量的情况下排序,比快速排序还要快。若待排序的数据元素个数比较少,桶排序的优势就不是那么明显了,因为桶排序就是基于分而治之的策略,可以将数据进行分布式排序,充分发挥并行计算的优势。

3.步骤

1》.找出待排序数组中的最大值max、最小值min

2》.我们使用 动态数组ArrayList 作为桶,桶里放的元素也用 ArrayList 存储。桶的数量为(max-min)/arr.length+1

3》.遍历数组 arr,计算每个元素 arr[i] 放的桶

4》.每个桶各自排序

5》.遍历桶数组,把排序好的元素放进输出数组

4.实现

def bucketsort(inputData: ArrayBuffer[Int], max: Int): ArrayBuffer[Int] = {
    var buckets = new Array[Int](max)
    for (i <- inputData.indices) //计数
      buckets(inputData(i)) = buckets(inputData(i)) + 1
    var j = 0
    for (i <- 0 until max)
      while (buckets(i) > 0) {
        inputData(j) = i
        j = j + 1
        buckets(i) = buckets(i) - 1
      }
    buckets = null
    inputData
  }

三、计数排序

1.优势

计数排序是桶排序的一种特殊情况,可以把计数排序当成每个桶里只有一个元素的情况,它是一个非基于比较的排序算法,它的优势在于在对一定范围内的整数排序时,它的复杂度为Ο(n+k)(其中k是整数的范围),快于任何比较排序算法。

2.思想

对于一个输入数组中的一个元素x,如果这个数组中比x小的元素有n个,那么我们就可以直接把x放到(n+1)的位置上。这就是计数排序的基本思想,类似于哈希表中的直接定址法,在给定的一组序列中,先找出该序列中的最大值和最小值,从而确定需要开辟多大的辅助空间,每一个数在对应的辅助空间中都有唯一的下标。
基于这个思想,计数排序的一个主要问题就是如何统计数组中元素的个数。再加上输入数组中的元素都是0-k区间的一个整数这个条件,那么就可以通过另外一个数组的地址表示输入元素的值,数组的值表示元素个数的方法来进行统计。

下面给出统计数组元素都是0-k区间的整数的数组中各个元素个数的方法。

  1. 找出序列中最大值和最小值,开辟Max-Min+1的辅助空间
  2. 最小的数对应下标为0的位置,遇到一个数就给对应下标处的值+1,。
  3. 遍历一遍辅助空间,就可以得到有序的一组序列

常用排序算法(五)基数排序、桶排序以及计数排序

3.算法分析: 

计数排序是一种以空间换时间的排序算法,并且只适用于待排序列中所有的数较为集中时,比如一组序列中的数据为0 1 2 3 4 999;就得开辟1000个辅助空间。 
时间复杂度 
计数排序的时间度理论为O(n+k),其中k为序列中数的范围。 
不过当O(k)>O(n*log(n))的时候其效率反而不如基于比较的排序(基于比较的排序的时间复杂度在理论上的下限是O(n*log(n)), 如归并排序,堆排序)

4.代码实现

def Countingsort(inputData: ArrayBuffer[Int], k: Int): Array[Int] = {
    //k表示有所输入数字都介于0到k之间
    val temp = new Array[Int](k)
    // 临时存储区
    val outdata = new Array[Int](inputData.length)
    val len = temp.length
    for (i <- 0 until len) {
      // 初始化
      temp(i) = 0
    }
    for (i <- inputData.indices) {
      temp(inputData(i)) = temp(inputData(i)) + 1
    }
    for (i <- 1 until len) {
      temp(i) = temp(i) + temp(i - 1)
    }
    // 把输入数组中的元素放在输出数组中对应的位置上
    var n = inputData.length - 1
    while (n >= 0) {
      // 从后往前遍历
      outdata(temp(inputData(n)) - 1) = inputData(n)
      temp(inputData(n)) = temp(inputData(n)) - 1
      n = n - 1
    }
    outdata
  }