web-dev-qa-db-fra.com

Comment trier sur place à l'aide de l'algorithme de tri par fusion?

Je sais que la question n'est pas trop spécifique.

Tout ce que je veux, c'est que quelqu'un me dise comment convertir une sorte de fusion normale en une sorte de fusion sur place (ou une sorte de fusion avec une surcharge d'espace supplémentaire constante).

Tout ce que je peux trouver (sur le net), ce sont des pages disant "c'est trop complexe" ou "hors de portée de ce texte".

Les seules façons connues de fusionner sur place (sans espace supplémentaire) sont trop complexes pour être réduites à un programme pratique. (pris à partir d'ici )

Même si c'est trop complexe, , quel est le concept de base sur la manière de rendre le tri par fusion in-situ?

219
Lazer

Knuth a laissé cela comme un exercice (Vol 3, 5.2.5). Il existe un tri par fusion sur place. Il doit être mis en œuvre avec soin.

Premièrement, une fusion sur place naïve telle que décrite ici n'est pas la bonne solution. Il rétrograde la performance à O (N2) .

L'idée est de trier une partie du tableau en utilisant le reste comme zone de travail pour la fusion.

Par exemple, en tant que fonction de fusion suivante.

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

Il prend le tableau xs, les deux sous-tableaux triés sont représentés respectivement par les plages [i, m) et [j, n). La zone de travail commence par w. Comparez avec l'algorithme de fusion standard donné dans la plupart des manuels, celui-ci échange le contenu entre le sous-tableau trié et la zone de travail. En conséquence, la zone de travail précédente contient les éléments triés fusionnés, tandis que les éléments précédents stockés dans la zone de travail sont déplacés vers les deux sous-tableaux.

Cependant, deux contraintes doivent être satisfaites:

  1. La zone de travail doit être dans les limites du tableau. En d’autres termes, il devrait être suffisamment grand pour contenir des éléments échangés sans causer d’erreur erronée;
  2. La zone de travail peut se chevaucher avec l’un ou l’autre des deux tableaux triés, mais il convient de s’assurer qu’aucun élément non fusionné n’est écrasé;

Avec cet algorithme de fusion défini, il est facile d’imaginer une solution capable de trier la moitié du tableau; La question suivante est de savoir comment traiter le reste de la pièce non triée stockée dans la zone de travail, comme indiqué ci-dessous:

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

Une idée intuitive est de trier de manière récursive une autre moitié de la zone de travail, ainsi il n'y a que 1/4 des éléments qui n'ont pas encore été triés.

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

Le point clé à ce stade est que nous devons tôt ou tard fusionner les 1/4 éléments B triés avec les 1/2 éléments A triés.

La zone de travail, qui ne contient que 1/4 d’éléments, est-elle suffisante pour fusionner A et B? Malheureusement, ça ne l'est pas.

Cependant, la deuxième contrainte mentionnée ci-dessus nous indique que nous pouvons l'exploiter en faisant en sorte que la zone de travail chevauche l'un ou l'autre des sous-tableaux si nous pouvons garantir à la séquence de fusion que les éléments non fusionnés ne seront pas écrasés.

En fait, au lieu de trier la seconde moitié de la zone de travail, nous pouvons trier la première moitié et placer la zone de travail entre les deux tableaux triés comme suit:

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

Ces effets de configuration organisent le chevauchement de la zone de travail avec le sous-réseau A. Cette idée est proposée dans [Jyrki Katajainen, Tomi Pasanen, Jukka Teuhola. `` Pratique in-situ mergesort ''. Nordic Journal of Computing, 1996].

Il ne reste donc qu’à répéter l’étape ci-dessus, ce qui réduit la zone de travail de 1/2 , 1/4, 1/8 ..., lorsque la zone de travail devient suffisamment petite, par exemple, il ne reste plus que deux éléments, nous pouvons passer à un type d'insertion trivial pour mettre fin à cet algorithme.

Voici la mise en œuvre dans ANSI C basée sur ce document.

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

Où wmerge est défini précédemment.

Le code source complet peut être trouvé ici et l'explication détaillée peut être trouvé ici

À propos, cette version n'est pas la sorte de fusion la plus rapide car elle nécessite davantage d'opérations de swap. Selon mon test, il est plus rapide que la version standard, qui alloue des espaces supplémentaires à chaque récursion. Mais elle est plus lente que la version optimisée, qui double le tableau d'origine à l'avance et l'utilise pour une fusion plus poussée.

127
Larry LIU Xinyu

Y compris son "grand résultat", cet article décrit quelques variantes du type de fusion sur place (PDF):

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

Tri sur place avec moins de déplacements

Jyrki Katajainen, Tomi A. Pasanen

Il est montré qu’un tableau de n éléments peut être trié en utilisant O(1) espace supplémentaire, O (n log n/log log n) élément se déplace et n log2comparaisons n + O (n log log n). Il s'agit du premier algorithme de tri sur place nécessitant des déplacements de o (n log n) dans le pire des cas tout en garantissant des comparaisons de O (n log n), mais en raison des facteurs constants impliqués, l'algorithme présente un intérêt essentiellement théorique.

Je pense que cela est également pertinent. J'en ai une copie imprimée qui m'a été remise par un collègue, mais je ne l'ai pas lue. Cela semble couvrir la théorie de base, mais je ne connais pas suffisamment le sujet pour en juger de manière exhaustive:

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

Fusion optimale optimale

Antonios Symvonis

Cet article montre comment fusionner de manière stable deux séquences A et B de tailles m et n, m ≤ n, respectivement, avec des affectations O (m + n), des comparaisons O (mlog (n/m + 1)) et en utilisant uniquement une constante. quantité d'espace supplémentaire. Ce résultat correspond à toutes les limites inférieures connues ...

57
Steve Jessop

L'étape critique consiste à obtenir le fusion lui-même sur place. Ce n'est pas aussi difficile que le prouvent ces sources, mais vous perdez quelque chose lorsque vous essayez.

En regardant une étape de la fusion:

[... liste -trié ... | x ... liste -A ... | y ... liste -B ...]

Nous savons que la séquence triée est inférieure à tout le reste, que x est inférieure à tout le reste de A, et que y est inférieur à tout le reste de B. Dans le cas où x est inférieur ou égal à y, vous déplacez simplement le pointeur au début de A sur un. Dans le cas où y est inférieur à x, vous devez mélanger y après la totalité de A à trié. Cette dernière étape est ce qui rend cela coûteux (sauf dans les cas dégénérés).

C’est généralement moins cher (surtout lorsque les tableaux ne contiennent en réalité que des mots par élément, par exemple un pointeur sur une chaîne ou une structure) pour échanger de l’espace contre du temps et disposer d’un tableau temporaire séparé.

10
Donal Fellows

Ce n'est vraiment ni facile ni efficace, et je suggère de ne pas le faire à moins que ce ne soit nécessaire (et probablement pas à moins que ce soit un devoir, car les applications de fusion inplace sont principalement théoriques). Ne pouvez-vous pas utiliser quicksort à la place? Quicksort sera quand même plus rapide avec quelques optimisations plus simples et sa mémoire supplémentaire est O (log N) .

Quoi qu'il en soit, si vous devez le faire, alors vous devez. Voici ce que j'ai trouvé: n et deux . Je ne suis pas familier avec le type de fusion inplace, mais il semble que l'idée de base consiste à utiliser des rotations pour faciliter la fusion de deux tableaux sans utiliser de mémoire supplémentaire.

Notez que cela est plus lent que le type de fusion classique qui n’est pas en place.

9
IVlad

Juste pour référence, voici un Nice implémentation d'un type de fusion sur place stable . Compliqué, mais pas trop mal.

J'ai fini par implémenter à la fois un tri de fusion sur place stable et un tri rapide sur place stable en Java. Veuillez noter que la complexité est O (n (log n) ^ 2)

8
Thomas Mueller

Un exemple de mergesort sans tampon en 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);
     }
}

Un exemple de mergesort adaptatif (optimisé).

Ajoute du code de support et des modifications pour accélérer la fusion lorsqu'un tampon auxiliaire de toute taille est disponible (fonctionne toujours sans mémoire supplémentaire). Utilise la fusion en avant et en arrière, la rotation des anneaux, la fusion et le tri de petites séquences et le fusionnement itératif.

#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

Ceci est ma version 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

Il existe une implémentation relativement simple du tri par fusion sur place utilisant la technique originale de Kronrod mais avec une implémentation plus simple. Un exemple illustré illustrant cette technique est disponible à l'adresse suivante: http://www.logiccoder.com/TheSortProblem/BestMergeInfo.htm .

Il existe également des liens vers une analyse théorique plus détaillée du même auteur associée à ce lien.

1
Calbert

Cette réponse a un exemple de code , qui implémente l'algorithme décrit dans l'article Fusion pratique sur place par Bing-Chao Huang et Michael A. Langston. Je dois admettre que je ne comprends pas les détails, mais la complexité de l’étape de fusion est O (n).

D'un point de vue pratique, il est évident que les implémentations sur place pures ne fonctionnent pas mieux dans des scénarios réels. Par exemple, la norme C++ définit std :: inplace_merge , comme son nom l'indique pour une opération de fusion sur place.

En supposant que les bibliothèques C++ sont généralement très bien optimisées, il est intéressant de voir comment elles sont implémentées:

1) libstdc ++ (partie de la base de code GCC): std :: inplace_merge

Les délégués d’implémentation à __ inplace_merge , qui esquivent le problème en essayant d’allouer un tampon temporaire:

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

Sinon, il retombe sur une implémentation ( __ merge_without_buffer ), qui ne nécessite aucune mémoire supplémentaire, mais ne s'exécute plus dans le temps O(n).

2) libc ++ (partie de la base de code Clang): std :: inplace_merge

Ressemble semblable. Il délègue à une fonction , qui tente également de allouer un tampon . Selon qu’il possède suffisamment d’éléments, il choisira la mise en œuvre. La fonction de repli de mémoire constante est appelée __ buffered_inplace_merge .

Peut-être que même le repli est toujours O(n) temps, mais le fait est qu'ils n'utilisent pas l'implémentation si de la mémoire temporaire est disponible.


Notez que la norme C++ donne explicitement aux implémentations la liberté de choisir cette approche en abaissant la complexité requise de O(n) à O (N log N):

Complexité: Exactement N-1 comparaisons si suffisamment de mémoire supplémentaire est disponible. Si la mémoire est insuffisante, comparaisons O (N log N).

Bien entendu, cela ne peut pas être considéré comme une preuve que la fusion d'espace constant sur place dans O(n) ne devrait jamais être utilisée. D'un autre côté, si cela devait être plus rapide, les bibliothèques C++ optimisées passeraient probablement à ce type d'implémentation.

1
Philipp Claßen