11
votes

C # Intersection la plus rapide de 2 séries de nombres triés

Je calcule l'intersection de 2 séries de nombres triés dans une partie critique de mon application. Ce calcul est le plus gros goulot d'étranglement de l'application entière afin que je dois accélérer.

J'ai essayé un tas d'options simples et je suis actuellement en utilisant ce: p> xxx pré>

fiancset code> et secondset code> est de type liste. P>

J'ai également essayé d'utiliser LINQ: P>

var intersection = firstSet.Where(t => secondSet.BinarySearch(t) >= 0).ToList();


1 commentaires

A décidé de créer la question Union , compte tenu de savoir ce que j'ai mis en œuvre: Stackoverflow.com/questions/7165152/...


5 Réponses :


0
votes

essayer

fiancsset.Itersect (secondse) .tolist ()

ou

premiersset.join (secondset, o => o, id => ID, (o, id) => o)


0 commentaires

5
votes

Vous utilisez une méthode linq plutôt inefficace pour ce type de tâche, vous devez opter pour intersect code> comme point de départ.

var intersection = firstSet.Intersect(secondSet);


4 commentaires

J'ai essayé cela auparavant, mais si je me souviens bien, cela a travaillé correctement, les 2 versions supérieures. Quoi qu'il en soit, je pense que la solution de Jon Skeet est aussi rapide que possible.


@gligoran, c'est curieux. Mon propre test de la recherche où / binaire vs intersect a observé un énorme gain de performance pour les grands ensembles de données, mais je vous laisserai à vous rappeler vos propres observations en utilisant vos propres données réelles.


J'ai un grand jeu de données, mais l'intersection se produit profondément dans l'algorithme. Je crois donc toujours 2 ensembles d'environ 50 éléments, mais je dois le faire environ 5,3 millions de fois. L'intersecte d'afaik n'exige pas des ensembles à trier pour que cela puisse faire 2500 passes de la double boucle, où la version de Jon Skeet le fait dans 50 si je suis correct. L'où / binaire aurait probablement besoin de 50 * log_2 (50) qui est juste de moins de 300 ans. Mais je peux me tromper.


@gligoran, intersect serait l'équivalent de chargement du premier réglé dans un hashset (1 passage), puis de le vérifier pour l'existence de chaque élément du deuxième ensemble (1 passage). Les recherches dans le hashset auraient O (1) complexité. Il devrait être relativement optimal, mais pas tout aussi optimal que l'approche de Jon dans ce cas où vous avez affaire à des données déjà triées.



27
votes

Si vous avez deux ensembles qui sont à la fois triés, vous pouvez mettre en œuvre une intersection plus rapide que tout prévu sur la boîte avec LINQ.

En gros, garder deux IEnumerator code> curseurs ouverts, un pour chaque ensemble. À tout moment, l'avance selon celui qui a la plus petite valeur. S'ils correspondent à n'importe quel point, avancez-les à la fois, etc. jusqu'à ce que vous atteigniez la fin de l'un ou l'autre itérateur. P>

La bonne chose à ce sujet est que vous n'avez besoin que d'itérer sur chaque ensemble une fois, et vous pouvez . le faire en O (1) mémoire p>

Voici une exemple d'implémentation - non testé, mais il ne compile :) il suppose que les deux séquences entrantes sont sans doublons et triés, à la fois selon le comparateur prévu (passe dans Comparer .Default code>): p>

(Il y a plus de texte à la fin de la réponse!) p>

static IEnumerable<T> IntersectSorted<T>(this IEnumerable<T> sequence1,
    IEnumerable<T> sequence2,
    IComparer<T> comparer)
{
    using (var cursor1 = sequence1.GetEnumerator())
    using (var cursor2 = sequence2.GetEnumerator())
    {
        if (!cursor1.MoveNext() || !cursor2.MoveNext())
        {
            yield break;
        }
        var value1 = cursor1.Current;
        var value2 = cursor2.Current;

        while (true)
        {
            int comparison = comparer.Compare(value1, value2);
            if (comparison < 0)
            {
                if (!cursor1.MoveNext())
                {
                    yield break;
                }
                value1 = cursor1.Current;
            }
            else if (comparison > 0)
            {
                if (!cursor2.MoveNext())
                {
                    yield break;
                }
                value2 = cursor2.Current;
            }
            else
            {
                yield return value1;
                if (!cursor1.MoveNext() || !cursor2.MoveNext())
                {
                    yield break;
                }
                value1 = cursor1.Current;
                value2 = cursor2.Current;
            }
        }
    }
}


12 commentaires

Ceci est vraiment similaire à Mergesort Algorithm.


Il ne renvoie pas la vraie intersection, vous supposez que les listes ont la même longueur et une (ou les deux) n'est pas vide.


NOOOO, ma croyance de Jon Skeet a été secouée, car je pense qu'il y a un bug. L'entrée des séquences {1,2} et {0,2} renvoie la séquence {1,2} mais ne devraient que revenir {2}. Le bug mineur est la ligne int Value2 = cursor1.current; bien sûr que le 1 doit être un 2 dans cette ligne.


@Jonathan: Je ne pense pas que je suppose que l'une de ces personnes. Si soit soit vide, il s'arrêtera immédiatement en raison des premiers appels à MOVENNEXT () . Après cela, chaque itération ne fait que faire avancer un curseur à moins d'une correspondance, auquel cas il avance les deux. Essayez!


@Jbsnorro: corrigé, merci. J'ai dit que c'était non testé :) Est-ce que le reste semble bien?


@Jon, oui, le reste semble parfait. Comme vous êtes gentil de votre demande de confirmation


Merci beaucoup Jon. Cela améliore la vitesse de ma vitesse.


Serais-je correct en disant qu'une recherche binaire à l'élément adjacent suivant serait plus rapide que le nombre inférieur de l'ensemble inférieur est inférieur à Log2 (n) de l'autre?


@SpenCeRoser: Je ne te sens pas vraiment, j'ai peur - mais je soupçonne pas.


Le cas que j'essaie de résoudre est dit 3 éléments dans une liste et 80 000 dans l'autre. Il doit y avoir un point où itératant sur toute la collection 80K est plus lent que la recherche binaire de la liste plus grande pour ce qui est dans la liste plus petite.


@Spencerrose: Ah, je vois ce que tu veux dire. Désolé, j'ai été confondu par la partie "prochain élément adjacent". Oui, dans ce cas, il serait plus rapide de faire une recherche binaire - supposant que vous ayez un accès aléatoire à la plus grande collection. (Ma solution ici fonctionne avec des séquences arbitraires, mais dans de nombreux cas, vous aurait avoir un accès aléatoire.) En fait, avec un accès aléatoire, vous pouvez faire mieux que de faire une recherche binaire naïve à chaque fois. Va éditer.


@ Jonskeet limitant les recherches résultantes aiderait beaucoup! Je vais ajouter ça à ma solution :) acclamations



8
votes

Puisque les deux listes sont triées, vous pouvez arriver à la solution en itération sur eux au plus une fois (vous pouvez également passer à une partie d'une liste, en fonction des valeurs réelles qu'ils contiennent).

Cette solution conserve un "Pointeur" de la partie de la liste que nous n'avons pas encore examiné et compare le premier numéro non examiné de chaque liste entre eux. Si l'un est plus petit que l'autre, le pointeur sur la liste qu'il appartient est incrémenté pour pointer vers le numéro suivant. S'ils sont égaux, le nombre est ajouté au résultat d'intersection et les deux pointeurs sont incrémentés. P>

var firstCount = firstSet.Count;
var secondCount = secondSet.Count;
int firstIndex = 0, secondIndex = 0;
var intersection = new List<int>();

while (firstIndex < firstCount && secondIndex < secondCount)
{
    var comp = firstSet[firstIndex].CompareTo(secondSet[secondIndex]);
    if (comp < 0) {
        ++firstIndex;
    }
    else if (comp > 0) {
        ++secondIndex;
    }
    else {
        intersection.Add(firstSet[firstIndex]);
        ++firstIndex;
        ++secondIndex;
    }
}


3 commentaires

Yup, c'est fondamentalement la version non diffusante de mon approche - bien que vous supposez que comparèteo renvoie toujours -1, 0 ou 1, plutôt que les conditions "moins de 0", 0, et "plus de 0".


@Jonskeet: vrai. Bug de style C y a-t-il aussi. :) corrigé, merci de l'avoir tapé.


+1 pour une bonne approche. Merci. Le vôtre était un peu plus facile à comprendre et serait formidable si je voulais le fusionner avec du code. On peut par exemple faire le traitement d'une certaine sorte au lieu d'ajouter la valeur à la liste d'intersection.



2
votes

J'utilisais l'approche de Jon, mais j'avais besoin d'exécuter ces centaines de milliers de fois pour une opération en vrac sur de très grands ensembles et avait besoin de plus de performances. Le cas que j'exécutitais était fortement déséquilibré des listes (par exemple 5 et 80 000) et souhaitais éviter de itération de toute la grande liste.

J'ai trouvé que la détection du déséquilibre et changeant à un algorithme alternatif m'a donné d'énormes benifits sur Ensembles de données spécifiques: p>

public static IEnumerable<T> IntersectSorted<T>(this List<T> sequence1,
        List<T> sequence2,
        IComparer<T> comparer)
{
    List<T> smallList = null;
    List<T> largeList = null;

    if (sequence1.Count() < Math.Log(sequence2.Count(), 2))
    {
        smallList = sequence1;
        largeList = sequence2;
    }
    else if (sequence2.Count() < Math.Log(sequence1.Count(), 2))
    {
        smallList = sequence2;
        largeList = sequence1;
    }

    if (smallList != null)
    {
        foreach (var item in smallList)
        {
            if (largeList.BinarySearch(item, comparer) >= 0)
            {
                yield return item;
            }
        }
    }
    else
    {
        //Use Jon's method
    }
}


2 commentaires

Je pense que c'est une approche raisonnable, cependant: * Vous appelez comptent () 4 fois sur ienumerables , ce qui pourrait être assez coûteux s'ils ne sont pas une liste ou similaire; Sans parler de multiples énumération. * Cela pourrait être amélioré en faisant le deuxième si un sinon si . * binaireSearch () n'est pas disponible pour ienumerable , donc cela ne fonctionne que si landgelist est une liste ou un < code> tableau ; Changez que c'est le type ou le jetez lorsque cela est nécessaire.


Mis à jour pour utiliser des listes à la place