Knuth はこれを演習として残しました (Vol 3, 5.2.5)。インプレースマージソートが存在します。それらは慎重に実装する必要があります。
まず、ここで説明されているような単純なインプレース マージは適切なソリューションではありません。パフォーマンスをO(N 2 )にダウングレードします。
アイデアは、マージの作業領域として残りを使用しながら、配列の一部をソートすることです。
たとえば、次のマージ関数のように。
void wmerge(Key* xs, int i, int m, int j, int n, int w) {
while (i < m && j < n)
swap(xs, w++, xs[i] < xs[j] ? i++ : j++);
while (i < m)
swap(xs, w++, i++);
while (j < n)
swap(xs, w++, j++);
}
配列を取り、xs
2 つの並べ替えられたサブ配列はそれぞれ範囲[i, m)
およびとして表されます[j, n)
。作業領域は から始まりw
ます。ほとんどの教科書に記載されている標準のマージ アルゴリズムと比較すると、このアルゴリズムは、並べ替えられたサブ配列と作業領域の間で内容を交換します。その結果、以前の作業領域にはマージされた並べ替えられた要素が含まれ、作業領域に格納されていた以前の要素は 2 つのサブ配列に移動されます。
ただし、満たさなければならない制約が 2 つあります。
- 作業域は、配列の境界内にある必要があります。つまり、境界外エラーを発生させることなく、交換される要素を保持するのに十分な大きさである必要があります。
- 作業領域は、2 つの並べ替えられた配列のいずれかとオーバーラップできます。ただし、マージされていない要素が上書きされないようにする必要があります。
このマージ アルゴリズムが定義されているので、配列の半分を並べ替えることができるソリューションを想像するのは簡単です。次の問題は、以下に示すように、ワークエリアに格納された残りの未ソート部分をどのように処理するかです。
... unsorted 1/2 array ... | ... sorted 1/2 array ...
直観的なアイデアの 1 つは、作業領域の別の半分を再帰的に並べ替えることです。したがって、まだ並べ替えられていない要素は 1/4 だけです。
... unsorted 1/4 array ... | sorted 1/4 array B | sorted 1/2 array A ...
この段階での重要なポイントは、ソートされた 1/4 要素 B をソートされた 1/2 要素 A と遅かれ早かれマージする必要があるということです。
A と B をマージするのに十分な大きさの 1/4 要素のみを保持する作業領域が残っていますか? 残念ながら、そうではありません。
ただし、上記の 2 番目の制約は、マージされていない要素が上書きされないというマージ シーケンスを保証できれば、作業領域をいずれかのサブ配列とオーバーラップするように配置することで、それを悪用できるというヒントを与えてくれます。
実際には、作業領域の後半をソートする代わりに、前半をソートし、次のように 2 つのソートされた配列の間に作業領域を置くことができます。
... sorted 1/4 array B | unsorted work area | ... sorted 1/2 array A ...
このセットアップは、サブアレイ A とオーバーラップする作業領域を効果的に配置します。「実用的なインプレース マージソート」。Nordic Journal of Computing、1996]。
したがって、残っている唯一のことは、上記のステップを繰り返すことです。これにより、作業領域が 1/2、1/4、1/8 から減少します。作業領域が十分に小さくなったら (たとえば、要素が 2 つしか残っていない場合)、このアルゴリズムを終了するには、自明な挿入ソートに切り替えます。
これは、この論文に基づいた ANSI C での実装です。
void imsort(Key* xs, int l, int u);
void swap(Key* xs, int i, int j) {
Key tmp = xs[i]; xs[i] = xs[j]; xs[j] = tmp;
}
/*
* sort xs[l, u), and put result to working area w.
* constraint, len(w) == u - l
*/
void wsort(Key* xs, int l, int u, int w) {
int m;
if (u - l > 1) {
m = l + (u - l) / 2;
imsort(xs, l, m);
imsort(xs, m, u);
wmerge(xs, l, m, m, u, w);
}
else
while (l < u)
swap(xs, l++, w++);
}
void imsort(Key* xs, int l, int u) {
int m, n, w;
if (u - l > 1) {
m = l + (u - l) / 2;
w = l + u - m;
wsort(xs, l, m, w); /* the last half contains sorted elements */
while (w - l > 2) {
n = w;
w = l + (n - l + 1) / 2;
wsort(xs, w, n, l); /* the first half of the previous working area contains sorted elements */
wmerge(xs, l, l + n - w, n, u, w);
}
for (n = w; n > l; --n) /*switch to insertion sort*/
for (m = n; m < u && xs[m] < xs[m-1]; ++m)
swap(xs, m, m - 1);
}
}
wmerge が以前に定義されている場所。
完全なソースコードはここにあり、詳細な説明はここにあります
ところで、このバージョンはより多くのスワップ操作を必要とするため、最速のマージ ソートではありません。私のテストによると、再帰ごとに余分なスペースを割り当てる標準バージョンよりも高速です。ただし、元の配列を事前に 2 倍にし、それをさらにマージするために使用する最適化バージョンよりも低速です。