9
votes

String triant plus rapide avec un préfixe commun?

J'ai un ensemble de chaînes. 90% d'entre eux sont des URL commencent par "http: // www." . Je veux les trier alphabétiquement.

Actuellement, j'utilise C ++ STD :: Trier (). Mais STD :: Trier est une variante de tri rapide basée sur la comparaison et la comparaison de deux chaînes avec un préfixe long commun n'est pas efficace. Cependant (je pense) une sorte de radix ne fonctionnera pas non plus, car la plupart des cordes sont placées dans le même seau à cause d'un préfixe commun long.

Y a-t-il un meilleur algorithme que la normale Quick-Tri / Radix-Tri pour ce problème?


2 commentaires

Serait-il possible de supprimer le préfixe avant de trier?


Il pourrait y avoir une efficacité supplémentaire avec un arbre radix.


6 Réponses :


3
votes

Les préfixes communs semblent naturellement impliquer qu'une structure de données trie pourrait être utile. Donc, l'idée est de construire une trie de tous les mots, puis de trier chaque nœud. La commande devrait être que les enfants d'un nœud particulier résident dans une liste et sont triés. Cela peut être fait facilement depuis à un noeud particulier, nous n'avons besoin que de trier les enfants, donc naturellement une solution récursive se révèle. Consultez ceci pour plus d'inspiration: http://goanna.cs.rmit. Edu.au/~jz/fulltext/acsc03sz.pdf


1 commentaires

Je me demande diviser une chaîne en nœuds d'arbres aiderait vraiment. Les nœuds d'arbres font un accès beaucoup plus continu. Je ne veux pas éviter le pire des cas O (Nlogn * Len). Je crois que Trick-Tri encore plus vite qu'une solution à base d'arbres.



2
votes

Créer deux groupes: ceux avec le préfixe et ceux sans. Pour le premier ensemble, supprimez le préfixe, triez et ajoutez le préfixe. Pour le deuxième jeu, il suffit de trier. Après cela, divisez la seconde réglée dans avant le préfixe et après le préfixe. Maintenant, concaténer les trois listes (list_2_before, list_1, list_2_aducter).

Au lieu de supprimer et d'ajouter du préfixe pour la première liste, vous pouvez écrire votre propre code personnalisé qui démarrerait comparer après le préfixe (c'est-à-dire ignorer la partie de préfixe lors de la comparaison).

Addenda: Si vous avez plusieurs préfixes communes, vous pouvez les utiliser plus loin pour accélérer. Il est préférable de créer un arbre peu profond avec des préfixes très courants et de les rejoindre.


3 commentaires

Cela aidera à améliorer les performances. Mais les URL peuvent être divisées en groupes plus petits tels que http://www.google.com/xxxx ou http://yahoo.com/xxxx . Pouvons-nous l'améliorer plus loin?


Vous pouvez améliorer plus loin que le nombre de préfixes à gérer. Pour un cas plus général, je vous recommande d'utiliser une structure arborescente très peu profonde (par exemple, une trie taillée) pour garder une trace de divers préfixes.


Vous pouvez remplacer le préfixe avec un seul caractère attribué, mais vous auriez des difficultés à maintenir l'ordre de tri vrai des chaînes non préfixées (ce qui aurait vraisemblablement un caractère attribué «sans préfixe»). Est-il important d'être trié par ordre alphabétique?



4
votes

Je soupçonnerais que le temps de traitement que vous passez à essayer d'exploiter des préfixes communs sur l'ordre de 10 caractères par URL ne paie même pas lui-même lorsque vous envisagez la longueur moyenne des URL.

Essayez simplement un type complètement standard. Si ce n'est pas assez rapide, regardez la paralLement ou la distribution d'un type complètement standard. C'est une approche simple qui fonctionnera.


2 commentaires

Traiter un nombre fixe de préfixes est un processus O (n); Le tri (n logn) nécessite des comparaisons O (n logn). Pour suffisamment grand N, ceci est certainement une bonne idée, mais la question est de savoir la taille de ne pas atteindre une pause-même


"Le traitement des préfixes est un processus O (n)" - est-ce? Cela dépend de quel traitement vous faites. Bien que vous puissiez avoir raison que théoriquement, il pourrait y avoir une complexité plus faible de faire quelque chose de plus intelligent, je donnais des conseils pratiques. Par exemple: Théoriquement, Tri radix est O (N) (facilement moins de 100 caractères URL valides), alors pourquoi regarder ailleurs?



2
votes

Si vous découvrez les valeurs minimales et maximales dans le vecteur avant de démarrer QuickSorting, vous connaissez toujours la plage de valeurs pour chaque appel à partition (), puisque la valeur de partition est soit au minimum ou au maximum (ou à moins près du minimum / maximum) de chaque sous-bande et le minimum de la partition contenant la partition et le maximum sont l'autre extrémité de chaque sous-bande.

Si le minimum et le maximum d'une sous-groupe partagent un préfixe commune, vous pouvez faire toutes les comparaisons de partition à partir de la position de caractères suivant le préfixe commune. Comme le progresse QuicksTort, les gammes deviennent plus petites et plus petites, de sorte que leurs préfixes communs devraient devenir plus longues et plus longues et les ignorer pour que les comparaisons économiseront de plus en plus de temps. Combien, je ne sais pas; Vous devriez comparer cela pour voir s'il aide réellement.

En tout état de cause, les frais généraux supplémentaires sont assez petits; On passe à travers le vecteur pour trouver la chaîne minimale et maximale, coûtant 1,5 comparaisons par chaîne (*), puis une seule vérification de chaque partition pour trouver le préfixe partagé maximum pour la partition; Le chèque est équivalent à une comparaison et peut commencer à partir du préfixe partagé maximal du préfixe contenant, de sorte qu'il n'est même pas une comparaison complète de chaînes.


  • L'algorithme min / max: scannez le vecteur deux éléments à la fois. Pour chaque paire, comparez-les d'abord les uns avec les autres, puis comparez le plus petit avec le minimum de fonctionnement et le plus grand avec le maximum de fonctionnement. Résultat: trois comparaisons pour deux éléments ou 1,5 comparaisons par élément.

1 commentaires

C'est un peu difficile à suivre, mais je crois que c'est la meilleure réponse. Le noyau de l'idée, pour moi, est qu'une fois que vous avez partitionné la liste plus d'une fois, vous pouvez trouver les préfixes courants des éléments de partition adjacents et sachez que les éléments entre les partagent que le préfixe.



2
votes

enfin, j'ai trouvé un Ternary Quick Trict Sort Strong> fonctionne bien. J'ai trouvé l'algorithme à www.larsson.dogma.net/qsufsort.c .

Voici mon implémentation modifiée, avec une interface similaire à STD :: Trier. Il est environ 40% plus rapide que STD :: Trier sur ma machine et ensemble de données. P>

#include <iterator>

template<class RandIt> static inline void multiway_qsort(RandIt beg, RandIt end, size_t depth = 0, size_t step_len = 6) {
    if(beg + 1 >= end) {
        return;
    }

    struct { /* implement bounded comparing */
        inline int operator() (
                const typename std::iterator_traits<RandIt>::value_type& a,
                const typename std::iterator_traits<RandIt>::value_type& b, size_t depth, size_t step_len) {

            for(size_t i = 0; i < step_len; i++) {
                if(a[depth + i] == b[depth + i] && a[depth + i] == 0) return 0;
                if(a[depth + i] <  b[depth + i]) return +1;
                if(a[depth + i] >  b[depth + i]) return -1;
            }
            return 0;
        }
    } bounded_cmp;

    RandIt i = beg;
    RandIt j = beg + std::distance(beg, end) / 2;
    RandIt k = end - 1;

    typename std::iterator_traits<RandIt>::value_type key = ( /* median of l,m,r */
            bounded_cmp(*i, *j, depth, step_len) > 0 ?
            (bounded_cmp(*i, *k, depth, step_len) > 0 ? (bounded_cmp(*j, *k, depth, step_len) > 0 ? *j : *k) : *i) :
            (bounded_cmp(*i, *k, depth, step_len) < 0 ? (bounded_cmp(*j, *k, depth, step_len) < 0 ? *j : *k) : *i));

    /* 3-way partition */
    for(j = i; j <= k; ++j) {
        switch(bounded_cmp(*j, key, depth, step_len)) {
            case +1: std::iter_swap(i, j); ++i;      break;
            case -1: std::iter_swap(k, j); --k; --j; break;
        }
    }
    ++k;

    if(beg + 1 < i) multiway_qsort(beg, i, depth, step_len); /* recursively sort [x > pivot] subset */
    if(end + 1 > k) multiway_qsort(k, end, depth, step_len); /* recursively sort [x < pivot] subset */

    /* recursively sort [x == pivot] subset with higher depth */
    if(i < k && (*i)[depth] != 0) {
        multiway_qsort(i, k, depth + step_len, step_len);
    }
    return;
}


6 commentaires

Je l'aime bien! Je n'avais pas entendu parler de cela auparavant. Comment cela aide-t-il avec le problème du préfixe?


@TigRIGT dans la normale Quick-Tri, il prend une heure (Nlogn * l), où L est la longueur moyenne du préfixe commune. Dans Terneary Quick-Tri nous prenons O (N) pour la partition et passer à une profondeur plus grande, le déplacement prend des heures. Donc, la pire performance est O (n * l).


@richselian je pense que quelque chose ne va pas avec votre grosse analyse. Les partitions gauche et droite sont triées comme dans Quicksort, car la «profondeur» n'a pas augmenté. Les éléments de la partition du milieu peuvent être considérés comme étant étiquetés QuicksTort sur des chaînes plus courtes que nous voyons peut être comparé plus rapidement dans la pratique, mais le temps de comparaison est considéré comme constant de toute façon, sauf si nous voulons introduire un terme pour la longueur moyenne de la chaîne.


@Joelnelson, vous pouvez obtenir une provision stricte à Larsson.dogma.net/ssrev-tr. PDF . Cet algorithme fait partie du Qsufsort de Jesper Larsson.


@richselian merci. La façon dont je le lis, il dit que c'est O (n logn). O (n * l) est clairement meilleur que O (n logn), il serait donc tout à fait quelque chose si o (n * l) était vraiment le pire des cas. +1 pour un algorithme très cool. Il se peut que ce soit le tri le plus rapide dans ce scénario. L'amélioration de 40% ne pointe pas d'une amélioration asymptotique.


@Joelnelson, peut-être que je ne fais pas autant d'optimiser que dans STD :: Trier (comme swictement à insert_sort pour une petite taille). J'ai téléchargé des témoignages à Github.com/richox/multiway-qsort .



1
votes

Le document ingénierie parallèle chaîne tri a, de façon surprenante, les repères d'un grand nombre de monothread algorithmes sur un ensemble de données d'URL (voir page 29). La variante de Rantala de multitouche quicksort mise en cache est en avance; vous pouvez tester multikey_cache8 ce référentiel cité dans le papier.

Je l'ai testé l'ensemble de données dans ce document, et si elle est une indication que vous voyez à peine un peu d'entropie dans les premiers caractères dix et préfixes distinguer dans la gamme de 100 caractères. Faire 100 passes de tri radix débattront le cache avec peu d'avantages, par exemple le tri d'un million de moyens urls que vous cherchez ~ 20 bits distinctifs de chaque clé à un coût de défauts de cache ~ 100.

Alors qu'en général, sorte radix ne peut pas bien sur les longues chaînes, les optimisations qui Kärkkäinen et Rantala décrivent Ingénierie Radix Trier pour cordes suffisent pour l'ensemble de données d'URL. En particulier, ils lisent avant 8 caractères et les mettre en cache avec les pointeurs de chaîne; le tri sur ces valeurs mises en cache donne suffisamment d'entropie pour dépasser le problème cache-miss.

Pour les chaînes essayer certains des algorithmes basés sur LCP-dans ce dépôt; dans mon expérience l'ensemble de données d'URL est sur le point au point de rupture même entre sortes radix de type hautement optimisés et des algorithmes à base de LCP qui ne asymptotiquement mieux sur des chaînes plus longues.


0 commentaires