it-swarm.com.de

Wie sortiere ich direkt mit dem Merge-Sortier-Algorithmus?

Ich weiß, dass die Frage nicht zu spezifisch ist.

Ich möchte nur, dass mir jemand sagt, wie ich eine normale Zusammenführungssortierung in eine direkte Zusammenführungssortierung (oder eine Zusammenführungssortierung mit konstantem zusätzlichen Speicherplatzaufwand) umwandeln kann.

Alles, was ich (im Internet) finden kann, sind Seiten, auf denen steht "es ist zu komplex" oder "außerhalb des Geltungsbereichs dieses Textes".

Die einzigen bekannten Möglichkeiten zum Zusammenführen an Ort und Stelle (ohne zusätzlichen Platz) sind zu komplex, um auf ein praktisches Programm reduziert zu werden. (genommen von hier )

Auch wenn es zu komplex ist, was ist das grundlegende Konzept, wie die Zusammenführungssortierung an Ort und Stelle vorgenommen werden kann?

219
Lazer

Knuth ließ dies als Übung (Vol 3, 5.2.5). Es gibt eine direkte Zusammenführungssortierung. Es muss sorgfältig umgesetzt werden.

Erstens ist eine naive Zusammenführung an Ort und Stelle wie beschrieben hier nicht die richtige Lösung. Die Leistung wird auf O (N2) .

Die Idee ist, einen Teil des Arrays zu sortieren, während der Rest als Arbeitsbereich zum Zusammenführen verwendet wird.

Zum Beispiel als folgende Zusammenführungsfunktion.

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++);
}  

Es wird das Array xs verwendet, die beiden sortierten Sub-Arrays werden als Bereich [i, m) Bzw. [j, n) Dargestellt. Der Arbeitsbereich beginnt bei w. Vergleichen Sie mit dem Standard-Zusammenführungsalgorithmus, der in den meisten Lehrbüchern verwendet wird. Dieser Algorithmus tauscht den Inhalt zwischen dem sortierten Sub-Array und dem Arbeitsbereich aus. Infolgedessen enthält der vorherige Arbeitsbereich die zusammengeführten sortierten Elemente, während die im Arbeitsbereich gespeicherten vorherigen Elemente in die beiden untergeordneten Arrays verschoben werden.

Es gibt jedoch zwei Einschränkungen, die erfüllt sein müssen:

  1. Der Arbeitsbereich sollte innerhalb des Arrays liegen. Mit anderen Worten, es sollte groß genug sein, um Elemente aufzunehmen, die ausgetauscht werden, ohne dass ein Fehler außerhalb der Grenzen auftritt.
  2. Der Arbeitsbereich kann mit einem der beiden sortierten Arrays überlappt werden. Es sollte jedoch sichergestellt werden, dass keine nicht zusammengeführten Elemente überschrieben werden.

Wenn dieser Zusammenführungsalgorithmus definiert ist, ist es einfach, sich eine Lösung vorzustellen, die die Hälfte des Arrays sortieren kann. Die nächste Frage ist, wie mit dem Rest des unsortierten Teils umzugehen ist, der im Arbeitsbereich gespeichert ist, wie unten gezeigt:

... unsorted 1/2 array ... | ... sorted 1/2 array ...

Eine intuitive Idee ist es, eine weitere Hälfte des Arbeitsbereichs rekursiv zu sortieren, sodass nur 1/4 der Elemente noch nicht sortiert wurden.

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

Der entscheidende Punkt in dieser Phase ist, dass wir die sortierten 1/4 Elemente B früher oder später mit den sortierten 1/2 Elementen A zusammenführen müssen.

Ist der Arbeitsbereich, der nur noch 1/4 Elemente enthält, groß genug, um A und B zusammenzuführen? Leider ist es nicht.

Die zweite oben erwähnte Einschränkung gibt uns jedoch den Hinweis, dass wir sie ausnutzen können, indem wir den Arbeitsbereich so anordnen, dass er sich mit einem der Teilarrays überlappt, wenn wir die Zusammenführungsreihenfolge sicherstellen können, dass die nicht zusammengeführten Elemente nicht überschrieben werden.

Anstatt die zweite Hälfte des Arbeitsbereichs zu sortieren, können wir auch die erste Hälfte sortieren und den Arbeitsbereich wie folgt zwischen die beiden sortierten Arrays legen:

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

Diese Setup-Effekte ordnen die Überlappung des Arbeitsbereichs mit dem Subarray A an. Diese Idee wird in [Jyrki Katajainen, Tomi Pasanen, Jukka Teuhola. `` Praktisches In-Place-Mergesort ''. Nordic Journal of Computing, 1996].

Das Einzige, was noch übrig bleibt, ist, den obigen Schritt zu wiederholen, der den Arbeitsbereich von 1/2 , 1/4, 1/8 ... verkleinert. Wir können zu einer einfachen Einfügesorte wechseln, um diesen Algorithmus zu beenden.

Hier ist die Implementierung in ANSI C basierend auf diesem Dokument.

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);
    }
}

Wobei wmerge zuvor definiert wurde.

Den vollständigen Quellcode finden Sie hier und die ausführliche Erklärung hier

Übrigens ist diese Version nicht die schnellste Zusammenführungssorte, da mehr Swap-Operationen erforderlich sind. Meinem Test zufolge ist es schneller als die Standardversion, die bei jeder Rekursion zusätzliche Leerzeichen zuweist. Es ist jedoch langsamer als die optimierte Version, die das ursprüngliche Array im Voraus verdoppelt und zum weiteren Zusammenführen verwendet.

127
Larry LIU Xinyu

In diesem Artikel werden unter Einbeziehung des "großen Ergebnisses" einige Varianten der direkten Zusammenführungssortierung (PDF) beschrieben:

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

Direktes Sortieren mit weniger Zügen

Jyrki Katajainen, Tomi A. Pasanen

Es wird gezeigt, dass ein Array von n Elementen sortiert werden kann, indem O(1) zusätzlicher Speicherplatz, O (n log n/log log n) -Elementverschiebungen und n log verwendet werden2n + O (n log log n) Vergleiche. Dies ist der erste direkte Sortieralgorithmus, bei dem im ungünstigsten Fall o (n log n) Bewegungen erforderlich sind, um O (n log n) Vergleiche zu gewährleisten. Aufgrund der konstanten Faktoren ist der Algorithmus jedoch vorwiegend von theoretischem Interesse.

Ich denke, das ist auch relevant. Ich habe einen Ausdruck herumliegen, der von einem Kollegen an mich weitergeleitet wurde, aber ich habe ihn nicht gelesen. Es scheint die Grundtheorie zu behandeln, aber ich bin mit dem Thema nicht vertraut genug, um beurteilen zu können, wie umfassend:

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

Optimale stabile Verschmelzung

Antonios Symvonis

In diesem Artikel wird gezeigt, wie zwei Sequenzen A und B der Größen m und n, m ≤ n, mit O (m + n) -Zuweisungen, O (mlog (n/m + 1)) -Vergleichen und nur mit einer Konstanten stabil zusammengeführt werden können Menge zusätzlichen Platzes. Dieses Ergebnis stimmt mit allen bekannten Untergrenzen überein ...

57
Steve Jessop

Der entscheidende Schritt besteht darin, das Zusammenführen selbst in Kraft zu setzen. Es ist nicht so schwierig, wie diese Quellen erkennen, aber Sie verlieren etwas, wenn Sie es versuchen.

Einen Schritt der Zusammenführung betrachten:

[... liste -sortiert ... | x ... liste - A . .. | y ... liste - B ...]

Wir wissen, dass die sortierte Sequenz kleiner ist als alles andere, dass x kleiner ist als alles andere in A , und das y ist weniger als alles andere in B . In dem Fall, in dem x kleiner oder gleich y ist, bewegen Sie den Zeiger einfach an den Anfang von A auf eins. Wenn y kleiner als x ist, müssen Sie y über A bis sortiert. Dieser letzte Schritt macht das teuer (außer in entarteten Fällen).

Im Allgemeinen ist es billiger (insbesondere, wenn die Arrays nur einzelne Wörter pro Element enthalten, z. B. einen Zeiger auf eine Zeichenfolge oder Struktur), etwas Platz für Zeit abzuwägen und ein separates temporäres Array zu haben, zwischen dem Sie hin und her sortieren.

10
Donal Fellows

Es ist wirklich nicht einfach oder effizient, und ich schlage vor, Sie tun es nicht, es sei denn, Sie müssen es wirklich tun (und Sie müssen es wahrscheinlich nicht, es sei denn, es handelt sich um Hausaufgaben, da die Anwendungen der Inplace-Zusammenführung größtenteils theoretischer Natur sind). Kannst du nicht stattdessen Quicksort verwenden? Quicksort wird mit ein paar einfacheren Optimierungen sowieso schneller und sein zusätzlicher Speicher ist O (log N) .

Jedenfalls müssen Sie es tun, wenn Sie es tun müssen. Folgendes habe ich gefunden: eins und zwei . Ich bin mit der Inplace-Merge-Sortierung nicht vertraut, aber es scheint, als ob die Grundidee darin besteht, Rotationen zu verwenden, um das Zusammenführen von zwei Arrays zu vereinfachen, ohne zusätzlichen Speicher zu verwenden.

Beachten Sie, dass dies langsamer ist als die klassische Zusammenführungssortierung, die nicht vorhanden ist.

9
IVlad

Nur als Referenz, hier ist ein Nice Implementierung einer stabilen in-place Merge-Sortierung . Kompliziert, aber nicht schlecht.

Am Ende implementierte ich sowohl eine stabile In-Place-Merge-Sortierung als auch eine stabile In-Place-QuickSortierung in Java. Bitte beachten Sie, dass die Komplexität O (n (log n) ^ 2) ist.

8
Thomas Mueller

Ein Beispiel für eine pufferlose Zusammenführung in 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);
     }
}

Ein Beispiel für adaptives Mergesort (optimiert).

Fügt Support-Code und Änderungen hinzu, um die Zusammenführung zu beschleunigen, wenn ein Hilfspuffer beliebiger Größe verfügbar ist (funktioniert immer noch ohne zusätzlichen Speicher). Verwendet das Zusammenführen in Vorwärts- und Rückwärtsrichtung, die Ringrotation, das Zusammenführen und Sortieren kleiner Sequenzen und das iterative Zusammenführen.

#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

Dies ist meine C-Version:

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

Es gibt eine relativ einfache Implementierung der direkten Zusammenführungssortierung unter Verwendung der ursprünglichen Technik von Kronrod, jedoch mit einer einfacheren Implementierung. Ein Bildbeispiel, das diese Technik veranschaulicht, finden Sie hier: http://www.logiccoder.com/TheSortProblem/BestMergeInfo.htm .

Es gibt auch Links zu detaillierteren theoretischen Analysen desselben Autors, die mit diesem Link verknüpft sind.

1
Calbert

Diese Antwort hat ein Codebeispiel , das den im Artikel Praktisches Zusammenführen an Ort und Stelle von Bing-Chao Huang und Michael A beschriebenen Algorithmus implementiert. Langston. Ich muss zugeben, dass ich die Details nicht verstehe, aber die gegebene Komplexität des Zusammenführungsschritts ist O (n).

Aus praktischer Sicht gibt es Hinweise darauf, dass reine In-Place-Implementierungen in realen Szenarien keine bessere Leistung erbringen. Zum Beispiel definiert der C++ - Standard std :: inplace_merge , was bedeutet, dass der Name eine direkte Zusammenführungsoperation impliziert.

Unter der Annahme, dass C++ - Bibliotheken normalerweise sehr gut optimiert sind, ist es interessant zu sehen, wie sie implementiert werden:

1) libstdc ++ (Teil der GCC-Codebasis): std :: inplace_merge

Die Implementierung delegiert an __ inplace_merge , wodurch das Problem umgangen wird, indem versucht wird, einen temporären Puffer zuzuweisen:

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);

Andernfalls wird auf eine Implementierung ( __ merge_without_buffer ) zurückgegriffen, die keinen zusätzlichen Speicher benötigt, jedoch nicht mehr in der Zeit O(n) ausgeführt wird.

2) libc ++ (Teil der Clang-Codebasis): std :: inplace_merge

Sieht ähnlich aus. Es delegiert an eine Funktion , die ebenfalls versucht einen Puffer zuzuweisen . Abhängig davon, ob genügend Elemente vorhanden sind, wird die Implementierung ausgewählt. Die Konstantenspeicher-Fallback-Funktion heißt __ buffered_inplace_merge .

Vielleicht ist sogar der Fallback noch O(n) Zeit, aber der Punkt ist, dass sie die Implementierung nicht verwenden, wenn temporärer Speicher verfügbar ist.


Beachten Sie, dass der C++ - Standard Implementierungen ausdrücklich die Freiheit gibt, diesen Ansatz zu wählen, indem die erforderliche Komplexität von O(n) auf O (N log N) gesenkt wird:

Komplexität: Genau N-1-Vergleiche, wenn genügend zusätzlicher Speicher verfügbar ist. Wenn der Speicher nicht ausreicht, werden O (N log N) Vergleiche durchgeführt.

Dies kann natürlich nicht als Beweis dafür gewertet werden, dass konstanter Raum an Ort und Stelle in O(n) - Zeit verschmilzt. Wäre es dagegen schneller, würden die optimierten C++ - Bibliotheken wahrscheinlich auf diese Art der Implementierung umsteigen.

1
Philipp Claßen