今日はクイックソートでハマったことを書きたいと思います。クイックソートはデータを小さい順(または大きい順)に並べるアルゴリズムです。ここでは与えられたデータを「集合」と呼びますが、数学で扱う集合とは異なり、同じ要素を複数含んでいても良いことにします。
クイックソートでは、データの集合Sを3つの部分に分割します。そのためにある基準となる値(ピボットと呼ばれる)xを決めます。xは集合Sの中から取ってくることにします。第一の集合は、xよりも小さい要素の集合S1です。第二は、データxと同じ値を持つ要素の集合S2。第三はxよりも大きい値を持つ要素の集合S3です。S2は同じ値の要素だけなので、すでに整列されています。次の段階は、S1とS3について同じ方法でソートすることです。これらの集合は要素の数がSより小さいので、同じアルゴリズムの有限回の繰り返しで整列が完了します。
このアルゴリズムは完全なのですが、3分割を2分割で済ませられれば、実装が簡単になります。例えば、S1は同じで、S2とS3を一つの集合S4にするのです。 つまりSがxより小さい要素の集合、S4がx以上の要素の集合になります。 ところがこれだとうまくいかない場合があるのです。xの取り方によってはSの全ての要素がx以上であることがあり得ます。その場合S1は空集合、S4はSに等しくなります。ということは、S4の要素の数が減りませんから、アルゴリズムを繰り返すと無限ループに陥ってしまいます。このバグに気づくまで、何時間もかかってしまいました(TT)。
最初の方法では、xがSの要素で、S2が空ではないことが効いています。第二の方法では何らかの方法で、無限ループを回避することが必要です。例えば、S1が空集合だった場合に、S4をS2とS3に分けることにより、第一の方法に帰着させることが考えられます。この方法で書いたCプログラムを以下に載せておきます。何か間違いがあったら、コメントで指摘していただくと助かります。
クイックソート: 配列a[]のインデックスlからhまでの要素を整列する。
void
q_sort(int a[], int l, int h) {
int swap, l1, h1, s;
if (l >= h)
return;
s = a[(l+h)/2];
l1 = l;
h1 = h;
while (l1 <= h1) {
for (;a[l1] < s; ++l1)
;
for (;a[h1] >= s;--h1)
;
if (l1 < h1) {
swap = a[l1];
a[l1++] = a[h1];
a[h1--] = swap;
}
}
if (l1 == l) {
h1 = h;
while (l1 < h1) {
for (;a[l1] == s; ++l1)
;
for (;a[h1] > s;--h1)
;
if (l1 < h1) {
swap = a[l1];
a[l1++] = a[h1];
a[h1--] = swap;
}
}
}
if (h1 == h)
return;
q_sort (a, l, h1);
q_sort (a, l1, h);
}
追記:
投稿後、「プログラム言語 C」のクイックソートプログラムを見たら、発想は同じだが、もっとスマートなプログラムでした。当然ですね。当時の最先端の研究者のプログラムですから。
この方法は、xがS4の最小値であることを利用するものです。はじめに、配列中のxを値に持つ要素を配列の端(プログラム言語Cでは左端)と交換して、残りの部分で集合S1と集合S4に分けます。そのあと、はじめに端に置いた要素と、S1の右端の要素(最初に左端においた場合)を交換、またはS2の左端の要素(最初に右端においた場合)と交換します。あらためて、xより小さい要素の集合をS1、最後に端から中間に移動した要素を除いたx以上の要素の集合をS2とします。
(1) S1={s|s<x}
(2) 端から中間に移動した要素
(3) S4={s|s>=x,ただし、端から中間に移動した要素を除く}
これで、S4がSよりも少ない要素数になり、無限ループを避けることができます。
やはり、基本は3つの部分に分けることだったんですね。「プログラム言語C」のクイックソートをそのまま載せるわけにはいかないので、新たにプログラムを書きました。このプログラムでは右端にピボット要素を移動しています。
追記の追記:
申し訳ありません。プログラムにバグがありましたので、修正しました。
- 14行目のwhile文の不等号<を<=に修正しました。
- 15行目と17行目のfor文の条件に、15行目に「&& l1<=h-1」17行目に「&& h1 >= l」を入れました。
static void
q_sort(int a[], int l, int h) {
int swap, l1, h1, s, mid;
if (l >= h)
return;
mid = (l + h) / 2;
s = a[mid];
swap = a[h];
a[h] = a[mid];
a[mid] = swap;
l1 = l;
h1 = h-1;
while (l1 <= h1) {
for (;a[l1] < s && l1<=h-1; ++l1)
;
for (;a[h1] >= s && h1 >= l;--h1)
;
if (l1 < h1) {
swap = a[l1];
a[l1++] = a[h1];
a[h1--] = swap;
}
}
swap = a[h];
a[h] = a[l1];
a[l1++] = swap;
q_sort (a, l, h1);
q_sort (a, l1, h);
}