web-dev-qa-db-fra.com

Fusionner efficacement les tableaux de chaînes dans .NET, en conservant des valeurs distinctes

J'utilise .NET 3.5. J'ai deux tableaux de chaînes, qui peuvent partager une ou plusieurs valeurs:

string[] list1 = new string[] { "Apple", "orange", "banana" };
string[] list2 = new string[] { "banana", "pear", "grape" };

Je voudrais un moyen de les fusionner en un seul tableau sans valeurs en double:

{ "Apple", "orange", "banana", "pear", "grape" }

Je peux le faire avec LINQ:

string[] result = list1.Concat(list2).Distinct().ToArray();

mais j'imagine que ce n'est pas très efficace pour les grands tableaux.

Y a-t-il une meilleure façon?

40
Jason Anderson
string[] result = list1.Union(list2).ToArray();

from msdn : "Cette méthode exclut les doublons de l'ensemble de retours. Il s'agit d'un comportement différent de la méthode Concat (TSource), qui renvoie tous les éléments des séquences d'entrée, y compris les doublons."

97
Wonko

Pourquoi pensez-vous que ce serait inefficace? Pour autant que je sache, Concat et Distinct sont évalués paresseusement, en utilisant un HashSet en arrière-plan pour Distinct pour garder une trace des éléments qui ont déjà été retournés.

Je ne sais pas comment vous pourriez le rendre plus efficace que cela de manière générale :)

EDIT: Distinct utilise en fait Set (une classe interne) au lieu de HashSet, mais le Gist est toujours correct. Ceci est un très bon exemple de la netteté de LINQ. La réponse la plus simple est à peu près aussi efficace que possible sans plus de connaissance du domaine.

L'effet est l'équivalent de:

public static IEnumerable<T> DistinctConcat<T>(IEnumerable<T> first, IEnumerable<T> second)
{
    HashSet<T> returned = new HashSet<T>();
    foreach (T element in first)
    {
        if (returned.Add(element))
        {
            yield return element;
        }
    }
    foreach (T element in second)
    {
        if (returned.Add(element))
        {
            yield return element;
        }
    }
}
12
Jon Skeet

.NET 3.5 a introduit la classe HashSet qui pourrait faire ceci:

IEnumerable<string> mergedDistinctList = new HashSet<string>(list1).Union(list2);

Je ne suis pas sûr des performances, mais il devrait battre l'exemple Linq que vous avez donné.

EDIT: Je me tiens corrigé. L'implémentation paresseuse de Concat et Distinct a un avantage clé en termes de mémoire ET de vitesse. Concat/Distinct est environ 10% plus rapide et enregistre plusieurs copies de données.

J'ai confirmé par code:

Setting up arrays of 3000000 strings overlapping by 300000
Starting Hashset...
HashSet: 00:00:02.8237616
Starting Concat/Distinct...
Concat/Distinct: 00:00:02.5629681

est la sortie de:

        int num = 3000000;
        int num10Pct = (int)(num / 10);

        Console.WriteLine(String.Format("Setting up arrays of {0} strings overlapping by {1}", num, num10Pct));
        string[] list1 = Enumerable.Range(1, num).Select((a) => a.ToString()).ToArray();
        string[] list2 = Enumerable.Range(num - num10Pct, num + num10Pct).Select((a) => a.ToString()).ToArray();

        Console.WriteLine("Starting Hashset...");
        Stopwatch sw = new Stopwatch();
        sw.Start();
        string[] merged = new HashSet<string>(list1).Union(list2).ToArray();
        sw.Stop();
        Console.WriteLine("HashSet: " + sw.Elapsed);

        Console.WriteLine("Starting Concat/Distinct...");
        sw.Reset();
        sw.Start();
        string[] merged2 = list1.Concat(list2).Distinct().ToArray();
        sw.Stop();
        Console.WriteLine("Concat/Distinct: " + sw.Elapsed);
3
TheSoftwareJedi

Disclaimer Il s'agit d'une optimisation prématurée. Pour vos exemples de tableaux, utilisez les méthodes d'extension 3.5. Jusqu'à ce que vous sachiez que vous avez un problème de performances dans cette région, vous devez utiliser le code de bibliothèque.


Si vous pouvez trier les tableaux, ou qu'ils sont triés lorsque vous arrivez à ce point dans le code, vous pouvez utiliser les méthodes suivantes.

Ceux-ci tireront un élément des deux et produiront l'élément "le plus bas", puis récupéreront un nouvel élément de la source correspondante, jusqu'à ce que les deux sources soient épuisées. Dans le cas où l'élément actuel extrait des deux sources est égal, il produira celui de la première source et les ignorera dans les deux sources.

private static IEnumerable<T> Merge<T>(IEnumerable<T> source1,
    IEnumerable<T> source2)
{
    return Merge(source1, source2, Comparer<T>.Default);
}

private static IEnumerable<T> Merge<T>(IEnumerable<T> source1,
    IEnumerable<T> source2, IComparer<T> comparer)
{
    #region Parameter Validation

    if (Object.ReferenceEquals(null, source1))
        throw new ArgumentNullException("source1");
    if (Object.ReferenceEquals(null, source2))
        throw new ArgumentNullException("source2");
    if (Object.ReferenceEquals(null, comparer))
        throw new ArgumentNullException("comparer");

    #endregion

    using (IEnumerator<T>
        enumerator1 = source1.GetEnumerator(),
        enumerator2 = source2.GetEnumerator())
    {
        Boolean more1 = enumerator1.MoveNext();
        Boolean more2 = enumerator2.MoveNext();

        while (more1 && more2)
        {
            Int32 comparisonResult = comparer.Compare(
                enumerator1.Current,
                enumerator2.Current);
            if (comparisonResult < 0)
            {
                // enumerator 1 has the "lowest" item
                yield return enumerator1.Current;
                more1 = enumerator1.MoveNext();
            }
            else if (comparisonResult > 0)
            {
                // enumerator 2 has the "lowest" item
                yield return enumerator2.Current;
                more2 = enumerator2.MoveNext();
            }
            else
            {
                // they're considered equivalent, only yield it once
                yield return enumerator1.Current;
                more1 = enumerator1.MoveNext();
                more2 = enumerator2.MoveNext();
            }
        }

        // Yield rest of values from non-exhausted source
        while (more1)
        {
            yield return enumerator1.Current;
            more1 = enumerator1.MoveNext();
        }
        while (more2)
        {
            yield return enumerator2.Current;
            more2 = enumerator2.MoveNext();
        }
    }
}

Notez que si l'une des sources contient des doublons, vous pouvez voir des doublons dans la sortie. Si vous souhaitez supprimer ces doublons dans les listes déjà triées, utilisez la méthode suivante:

private static IEnumerable<T> CheapDistinct<T>(IEnumerable<T> source)
{
    return CheapDistinct<T>(source, Comparer<T>.Default);
}

private static IEnumerable<T> CheapDistinct<T>(IEnumerable<T> source,
    IComparer<T> comparer)
{
    #region Parameter Validation

    if (Object.ReferenceEquals(null, source))
        throw new ArgumentNullException("source");
    if (Object.ReferenceEquals(null, comparer))
        throw new ArgumentNullException("comparer");

    #endregion

    using (IEnumerator<T> enumerator = source.GetEnumerator())
    {
        if (enumerator.MoveNext())
        {
            T item = enumerator.Current;

            // scan until different item found, then produce
            // the previous distinct item
            while (enumerator.MoveNext())
            {
                if (comparer.Compare(item, enumerator.Current) != 0)
                {
                    yield return item;
                    item = enumerator.Current;
                }
            }

            // produce last item that is left over from above loop
            yield return item;
        }
    }
}

Notez qu'aucun d'entre eux n'utilisera en interne une structure de données pour conserver une copie des données, ils seront donc bon marché si l'entrée est triée. Si vous ne pouvez pas ou ne voulez pas garantir cela, vous devez utiliser les méthodes d'extension 3.5 que vous avez déjà trouvées.

Voici un exemple de code qui appelle les méthodes ci-dessus:

String[] list_1 = { "Apple", "orange", "Apple", "banana" };
String[] list_2 = { "banana", "pear", "grape" };

Array.Sort(list_1);
Array.Sort(list_2);

IEnumerable<String> items = Merge(
    CheapDistinct(list_1),
    CheapDistinct(list_2));
foreach (String item in items)
    Console.Out.WriteLine(item);
2
Lasse V. Karlsen

La création d'une table de hachage avec vos valeurs sous forme de clés (en ajoutant uniquement celles qui ne sont pas déjà présentes) puis en convertissant les clés en tableau pourrait être une solution viable.

1
petr k.

Vous ne savez pas quelle approche est la plus rapide jusqu'à ce que vous la mesuriez. La méthode LINQ est élégante et facile à comprendre.

Une autre façon consiste à implémenter un ensemble en tant que tableau de hachage (dictionnaire) et à ajouter tous les éléments des deux tableaux à l'ensemble. Utilisez ensuite la méthode set.Keys.ToArray () pour créer le tableau résultant.

1
danatel