web-dev-qa-db-ja.com

マージソートアルゴリズムを使用してインプレースでソートする方法は?

質問があまり具体的ではないことは知っています。

私が欲しいのは、通常のマージソートをインプレースマージソート(または一定の余分なスペースオーバーヘッドを持つマージソート)に変換する方法を教えてくれる人だけです。

(ネット上で)見つけられるのは、「複雑すぎる」または「このテキストの範囲外」と言っているページだけです。

インプレースで(余分なスペースなしで)マージする唯一の既知の方法は、実用的なプログラムに縮小するには複雑すぎます。 (撮影 ここから

複雑すぎてもマージソートをインプレースにする基本的な概念は何ですか?

216
Lazer

Knuthはこれを演習として残しました(第3巻、5.2.5)。インプレースマージソートが存在します。慎重に実装する必要があります。

まず、 here のような単純なインプレースマージは適切な解決策ではありません。パフォーマンスをO(N2

アイデアは、残りをマージの作業領域として使用しながら、配列の一部をソートすることです。

たとえば、次のマージ関数として。

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つの制約を満たす必要があります。

  1. 作業領域は配列の境界内にある必要があります。言い換えれば、範囲外のエラーを引き起こすことなく、交換された要素を保持するのに十分な大きさでなければなりません。
  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にマージする必要があることです。

1/4の要素のみを保持し、AとBを結合するのに十分な大きさの作業領域が残っていますか?残念ながら、そうではありません。

ただし、上記の2番目の制約は、マージされていない要素が上書きされないようにマージシーケンスを確保できる場合、作業領域をいずれかのサブ配列とオーバーラップするように配置することで活用できるというヒントを提供します。

実際、作業領域の後半を並べ替える代わりに、前半を並べ替えて、次のように2つの並べ替えられた配列の間に作業領域を配置できます。

... sorted 1/4 array B | unsorted work area | ... sorted 1/2 array A ...

このセットアップ効果は、作業領域をサブ配列Aとオーバーラップさせます。このアイデアは、[Jyrki Katajainen、Tomi Pasanen、Jukka Teuhola。 「実用的なインプレースマージソート」 Nordic Journal of Computing、1996]。

したがって、残っているのは上記の手順を繰り返すことです。これにより、作業領域が1/2、1/4、1/8から減少します。簡単な挿入ソートに切り替えて、このアルゴリズムを終了できます。

このペーパーに基づいた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が以前に定義されている場所。

完全なソースコードは here にあり、詳細な説明は here にあります。

ところで、このバージョンはより多くのスワップ操作を必要とするため、最速のマージソートではありません。私のテストによると、すべての再帰で余分なスペースを割り当てる標準バージョンよりも高速です。ただし、最適化されたバージョンよりも遅く、事前に元の配列を2倍にして、それをさらにマージするために使用します。

124
Larry LIU Xinyu

「大きな成果」を含め、このペーパーでは、インプレースマージソート(PDF)のいくつかのバリエーションについて説明します。

http://citeseerx.ist.psu.edu/viewdoc/download?doi=10.1.1.22.5514&rep=rep1&type=pdf

より少ない移動でのインプレースソート

ジルキ・カタヤイネン、トミ・A・パサネン

O(1)余分なスペース、O(n log n/log log n)要素の移動、およびn logを使用して、n要素の配列をソートできることが示されています。2n + O(n log log n)比較。これは、O(n log n)の比較を保証しながら、最悪の場合にo(n log n)の移動を必要とする最初のインプレースソートアルゴリズムですが、関与する一定の要因により、アルゴリズムは主に理論的に興味深いものです。

これも関連があると思います。同僚から渡されたプリントアウトが横たわっていますが、まだ読んでいません。それは基本的な理論をカバーしているようですが、私はどのくらい包括的に判断するトピックに十分に精通していません:

http://comjnl.oxfordjournals.org/cgi/content/abstract/38/8/681

最適な安定マージ

アントニオス・シンボニス

この論文では、O(m + n)の割り当て、O(mlog(n/m + 1))の比較、および定数のみを使用して、サイズmおよびn、m≤nの2つのシーケンスAおよびBをそれぞれ安定してマージする方法を示します追加スペースの量。この結果は、既知のすべての下限と一致します...

57
Steve Jessop

重要なステップは、merge自体をインプレースにすることです。これらの情報源が示すほど難しくはありませんが、試してみると何かを失います。

マージの1つのステップを見る:

[... list-sorted... |x.. .list-A... |y... list-B...]

sortedシーケンスが他のすべてのものよりも少ないこと、つまりxAの他のすべてよりも小さく、そのyBの他のすべてよりも少ない。 xy以下の場合、ポインタをAの先頭に移動するだけです。 yxより小さい場合、シャッフルy全体Atosorted。その最後のステップは、これを高価にするものです(縮退した場合を除く)。

一般に、(特に、配列が実際に要素ごとに単一の単語、たとえば文字列または構造体へのポインターのみを含む場合)いくらかのスペースを時間と引き換えに、前後に並べ替える別個の一時配列を用意する方が安価です。

10
Donal Fellows

本当に簡単でも効率的でもありません。本当に必要な場合を除き、実行しないことをお勧めします(そして、インプレースマージのアプリケーションはほとんど理論的であるため、これが宿題でない限りおそらく実行する必要はありません)。代わりにクイックソートを使用できませんか?とにかくいくつかのより簡単な最適化により、クイックソートはより高速になり、その追加メモリはO(log N)です。

とにかく、あなたがそれをしなければならないなら、あなたはしなければなりません。ここに私が見つけたものがあります: onetwo 。私はインプレースマージソートに慣れていませんが、基本的な考え方は、余分なメモリを使用せずに2つの配列のマージを容易にするために回転を使用することです。

これは、インプレースではない従来のマージソートよりも遅いことに注意してください。

9
IVlad

参考までに、ここにニース 安定したインプレースマージソートの実装 を示します。複雑ですが、それほど悪くはありません。

Javaで 安定したインプレースマージソート安定したインプレースクイックソート の両方を実装することになりました。複雑さはO(n(log n)^ 2)であることに注意してください

8
Thomas Mueller

Cのバッファレスマージソートの例.

#define SWAP(type, a, b) \
    do { type t=(a);(a)=(b);(b)=t; } while (0)

static void reverse_(int* a, int* b)
{
    for ( --b; a < b; a++, b-- )
       SWAP(int, *a, *b);
}
static int* rotate_(int* a, int* b, int* c)
/* swap the sequence [a,b) with [b,c). */
{
    if (a != b && b != c)
     {
       reverse_(a, b);
       reverse_(b, c);
       reverse_(a, c);
     }
    return a + (c - b);
}

static int* lower_bound_(int* a, int* b, const int key)
/* find first element not less than @p key in sorted sequence or end of
 * sequence (@p b) if not found. */
{
    int i;
    for ( i = b-a; i != 0; i /= 2 )
     {
       int* mid = a + i/2;
       if (*mid < key)
          a = mid + 1, i--;
     }
    return a;
}
static int* upper_bound_(int* a, int* b, const int key)
/* find first element greater than @p key in sorted sequence or end of
 * sequence (@p b) if not found. */
{
    int i;
    for ( i = b-a; i != 0; i /= 2 )
     {
       int* mid = a + i/2;
       if (*mid <= key)
          a = mid + 1, i--;
     }
    return a;
}

static void ip_merge_(int* a, int* b, int* c)
/* inplace merge. */
{
    int n1 = b - a;
    int n2 = c - b;

    if (n1 == 0 || n2 == 0)
       return;
    if (n1 == 1 && n2 == 1)
     {
       if (*b < *a)
          SWAP(int, *a, *b);
     }
    else
     {
       int* p, * q;

       if (n1 <= n2)
          p = upper_bound_(a, b, *(q = b+n2/2));
       else
          q = lower_bound_(b, c, *(p = a+n1/2));
       b = rotate_(p, b, q);

       ip_merge_(a, p, b);
       ip_merge_(b, q, c);
     }
}

void mergesort(int* v, int n)
{
    if (n > 1)
     {
       int h = n/2;
       mergesort(v, h); mergesort(v+h, n-h);
       ip_merge_(v, v+h, v+n);
     }
}

適応マージソートの例(最適化)。

サポートコードと変更を追加して、任意のサイズの補助バッファーが使用可能な場合にマージを高速化します(メモリを追加しなくても機能します)。前方および後方のマージ、リングの回転、小さなシーケンスのマージとソート、および反復マージソートを使用します。

#include <stdlib.h>
#include <string.h>

static int* copy_(const int* a, const int* b, int* out)
{
    int count = b - a;
    if (a != out)
       memcpy(out, a, count*sizeof(int));
    return out + count;
}
static int* copy_backward_(const int* a, const int* b, int* out)
{
    int count = b - a;
    if (b != out)
       memmove(out - count, a, count*sizeof(int));
    return out - count;
}

static int* merge_(const int* a1, const int* b1, const int* a2,
  const int* b2, int* out)
{
    while ( a1 != b1 && a2 != b2 )
       *out++ = (*a1 <= *a2) ? *a1++ : *a2++;
    return copy_(a2, b2, copy_(a1, b1, out));
}
static int* merge_backward_(const int* a1, const int* b1,
  const int* a2, const int* b2, int* out)
{
    while ( a1 != b1 && a2 != b2 )
       *--out = (*(b1-1) > *(b2-1)) ? *--b1 : *--b2;
    return copy_backward_(a1, b1, copy_backward_(a2, b2, out));
}

static unsigned int gcd_(unsigned int m, unsigned int n)
{
    while ( n != 0 )
     {
       unsigned int t = m % n;
       m = n;
       n = t;
     }
    return m;
}
static void rotate_inner_(const int length, const int stride,
  int* first, int* last)
{
    int* p, * next = first, x = *first;
    while ( 1 )
     {
       p = next;
       if ((next += stride) >= last)
          next -= length;
       if (next == first)
          break;
       *p = *next;
     }
    *p = x;
}
static int* rotate_(int* a, int* b, int* c)
/* swap the sequence [a,b) with [b,c). */
{
    if (a != b && b != c)
     {
       int n1 = c - a;
       int n2 = b - a;

       int* i = a;
       int* j = a + gcd_(n1, n2);

       for ( ; i != j; i++ )
          rotate_inner_(n1, n2, i, c);
     }
    return a + (c - b);
}

static void ip_merge_small_(int* a, int* b, int* c)
/* inplace merge.
 * @note faster for small sequences. */
{
    while ( a != b && b != c )
       if (*a <= *b)
          a++;
       else
        {
          int* p = b+1;
          while ( p != c && *p < *a )
             p++;
          rotate_(a, b, p);
          b = p;
        }
}
static void ip_merge_(int* a, int* b, int* c, int* t, const int ts)
/* inplace merge.
 * @note works with or without additional memory. */
{
    int n1 = b - a;
    int n2 = c - b;

    if (n1 <= n2 && n1 <= ts)
     {
       merge_(t, copy_(a, b, t), b, c, a);
     }
    else if (n2 <= ts)
     {
       merge_backward_(a, b, t, copy_(b, c, t), c);
     }
    /* merge without buffer. */
    else if (n1 + n2 < 48)
     {
       ip_merge_small_(a, b, c);
     }
    else
     {
       int* p, * q;

       if (n1 <= n2)
          p = upper_bound_(a, b, *(q = b+n2/2));
       else
          q = lower_bound_(b, c, *(p = a+n1/2));
       b = rotate_(p, b, q);

       ip_merge_(a, p, b, t, ts);
       ip_merge_(b, q, c, t, ts);
     }
}
static void ip_merge_chunk_(const int cs, int* a, int* b, int* t,
  const int ts)
{
    int* p = a + cs*2;
    for ( ; p <= b; a = p, p += cs*2 )
       ip_merge_(a, a+cs, p, t, ts);
    if (a+cs < b)
       ip_merge_(a, a+cs, b, t, ts);
}

static void smallsort_(int* a, int* b)
/* insertion sort.
 * @note any stable sort with low setup cost will do. */
{
    int* p, * q;
    for ( p = a+1; p < b; p++ )
     {
       int x = *p;
       for ( q = p; a < q && x < *(q-1); q-- )
          *q = *(q-1);
       *q = x;
     }
}
static void smallsort_chunk_(const int cs, int* a, int* b)
{
    int* p = a + cs;
    for ( ; p <= b; a = p, p += cs )
       smallsort_(a, p);
    smallsort_(a, b);
}

static void mergesort_lower_(int* v, int n, int* t, const int ts)
{
    int cs = 16;
    smallsort_chunk_(cs, v, v+n);
    for ( ; cs < n; cs *= 2 )
       ip_merge_chunk_(cs, v, v+n, t, ts);
}

static void* get_buffer_(int size, int* final)
{
    void* p = NULL;
    while ( size != 0 && (p = malloc(size)) == NULL )
       size /= 2;
    *final = size;
    return p;
}
void mergesort(int* v, int n)
{
    /* @note buffer size may be in the range [0,(n+1)/2]. */
    int request = (n+1)/2 * sizeof(int);
    int actual;
    int* t = (int*) get_buffer_(request, &actual);

    /* @note allocation failure okay. */
    int tsize = actual / sizeof(int);
    mergesort_lower_(v, n, t, tsize);
    free(t);
}
4
Johnny Cage

これは私のCバージョンです。

void mergesort(int *a, int len) {
  int temp, listsize, xsize;

  for (listsize = 1; listsize <= len; listsize*=2) {
    for (int i = 0, j = listsize; (j+listsize) <= len; i += (listsize*2), j += (listsize*2)) {
      merge(& a[i], listsize, listsize);
    }
  }

  listsize /= 2;

  xsize = len % listsize;
  if (xsize > 1)
    mergesort(& a[len-xsize], xsize);

  merge(a, listsize, xsize);
}

void merge(int *a, int sizei, int sizej) {
  int temp;
  int ii = 0;
  int ji = sizei;
  int flength = sizei+sizej;

  for (int f = 0; f < (flength-1); f++) {
    if (sizei == 0 || sizej == 0)
      break;

    if (a[ii] < a[ji]) {
      ii++;
      sizei--;
    }
    else {
      temp = a[ji];

      for (int z = (ji-1); z >= ii; z--)
        a[z+1] = a[z];  
      ii++;

      a[f] = temp;

      ji++;
      sizej--;
    }
  }
}
2
Dylan Nissley

Kronrodのオリジナルのテクニックを使用してインプレースマージソートを比較的単純に実装しますが、実装はより単純です。この手法を説明する図の例は、ここにあります: http://www.logiccoder.com/TheSortProblem/BestMergeInfo.htm

このリンクに関連する同じ著者によるより詳細な理論的分析へのリンクもあります。

1
Calbert

この回答 には コード例 があり、これは論文で説明されているアルゴリズムを実装しています 実用的なインプレースマージ Bing-Chao HuangとMichael Aによってラングストン。私は詳細を理解していないことを認めざるを得ませんが、マージ手順の複雑さはO(n)です。

実用的な観点から、純粋なインプレース実装は実際のシナリオではパフォーマンスが良くないという証拠があります。たとえば、C++標準では std :: inplace_merge を定義しています。これは、名前がインプレースマージ操作を意味するためです。

C++ライブラリは通常非常に最適化されていると仮定すると、どのように実装されているかを見るのは興味深いことです。

1)libstdc ++(GCCコードベースの一部): std :: inplace_merge

実装は __ inplace_merge に委任します。これにより、一時バッファーを割り当てようとすることで問題を回避できます。

typedef _Temporary_buffer<_BidirectionalIterator, _ValueType> _TmpBuf;
_TmpBuf __buf(__first, __len1 + __len2);

if (__buf.begin() == 0)
  std::__merge_without_buffer
    (__first, __middle, __last, __len1, __len2, __comp);
else
  std::__merge_adaptive
   (__first, __middle, __last, __len1, __len2, __buf.begin(),
     _DistanceType(__buf.size()), __comp);

それ以外の場合、実装にフォールバックします( __ merge_without_buffer )。これにより、余分なメモリは不要ですが、O(n)時間で実行されなくなります。

2)libc ++(Clangコードベースの一部): std :: inplace_merge

似ています。 function に委任します。これは バッファの割り当て を試みます。十分な要素を取得したかどうかに応じて、実装を選択します。定数メモリフォールバック関数は __ buffered_inplace_merge と呼ばれます。

フォールバックでさえO(n)時間であるかもしれませんが、ポイントは一時メモリが利用可能な場合、実装を使用しないことです。


C++標準では、必要な複雑さをO(n)からO(N log N)に下げることで、このアプローチを選択する自由を実装に明示的に与えることに注意してください。

複雑さ:十分な追加メモリが利用可能な場合、正確にN-1回の比較。メモリが不足している場合、O(N log N)比較。

もちろん、これはO(n)時間での定スペースのインプレースマージが決して使用されるべきではないという証拠としてとることはできません。一方で、高速化された場合、最適化されたC++ライブラリはおそらくそのタイプの実装に切り替わります。

1
Philipp Claßen