让我们从将您的代码改写成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];
memset(bucket, 0, sizeof(int) * bucket_len);
for (i = 0; i < a_len; i++)
{
bucket[a[i]]++;
}
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;
for (i = 0; i != ndigits; ++i)
{
int j;
int bucket[10] = {0};
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};
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;
}
}
}
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的乘数。
现在让我们回想一下在代码中看到了什么:
- 计数排序--简单的桶,简单的处理,内存开销
- 基数排序--简单的桶,复杂的处理,速度开销(仍需要额外的静态内存)
- 桶排序--复杂的桶,简单的处理,需要动态内存,在平均情况下表现良好
因此,基数排序和桶排序是计数排序的两种有用的泛化。它们与计数排序和彼此都有很多共同之处,但在每种情况下,我们都会失去一些东西并赢得一些东西。软件工程是在这些机会之间取得平衡的。
else if (a[i] <= bptr->elem)
中的比较应该是<
。另外,while (bptr != 0)
可以改为while (true)
。 - Powereleven