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 要素の配列、主にゼロを使用しました。ひどい。並べ替えをカウントするこの問題を処理するために、2 つの一般化方法が考えられます。
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 日作業と 1 週間作業は、両方がそれぞれ 1*O(N) と 7*O(N) で動作する場合でも、ユーザー ビューとは大きく異なります。そして、2 番目の一般化に進みます。
2) 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 に近い乗数を獲得しています。
ここで、コードで見たものを思い出してみましょう。
- 並べ替えのカウント -- 単純なバケット、単純な処理、メモリ オーバーヘッド
- 基数ソート -- 単純なバケット、高度な処理、速度のオーバーヘッド (さらに追加の静的メモリが必要)
- バケット ソート -- 洗練されたバケット、単純な処理、動的メモリが必要、平均的に良好
したがって、基数ソートとバケット ソートは、カウント ソートの 2 つの有用な一般化です。それらは並べ替えを数えたり、お互いに多くの共通点がありますが、いずれの場合も何かを失い、何かを獲得しています。ソフトウェア エンジニアリングは、これらの機会のバランスを取ることです。