基数排序、计数排序和桶排序。它们有什么区别?

65

我正在阅读基数排序、计数排序和桶排序的定义,似乎它们都只是下面的代码:

public static void sort(int[] a, int maxVal){
    int [] bucket=new int[maxVal+1];

    for (int i=0; i<bucket.length; i++){
        bucket[i]=0;
    }

    for (int i=0; i<a.length; i++){
        bucket[a[i]]++;
    }

    int outPos=0;
    for (int i=0; i<bucket.length; i++){
        for (int j=0; j<bucket[i]; j++){
            a[outPos++]=i;
        }
    }
}

我知道我可能是错的,那么我错在哪里?如果您认为展示Java或C代码有助于解释,请展示代码。

7个回答

80

让我们从将您的代码改写成C语言开始,因为我更熟悉C语言并且能更好地解释。所以让我们通过一些注释来回忆一下您的代码:

int
counting_sort(int a[], int a_len, int maxVal)
{
  int i, j, outPos = 0;
  int bucket_len = maxVal+1;
  int bucket[bucket_len]; /* simple bucket structure */

  memset(bucket, 0, sizeof(int) * bucket_len);

  /* one loop bucket processing */
  for (i = 0; i < a_len; i++)
    {
      bucket[a[i]]++; /* simple work with buckets */
    }

  for (i=0; i < bucket_len; i++)
    {
      for (j = 0; j < bucket[i]; j++)
        {
          a[outPos++] = i;
        }
    }

  return 0;
}

现在让我们给这个人提供一些真实的数据:

[126,348,343,432,316,171,556,223,670,201]

输出结果如下:

[126,171,201,223,316,343,348,432,556,670]

看起来一切都好了?还不够。让我们看一下maxVal。它是670(!)为了对10个元素的数组进行排序,我们使用了一个由主要是零的670个元素组成的数组。很糟糕。为了解决计数排序的这个问题,我们有两种可能的泛化方法:

1)第一种方法--逐位排序。这被称为基数排序。让我们展示一些代码,试图使它尽可能接近计数排序代码。再次查看注释:

int
radix_sort(int a[], int a_len, int ndigits)
{
  int i;
  int b[a_len];
  int expn = 1;

  /* additional loop for digits */
  for (i = 0; i != ndigits; ++i)
    {
      int j;
      int bucket[10] = {0}; /* still simple buckets */

      /* bucket processing becomes tricky */
      for (j = 0; j != a_len; ++j)
        bucket[ a[j] / expn % 10 ]++;

      for (j = 1; j != 10; ++j)
        bucket[j] += bucket[j - 1];

      for (j = a_len - 1; j >= 0; --j)
        b[--bucket[a[j] / expn % 10]] = a[j];

      for (j = 0; j != a_len; ++j)
        a[j] = b[j];

      expn *= 10;
    }
}

我们正在将倍增器接近 N 的交易换成内存。利润?也许吧。但在某些情况下,倍增器接近 N 是非常重要的。即使两者都以 1*O(N) 和 7*O(N) 工作,从用户的角度来看,每天工作和每周工作是非常不同的。因此,我们得出了第二个概括:

2)第二种方法是使“桶”更为复杂。这被称为桶排序。

让我们再次从一些代码开始。我更喜欢在哲学论证之前先放更多代码。注意注释,它们是必不可少的。

int
bucket_sort(int a[], int a_len, int maxVal)
{
  int i, aidx;

  typedef struct tag_list {
    int elem;
    struct tag_list *next;
  } list_t, *list_p;

  list_p bucket[10] = {0}; /* sophisticated buckets */

  /* one loop simple processing with one more inner loop 
    to get sorted buckets (insert-sort on lists, Cormen-style) */
  for (i = 0; i != a_len; ++i)
    {
      int bnum = (10 * a[i]) / maxVal;
      list_p bptr = bucket[bnum];
      list_p belem = malloc(sizeof(list_t));
      belem->elem = a[i];
      if (bptr == 0)
        {
          bucket[bnum] = belem;
          belem->next = 0;
          continue;
        }
      else if (a[i] <= bptr->elem)
        {
          belem->next = bptr;
          bucket[bnum] = belem;
          continue;
        }
      else
        {
          while (bptr != 0)
            {
              if ((bptr->elem <= a[i]) && ((bptr->next == 0) || (bptr->next->elem > a[i])))
                {
                  belem->next = bptr->next;
                  bptr->next = belem;
                  break;
                }
               bptr = bptr->next;
            }
         }
    }

  /* one loop (looks as two) to get all back */
  aidx = 0;

  for (i = 0; i != 10; ++i)
    {
      list_p bptr = bucket[i];
      while (bptr)
        {
          list_p optr = bptr;
          a[aidx] = bptr->elem;
          aidx += 1;
          bptr = bptr->next;
          free(optr);
        }
    }

  return 0;
}

那么我们在这里有什么?我们交易一些复杂的桶结构和对动态分配内存的要求,但赢得了静态内存和近似于N的乘数。

现在让我们回想一下在代码中看到了什么:

  1. 计数排序--简单的桶,简单的处理,内存开销
  2. 基数排序--简单的桶,复杂的处理,速度开销(仍需要额外的静态内存)
  3. 桶排序--复杂的桶,简单的处理,需要动态内存,在平均情况下表现良好

因此,基数排序和桶排序是计数排序的两种有用的泛化。它们与计数排序和彼此都有很多共同之处,但在每种情况下,我们都会失去一些东西并赢得一些东西。软件工程是在这些机会之间取得平衡的。


请注意,您用于桶的简单哈希在最大整数(访问索引10)时会溢出。此外,如果添加负数,它也会出错。而且,它是不稳定的。为了使其稳定,else if (a[i] <= bptr->elem)中的比较应该是<。另外,while (bptr != 0)可以改为while (true) - Powereleven

19
桶排序与计数排序与基数排序。它们有何不同?
桶排序将要排序的键或元素放入桶中。如何放入桶中是任意的,可以是复合键的一部分和任何您喜欢的分布。单独的桶可能需要进一步排序。
内存排序比磁盘上的排序更快。但是,如果您有的数据超过了内存容量,则需要另一种选择。您可以进行桶排序,其中桶足够小,可以适合内存。即每个桶中有大量条目。您可以对这些条目进行快速排序。
基数排序是特定类型的桶排序。它从顶部n位或n位数字开始,并可以使用基数排序等方式对这些桶进行排序,直到每个条目都已排序。
计数排序类似于使用基数排序,只是您使用整个值。它为每个对象拥有一个桶,而不是记录每个对象,而只计算出现的次数。当您具有有限数量的可能键并且有许多重复项时,这很有效。

12

据Geekviewpoint称:

基数排序: http://www.geekviewpoint.com/java/sorting/radixsort

基数排序、计数排序和桶排序一样,是一种基于整数的算法(即假定输入数组的值为整数)。因此,基数排序在理论上是最快的排序算法之一。基数排序的特别之处在于它为每个密码(即数字)创建一个桶;因此,类似于桶排序,基数排序中的每个桶都必须是一个可增长的列表,可以接受不同的键。

桶排序:http://www.geekviewpoint.com/java/sorting/bucketsort

考虑到计数排序是其上限,桶排序实际上非常好。而计数排序速度非常快。桶排序的特别之处在于它使用哈希函数将输入数组的键分区,以便多个键可能散列到同一个桶中。因此,每个桶必须有效地是一个可增长的列表;与基数排序类似。

计数排序:http://www.geekviewpoint.com/java/sorting/countingsort

计数排序的特别之处在于它为每个值创建一个桶,并在每个桶中保持一个计数器。然后,每次在输入集合中遇到一个值时,适当的计数器就会增加。因为计数排序为每个值创建一个桶,所以强加的限制是必须事先知道输入数组中的最大值。

他们在网站上提供了更详细的解释。

编辑:

  • 如果您使用基数排序且数字是十进制数,则需要10个桶,一个桶用于存储0到9的每个数字。

  • 如果你正在使用计数排序,那么你需要为输入中的每个唯一值准备一个桶(实际上你需要为0到最大值之间的每个值都准备一个桶)。

  • 如果你正在使用桶排序,那么你不知道会用到多少个桶。无论你使用什么哈希函数,它都将确定桶的数量。


  • 基数排序与计数排序相似,因为在两种排序算法中,必须事先知道桶的数量。 - Konsol Labapen
    1
    使用基数为10的基数排序是无意义的。应该始终使用2的幂作为基数,如16或256。 - Gunther Piez
    @hirschhornsalz 我猜是因为这是一种教程,作者使用十进制而不是二进制来保持简单。 - kasavbere

    6

    你的代码是一个简单的基于键值而不是数据的计数排序。

    基数排序是基于这种方法的排序。计数排序的问题在于内存需求:int [] bucket=new int[maxVal+1];。基数排序解决了这个问题。它的思想是多次使用计数排序,首先用于低位数字,然后用于高位数字。例如,要对32位整数进行排序,可以使用以下方式:

    sort(a, 65535) using lower half as key
    sort(a, 65535) using higher half as key
    

    它的工作原理在于计数排序是稳定的-它保持具有相等键的数据的顺序。就像在电子表格中排序一样:按B排序;按A排序,让您按A排序的元素,并在A相等时按B排序。
    桶排序是计数排序的一般化。您可以使用它来对来自某些可预测概率分布(例如均匀分布(0,1))的实数进行排序。其想法是使用计数排序(使用floor(x*N_BUCKETS)作为键),然后仅独立地对每个桶进行排序。

    计数排序是否同时使用数据和键? - Denys S.
    @den-javamaniac - 它可以与数据一起使用。然后,您需要记住每个桶的元素。它可以通过一个额外的数组来实现。 - zch

    3
    首先让我们看看基数排序和桶排序之间的区别,因为这通常是令人困惑的,因为它们的思路似乎相同。然后我们看一下计数排序,它类似于这两种方法的主要版本,并且计数排序存在的问题导致使用其他两种方法。
    Radix和Bucket排序的初始步骤是相同的。元素被放入“桶”中,即0-10、11-20等等,取决于最大数字的位数,即基数。在下一步中,桶排序将这些“桶”排序并将它们附加到一个数组中。然而,基数排序方法将桶附加到数组中而不进行进一步排序,并根据数字的第二个位(十位)进行“重新分桶”。因此,桶排序更适用于“密集”的数组,而基数排序可以很好地处理稀疏数组。
    好吧,把桶排序看作是这样的:
    假设您有一个包含n个记录的列表,每个记录的关键字都是1到k之间的数字(我们略微推广了问题,因此k不一定等于n)。
    我们可以通过制作一个链接列表数组来解决这个问题。我们将每个输入记录移动到数组的适当位置的列表中,然后按顺序将所有列表连接在一起。
     bucket sort(L)
        {
        list Y[k+1]
        for (i = 0; i <= k; i++) Y[i] = empty
        while L nonempty
        {
            let X = first record in L
            move X to Y[key(X)]
        }
        for (i = 0; i <= k; i++)
        concatenate Y[i] onto end of L
        }
    

    当k很大时该怎么办?想一想一个数字的小数表示: x = a + 10 b + 100 c + 1000 d + ... 其中a、b、c等都在0..9范围内。这些数字足够小,可以使用桶排序。请保留HTML标签。
       radix sort(L):
        {
        bucket sort by a
        bucket sort by b
        bucket sort by c
        ...
        }
    

    或更简单
    radix sort(L):
    {
    while (some key is nonzero)
    {
        bucket sort(keys mod 10)
        keys = keys / 10
    }
    }
    

    为什么我们要先从最不重要的数字开始排序?同样地,为什么我们要进行多次桶排序,因为最后一次是将所有内容放置在正确位置的关键?
    回答:如果我们试图手动排序,我们往往会做一些不同的事情:首先进行桶排序,然后递归地对共享相同第一个数字的值进行排序。这种方法可以工作,但效率较低,因为它将问题分解成许多子问题。相比之下,基数排序永远不会分割列表;它只是将桶排序多次应用于同一列表。
    在基数排序中,桶排序的最后一次传递对整体顺序影响最大。因此,我们希望它使用最重要的数字。前面的桶排序通行仅用于处理在最后一次(mod 10)上具有相同键的两个项目的情况。
    既然我们已经了解了这一点,计数排序所做的就是保持一个大小为k的辅助数组C,所有元素都初始化为0。
    我们对输入数组A进行一次遍历,对于我们看到的A中的每个元素i,我们将C[i]增加1。在我们遍历完A的n个元素并更新C之后,C中索引j的值对应于j在A中出现的次数。这一步需要O(n)时间来遍历A。一旦我们有了C,我们可以通过遍历C并将每个元素j总共插入C[j]次到一个新列表(或A本身)中来构建A的排序版本。遍历C需要O(k)时间。最终结果是一个排序好的A,总共花费了O(n + k)的时间。
    计数排序的缺点是,如果元素范围太大,则可能不太实用。例如,如果我们需要排序的n个元素的范围是从1到n^3,则仅创建辅助数组C将需要O(n^3)的时间,并且计数排序在渐近意义下会比插入排序更糟糕。这也需要O(n^3)的空间,比我们迄今学到的任何其他排序算法使用的空间都要大得多。基数排序通过逐位对元素进行排序来解决这个问题。
    注意:答案和进一步阅读的来源:

    http://htmltolatex.sourceforge.net/samples/sample4.html

    第一个回答:桶排序和基数排序有什么区别?


    2

    基数排序使用计数排序作为子程序(可以使用,但通常会是计数排序)。

    计数排序是桶排序的一种特殊形式,正如kasavbere所回答的那样。

    而桶排序将键分成桶,然后逐个对桶进行排序。


    1
    使用计数排序对数组进行排序:
    #define MAX_INPUT 1000
    
    void sort(int arr[100], int n)
    {
        static int hash[MAX_INPUT], i, j;
    
        memset(hash, 0, sizeof hash);
    
        for (i = 0; i < n; ++i) ++hash[arr[i]];
    
        j = 0;
        for (i = 0; i < MAX_INPUT; ++i)
            while (hash[i]--)
               arr[j++] = i;
    }
    

    这只是O(MAX_INPUT),因此可以在线性时间内排序。对于桶排序,情况则非常不同。这里有一个实现


    3
    这个问题没有得到回答:基数排序、计数排序和桶排序有什么区别? - amit
    他们刚才把桶排序和计数排序搞混了。我只是向他们展示了它的代码。 - user586399
    运行时间实际上为O(MAX_INPUT + n)。 - Ivaylo Toskov
    省略 n,因为与 MAX_INPUT 相比较小(应该是)。 - user586399

    网页内容由stack overflow 提供, 点击上面的
    可以查看英文原文,
    原文链接