基数排序 vs 计数排序 vs 桶排序。有啥不同?
Posted
技术标签:
【中文标题】基数排序 vs 计数排序 vs 桶排序。有啥不同?【英文标题】:Radix sort vs Counting sort vs Bucket sort. What's the difference?基数排序 vs 计数排序 vs 桶排序。有什么不同? 【发布时间】:2013-01-16 21:41:37 【问题描述】:我正在阅读基数、计数和桶排序的定义,似乎所有这些都只是下面的代码:
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 进行解释,请显示代码。
【问题讨论】:
【参考方案1】:让我们从用 C 重写你的代码开始,因为 C 对我来说更熟悉解释。因此,让我们用一些 cmets 回忆您的代码:
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) 第一种方式——按数字排序。这称为基数排序。让我们展示一些代码,尝试使其尽可能接近计数排序代码。再看cmets:
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) 第二种方式——使存储桶更复杂。这称为桶排序。
让我们再次从一些代码开始。在哲学论证之前,我更喜欢更多的代码。还是看cmets吧,必不可少。
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 的乘数。
现在让我们回忆一下我们在代码中看到了什么:
-
计数排序 -- 简单的桶、简单的处理、内存开销
基数排序 -- 简单的存储桶、复杂的处理、速度开销(并且仍然需要额外的静态内存)
桶排序——复杂的桶,简单的处理,需要动态内存,平均不错
因此,基数和桶排序是计数排序的两个有用的概括。它们与计数排序和彼此有很多共同点,但在每种情况下,我们都在失去一些东西并赢得一些东西。软件工程就是要在这些机会之间取得平衡。
【讨论】:
【参考方案2】:基数排序 vs 计数排序 vs 桶排序。有什么区别?
桶排序将要排序的键或元素放入桶中。它们如何放置在存储桶中是任意的,可以是复合键的一部分,也可以是您喜欢的任何分布。各个桶可能需要进一步排序。
在内存中排序比在磁盘上排序更快。但是,如果您的数据超出内存容量,则需要另一种选择。您可以做的是桶排序,其中桶足够小以适合内存。即每个桶中有大量条目。这些您可以单独快速排序。
基数排序是一种特定类型的桶排序。它从前 n 位或 n 位开始,并可以使用基数排序等对这些桶进行排序,直到每个条目都被排序。
计数排序类似于使用基数排序,但您使用的是整个值。它不是记录每个对象,而是为每个对象都有一个存储桶,它只计算出现的次数。当您有有限数量的可能键并且您有很多重复项时,这很有效。
【讨论】:
【参考方案3】:根据极客观点:
基数: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 和最大值之间的值使用一个存储桶)。
如果您使用桶排序,您不知道将使用多少桶。无论您使用什么哈希函数都将决定存储桶的数量。
【讨论】:
基数排序类似于计数排序,因为在这两种排序中,您都必须知道手之前的桶数。 基数为 10 的基数排序是无稽之谈。应该始终使用 2 的幂作为基数,例如 16 或 256。 @hirschhornsalz 我猜是因为这是一个排序教程,作者使用十进制而不是二进制来保持简单。【参考方案4】:您的代码是计数排序的简单变体,没有数据,只有键。
基数排序就是基于这种方法的排序。计数排序的问题是内存要求:int [] bucket=new int[maxVal+1];
。基数排序解决了这个问题。这个想法是多次使用计数排序,首先是较低的数字,然后是较高的数字。例如,要对 32 位整数进行排序,您可能会使用:
sort(a, 65535) using lower half as key
sort(a, 65535) using higher half as key
它可以工作,因为计数排序是稳定的 - 它使用相等的键保持数据的顺序。这就像在电子表格中排序:sort by B; sort by A
为您提供按 A 排序的元素,当 As 相等时按 B 排序。
桶排序是计数排序的推广。您可以使用它从一些可预测的概率分布中对实数进行排序(例如,统一的(0,1)
)。这个想法是使用计数排序(使用floor(x*N_BUCKETS)
作为键),然后只对每个桶进行独立排序。
【讨论】:
计数排序是否同时使用数据和键? @den-javamaniac - 它可以与数据一起使用。然后,您需要记住每个存储桶的元素。它可以用一个额外的数组来实现。【参考方案5】:首先让我们看看基数排序和桶排序之间的区别,因为这通常是一个令人困惑的事情,因为想法似乎相同。然后我们看一下计数排序,它就像这两个的主要版本,计数排序的哪些问题导致使用其他两个
Radix 和 Bucket 排序的初始 pass 相同。将元素放入“Buckets”中,即 0-10、11-20、...等等,具体取决于最大 no 中的位数,即基数。然而,在下一轮中,桶排序将这些“桶”排序并将它们附加到一个数组中。但是,基数排序方法在不进一步排序的情况下附加存储桶,并根据数字的第二位(十位)“重新存储桶”。因此,桶排序对“密集”数组更有效,而基数排序可以很好地处理稀疏数组。 好吧,把桶排序想象成这样
假设您有一个包含 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 范围内。这些数字很容易小到可以进行桶排序。
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。在我们遍历 n A 的元素并更新 C,C 的索引 j 处的值对应于 j 在 A 中出现了多少次。这一步需要 O(n) 时间来迭代 通过 A。一旦我们有了 C,我们就可以通过以下方式构造 A 的排序版本 遍历 C 并插入每个元素 j 总共 C[j] 次 进入一个新列表(或 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
第一个回复:What is the difference between bucket sort and radix sort?
【讨论】:
【参考方案6】:基数排序使用一种计数排序形式作为子程序(好的,可以使用,但大多数情况下它是计数排序)。
正如 kasavbere 所回答的那样,计数排序是一种特殊形式的桶排序。
而 Bucketsort 将键分成桶,然后对桶进行单独排序。
【讨论】:
【参考方案7】:使用计数排序对数组进行排序:
#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)
,因此按线性时间排序。对于桶排序,这是非常不同的。这里是an implementation
【讨论】:
它没有回答问题:Radix sort vs Counting sort vs Bucket sort. What's the difference?
被问到的只是将桶排序与计数排序混淆了。我只是把它的代码展示给
运行时间其实是O(MAX_INPUT + n)。
省略 n 因为它与 MAX_INPUT 相比相对较小(应该)以上是关于基数排序 vs 计数排序 vs 桶排序。有啥不同?的主要内容,如果未能解决你的问题,请参考以下文章