「バケット」を用いたソート

概要

 整列したいデータの取りうる値がm種類あるとき、m個のバケツを用意しておき、値ごとに1個のバケツを対応づけることを考えます。この概念をソートに応用したアルゴリズムについて説明します。
 安定ソートのアルゴリズムとしては、理論上極めて高速なソートを実現できます。ただしm個のバケツのためのメモリを必要とするため、値のデータ長が短く、分布範囲が小さい集合に向いていると言えます。

バケツの概念

 以下の配列 array に対するソートを行うとします。

array 0 1 0 3 2 0 1 2 0 1

値のデータ範囲が 0, 1, 2, 3 であると分かっているとして、どのような構造の「バケツ」を準備すればいいでしょうか。

バケットソートの「バケツ」

 以下のように、それぞれの値そのものを格納するバケツを考えます。

buckets[4]        
値0用バケツ 値1用バケツ 値2用バケツ 値3用バケツ

array の先頭から1つずつ値を読み、値ごとのバケツに格納していきます。配列の先頭から読み込むことによって、それぞれのバケツには、以下のように同じ値が出現順に格納されるので、安定ソートが実現できます(bucketsがヒストグラムとして機能する場合は、それぞれのバケツの中で他のソーティングアルゴリズムを適用します)。

buckets[4] 0 0 0 0 1 1 1 2 2 3
値0用バケツ 値1用バケツ 値2用バケツ 値3用バケツ

昇順ソートの場合、値 0 用バケツから順に取り出すと、ソートが完了します。

array 0 0 0 0 1 1 1 2 2 3

この場合、「バケツ」は、線形連結リストで実現できます。データの数が充分少なくかつデータ長が充分短ければ、キューによって「バケツ」を実現することもできます。バケツにarrayのデータすべてをコピーするので、バケツの総容量は、arrayと同じサイズを必要とします。

分布数えソートの「バケツ」

 「バケツ」は、それぞれの値が出現した回数を数えるカウンタとして機能します。このとき、値の範囲は明確であることが理想です。

int buckets[4] 0 0 0 0
[0]…値0用バケツ [1]…値1用バケツ [2]…値2用バケツ [3]…値3用バケツ

array の先頭から1つずつ値を読み、値に応じてバケツの中身をインクリメントします。このとき buckets は、度数分布を示しています。

int buckets[4] 4 3 2 1
[0]…値0用バケツ [1]…値1用バケツ [2]…値2用バケツ [3]…値3用バケツ

次に、整列済みの配列として array_dst を準備しておきます

array_dst [0] [1] [2] [3] [4] [5] [6] [7] [8] [9]
                   

一般的に、値 n の書き込みを開始できる要素番号は、buckets[0]からbuckets[n-1]までの総和 として計算できます。
このとき、arrayには 0 が 4 個あり、昇順ソートの場合、最小値である 0 は [0] から書き込むことができ、1 は [4]、2 は [7]、3 は [9] から書き込めることが分かります。

arrayを先頭から読み、読みこんだ値に応じて、array_dst の既定の位置に書き込みます。以下は、先頭の 0, 1, 0 を array_dst に書き込んだ例です。

array_dst [0] [1] [2] [3] [4] [5] [6] [7] [8] [9]
0 0     1          

さらに 3, 2, 0 を array_dst に書き込みます。

array_dst [0] [1] [2] [3] [4] [5] [6] [7] [8] [9]
0 0 0   1     2   3

同様に最後まで array_dst に書き込み、ソートが完了します。

array_dst [0] [1] [2] [3] [4] [5] [6] [7] [8] [9]
0 0 0 0 1 1 1 2 2 3

 

この場合、「バケツ」は、数値型(整数型)配列で実現できます。線形連結リストを用いる必要がないので実装が簡単になりますが、バケツのサイズとして、arrayに含まれている値の範囲に従った大きさが必要となり、さらに整列済み配列としてarray と同じサイズのメモリ容量を必要とします。

単一キー、安定を考慮しないソートへの応用

 分布数えソートにおいて、「バケツ」は度数分布を示すという考えを発展させます。 

array 0 1 0 3 2 0 1 2 0 1
int buckets[4] 4 3 2 1
[0]…値0用バケツ [1]…値1用バケツ [2]…値2用バケツ [3]…値3用バケツ

レコードではなく、単純な数値の並べ替えのような単一キーの場合、buckets の要素番号(に対応した値)と度数を参照することによって、array を整列させることができます。

0を4回書き出す

array 0 0 0 0 2 0 1 2 0 1

次に、1を3回書き出す

array 0 0 0 0 1 1 1 2 0 1

次に、2を2回書き出す

array 0 0 0 0 1 1 1 2 2 1

最後に、3を1回書き出す

array 0 0 0 0 1 1 1 2 2 3

array の 度数分布を取った後は、度数分布に基づいた繰り返し処理によって、array の中身を参照することなく、上書きを行うことで、結果として並べ替えが成立します。
単一キーの値集合を扱うため影響はありませんが、安定ソートとはなりません。それ以前に厳密にはソーティングとは言えないかもしれません。しかしながら、配列の走査は読込みと書出しの2回だけで良く、上書き処理のために整列済み配列を準備する必要もなく、array 以外のメモリ領域は buckets のみという効率的な処理が可能です。

欠点・問題点

キューによるバケツの総容量

 array の中身が、すべて同じ数である可能性を考慮すると、キューを用いたバケツの総容量は (array に含まれる値の総数) * (array に用いられる値の総数) となります。

断続的な分布における「無駄なバケツ」

 分布数えソートにおいて、以下のような配列を考えてみます。 

array 0 1 0 3 8 0 1 2 0 1

このとき、array のに用いられる値は 0, 1, 2, 3, 8 と断続的であり、度数分布のための配列は以下のようになります。

int buckets[4] 4 3 1 1 0 0 0 0 1
[0] [1] [2] [3] [4] [5] [6] [7] [8]

結果的に[4]~[7]は、用いられることのない無駄なバケツになってしまいます。

未知の定義域・可変長データの場合

 array の定義域が未知である場合、データ型のすべての値を取る可能性を考慮すると、バケツの総容量は

(データ型において取りうるすべての値) * (arrayに用いられる値の総数) * (buckets に格納すべきデータ長)

と、膨大になります。可変長データに至っては、データ型において取りうるすべての値の想定は不可能です。

 この問題は、バケツにヒストグラムの機能を持たせて、基数ソートを適用することによって解決できることがあります。

バケツ容量を確定するための前提が未知である場合

array に含まれる値が、そのまま buckets の要素番号として適用できる場合、計算の機会は array を一度走査するときだけです。

buckets の要素数を決定するにあたって、array に含まれる値の最小値、最大値、階級幅は既知であることが必要です。未知であれば、これらを求めるための演算が必要です。
array に含まれる値が、そのまま buckets の要素番号として適用できない場合、両者の対応関係の演算が必要になります。

関連項目

更新履歴

2008/07/25: 作成


Back / Studying / Top