web-dev-qa-db-fra.com

Quel est l'algorithme le plus rapide pour trier une liste chaînée?

Je suis curieux de savoir si O (n log n) est le meilleur qu'une liste chaînée puisse faire.

85
Dirk

Il est raisonnable de penser que vous ne pouvez pas faire mieux que O (N log N) dans durée

Cependant, la partie intéressante consiste à déterminer si vous pouvez le trier in-situ , de manière stable , son comportement dans le pire des cas, etc.

Simon Tatham, de la gloire PuTTY, explique comment trier une liste liée avec un tri par fusion . Il conclut avec les commentaires suivants:

Comme tout algorithme de tri qui se respecte, il a la durée O (N log N). Comme il s'agit de Mergesort, le temps d'exécution le plus défavorable est toujours O (N log N); il n'y a pas de cas pathologique.

Les besoins en mémoire auxiliaire sont faibles et constants (c’est-à-dire quelques variables dans la routine de tri). Grâce au comportement fondamentalement différent des listes chaînées des tableaux, cette implémentation de Mergesort évite le coût de stockage auxiliaire O(N) normalement associé à l'algorithme.

Il existe également un exemple d’implémentation en C qui fonctionne pour les listes à une ou deux listes liées.

Comme @ Jørgen Fogh le mentionne ci-dessous, la notation big-O peut masquer certains facteurs constants susceptibles de rendre un algorithme plus performant en raison de la localisation de la mémoire, du faible nombre d'éléments, etc.

86
csl

En fonction d'un certain nombre de facteurs, il peut s'avérer plus rapide de copier la liste dans un tableau, puis d'utiliser un Quicksort .

La raison pour laquelle cela pourrait être plus rapide est qu’un tableau a de bien meilleures performances de cache que une liste chaînée. Si les noeuds de la liste sont dispersés en mémoire, vous risquez de générer des erreurs de cache dans tous les endroits. Là encore, si le tableau est grand, vous obtiendrez de toute façon des erreurs de cache.

Mergesort se parallélise mieux, donc ce peut être un meilleur choix si c'est ce que vous voulez. C'est aussi beaucoup plus rapide si vous l'exécutez directement dans la liste chaînée.

Étant donné que les deux algorithmes fonctionnent en O (n * log n), prendre une décision en connaissance de cause implique de les profiler tous les deux sur la machine sur laquelle vous souhaitez les exécuter.

--- MODIFIER

J'ai décidé de tester mon hypothèse et d'écrire un programme en C qui mesurait le temps (en utilisant clock()) pour trier une liste chaînée d'ints. J'ai essayé avec une liste chaînée où chaque nœud était alloué avec malloc() et une liste chaînée où les nœuds étaient disposés de manière linéaire dans un tableau afin d'améliorer les performances du cache. J'ai comparé ceux-ci avec le qsort intégré, qui comprenait tout copier d'une liste fragmentée dans un tableau, puis recopier le résultat. Chaque algorithme a été exécuté sur les mêmes 10 ensembles de données et les résultats ont été moyennés.

Ce sont les résultats:

N = 1000:

Liste fragmentée avec tri par fusion: 0.000000 secondes

Tableau avec qsort: 0.000000 secondes

Liste compacte avec tri par fusion: 0.000000 secondes

N = 100000:

Liste fragmentée avec tri par fusion: 0,039 000 secondes

Tableau avec qsort: 0.025000 secondes

Liste compacte avec tri par fusion: 0,009000 secondes

N = 1000000:

Liste fragmentée avec tri par fusion: 1,622000 secondes

Tableau avec qsort: 0.420000 secondes

Liste compacte avec tri par fusion: 0,112 000 secondes

N = 100000000:

Liste fragmentée avec tri par fusion: 364,797000 secondes

Tableau avec qsort: 61.166000 secondes

Liste remplie avec tri par fusion: 16,525000 secondes

Conclusion:

Au moins sur ma machine, la copie dans un tableau en vaut vraiment la peine pour améliorer les performances du cache, car vous avez rarement une liste de liens complètement remplie dans la vie réelle. Il convient de noter que ma machine a un Phenom II à 2,8 GHz, mais seulement 0,6 GHz de RAM, le cache est donc très important.

66
Jørgen Fogh

Les tris de comparaison (c'est-à-dire basés sur des éléments de comparaison) ne peuvent pas être plus rapides que n log n. Peu importe la structure de données sous-jacente. Voir Wikipedia .

Les autres types de tri qui tirent parti de la présence d’un grand nombre d’éléments identiques dans la liste (tels que le tri par comptage) ou d’une distribution attendue des éléments de la liste sont plus rapides, bien que je ne puisse en penser aucun qui fonctionne particulièrement bien sur une liste chaînée.

6
Artelius

Comme indiqué à maintes reprises, la limite inférieure du tri fondé sur la comparaison pour les données générales sera O (n log n). Pour résumer brièvement ces arguments, il y a n! différentes manières de trier une liste. Toute sorte d'arborescence de comparaison qui a n! (qui est dans O (n ^ n)) les tris finaux possibles nécessiteront au moins log (n!) comme hauteur: cela vous donne une limite inférieure O (log (n ^ n)), qui est O (n log n). 

Ainsi, pour les données générales sur une liste chaînée, le meilleur tri possible pour toutes les données pouvant comparer deux objets sera O (n log n). Cependant, si vous avez un domaine de travail plus limité, vous pouvez améliorer le temps nécessaire (au moins proportionnel à n). Par exemple, si vous travaillez avec des nombres entiers ne dépassant pas une valeur, vous pouvez utiliser Tri par comptage ou Tri par radix , car ils utilisent les objets spécifiques que vous triez pour réduire la complexité avec proportion à n . Soyez prudent, cependant, ceci ajoute quelques éléments à la complexité que vous pouvez ne pas prendre en compte (par exemple, Compter Tri et Radix trient les facteurs d’addition basés sur la taille des nombres que vous triez, O (n + k ) où k est la taille du plus grand nombre pour le tri par comptage, par exemple).

De même, si vous avez des objets qui ont un hachage parfait (ou au moins un hachage qui mappe toutes les valeurs différemment), vous pouvez essayer d’utiliser un tri ou une sorte de base sur leurs fonctions de hachage.

5
DivineWolfwood

Ceci est un joli petit article sur ce sujet. Sa conclusion empirique est que Treesort est le meilleur, suivi de Quicksort et Mergesort. Le tri des sédiments, le tri à bulle, le tri à la sélection fonctionnent très mal.

ETUDE COMPARATIVE DES ALGORITHMES DE TRI DE LISTE LIEEby Ching-Kuang Shene

http://citeseerx.ist.psu.edu/viewdoc/summary?doi=10.1.1.31.9981

5
Neal Richter

Le type Radix sort convient particulièrement à une liste chaînée, car il est facile de créer un tableau de pointeurs en tête correspondant à chaque valeur possible d'un chiffre.

3
Mark Ransom

Le tri par fusion ne nécessite pas d'accès O(1) et correspond à O (n ln n). Aucun algorithme connu pour le tri des données générales n'est meilleur que O (n ln n).

Les algorithmes de données spéciaux tels que le tri de base (taille limite des données) ou l’histogramme (comptage de données discrètes) peuvent trier une liste chaînée avec une fonction de croissance plus faible, à condition que vous utilisiez une structure différente avec O(1) accès en tant que stockage temporaire. 

Une autre classe de données spéciales est une sorte de comparaison d'une liste presque triée avec k éléments en désordre. Ceci peut être trié en opérations O (kn).

Copier la liste dans un tableau et vice-versa serait O (N), ainsi tout algorithme de tri peut être utilisé si l'espace n'est pas un problème.

Par exemple, étant donné une liste chaînée contenant uint_8, ce code le triera dans O(N) fois en utilisant un tri par histogramme:

#include <stdio.h>
#include <stdint.h>
#include <malloc.h>

typedef struct _list list_t;
struct _list {
    uint8_t value;
    list_t  *next;
};


list_t* sort_list ( list_t* list )
{
    list_t* heads[257] = {0};
    list_t* tails[257] = {0};

    // O(N) loop
    for ( list_t* it = list; it != 0; it = it -> next ) {
        list_t* next = it -> next;

        if ( heads[ it -> value ] == 0 ) {
            heads[ it -> value ] = it;
        } else {
            tails[ it -> value ] -> next = it;
        }

        tails[ it -> value ] = it;
    }

    list_t* result = 0;

    // constant time loop
    for ( size_t i = 255; i-- > 0; ) {
        if ( tails[i] ) {
            tails[i] -> next = result;
            result = heads[i];
        }
    }

    return result;
}

list_t* make_list ( char* string )
{
    list_t head;

    for ( list_t* it = &head; *string; it = it -> next, ++string ) {
        it -> next = malloc ( sizeof ( list_t ) );
        it -> next -> value = ( uint8_t ) * string;
        it -> next -> next = 0;
    }

    return head.next;
}

void free_list ( list_t* list )
{
    for ( list_t* it = list; it != 0; ) {
        list_t* next = it -> next;
        free ( it );
        it = next;
    }
}

void print_list ( list_t* list )
{
    printf ( "[ " );

    if ( list ) {
        printf ( "%c", list -> value );

        for ( list_t* it = list -> next; it != 0; it = it -> next )
            printf ( ", %c", it -> value );
    }

    printf ( " ]\n" );
}


int main ( int nargs, char** args )
{
    list_t* list = make_list ( nargs > 1 ? args[1] : "wibble" );


    print_list ( list );

    list_t* sorted = sort_list ( list );


    print_list ( sorted );

    free_list ( list );
}
2
Pete Kirkham

Ce n’est pas une réponse directe à votre question, mais si vous utilisez un Skip List , il est déjà trié et a une durée de recherche O (journal N).

1
Mitch Wheat

Comme je le sais, le meilleur algorithme de tri est O (n * log n), quel que soit le conteneur. Il a été prouvé que le tri au sens large du mot Word (style mergesort/quicksort, etc.) ne peut pas baisser. L'utilisation d'une liste chaînée ne vous donnera pas un meilleur temps d'exécution. 

Le seul algorithme qui fonctionne dans O(n) est un algorithme de "piratage" qui repose sur le comptage des valeurs plutôt que sur le tri.

1
laura

Voici une implémentation qui parcourt la liste une seule fois, en collectant les exécutions, puis planifie les fusions de la même manière que mergesort.

La complexité est O (n log m) où n est le nombre d'éléments et m le nombre de passages. Le meilleur cas est O(n) (si les données sont déjà triées) et le pire des cas est O (n log n) comme prévu.

Il nécessite O (log m) mémoire temporaire; le tri est fait sur place sur les listes.

(mis à jour ci-dessous. commenter on fait un bon point que je devrais le décrire ici)

Le Gist de l'algorithme est:

    while list not empty
        accumulate a run from the start of the list
        merge the run with a stack of merges that simulate mergesort's recursion
    merge all remaining items on the stack

Cumuler des séries n’exige pas beaucoup d’explications, mais il est bon de saisir l’occasion d’accumuler des séries croissantes et décroissantes (en sens inverse). Ici, il ajoute des éléments plus petits que la tête de la séquence et ajoute des éléments supérieurs ou égaux à la fin de la séquence. (Notez que le préfixe doit utiliser des méthodes strictes inférieures à pour préserver la stabilité du tri.)

Il est plus facile de simplement coller le code de fusion ici:

    int i = 0;
    for ( ; i < stack.size(); ++i) {
        if (!stack[i])
            break;
        run = merge(run, stack[i], comp);
        stack[i] = nullptr;
    }
    if (i < stack.size()) {
        stack[i] = run;
    } else {
        stack.Push_back(run);
    }

Pensez à trier la liste (ignorer les pistes). Les états de la pile se déroulent comme suit:

    [ ]
    [ (d) ]
    [ () (a d) ]
    [ (g), (a d) ]
    [ () () (a d g i) ]
    [ (b) () (a d g i) ]
    [ () (b e) (a d g i) ]
    [ (c) (b e) (a d g i ) ]
    [ () () () (a b c d e f g i) ]
    [ (j) () () (a b c d e f g i) ]
    [ () (h j) () (a b c d e f g i) ]

Enfin, fusionnez toutes ces listes.

Notez que le nombre d'éléments (exécutions) à la pile [i] est zéro ou 2 ^ i et que la taille de la pile est limitée par 1 + log2 (nruns). Chaque élément est fusionné une fois par niveau de pile, d’où des comparaisons entre O (n log m). Il y a une similarité passagère avec Timsort ici, bien que Timsort conserve sa pile en utilisant quelque chose comme une séquence de Fibonacci où cela utilise des puissances de deux.

Les analyses cumulatives tirent parti des données déjà triées pour que la complexité optimale soit de O(n) pour une liste déjà triée (une analyse). Etant donné que nous accumulons des analyses ascendantes et descendantes, les analyses auront toujours au moins la longueur 2. (Cela réduit la profondeur maximale de la pile d'au moins une, en prenant en charge le coût de recherche des analyses.) O (n log n), comme prévu, pour les données hautement aléatoires.

(Um ... Deuxième mise à jour.)

Ou juste voir wikipedia sur bottom-up mergesort .

1
Stan Switzer

Mergesort est le meilleur que vous puissiez faire ici.

0
ypnos

Vous pouvez le copier dans un tableau, puis le trier. 

  • Copier dans le tableau O (n),

  • trier O(nlgn) (si vous utilisez un algorithme rapide comme le tri par fusion),

  • recopier dans la liste chaînée O(n) si nécessaire,

donc ça va être O (nlgn).

notez que si vous ne connaissez pas le nombre d'éléments dans la liste liée, vous ne saurez pas la taille du tableau. Si vous codez en Java, vous pouvez utiliser un Arraylist par exemple. 

0
Shirin