39 votes

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

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

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

95voto

Wonko Points 1248
 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."

12voto

Jon Skeet Points 692016

Pourquoi pensez-vous qu'il serait inefficace? Pour autant que je suis conscient, à la fois Concat et Distincts, sont évalués paresseusement, à l'aide d'un HashSet derrière les scènes Distinctes pour garder une trace des éléments qui ont déjà été retournés.

Je ne suis pas sûr comment vous pouvez gérer pour le rendre plus efficaces que d'une manière générale :)

EDIT: Distinctes utilise Set (une classe interne) au lieu de HashSet, mais l'essentiel est toujours correcte. C'est vraiment un très bon exemple de la façon soignée LINQ est. La réponse la plus simple est à peu près aussi efficace que vous pouvez atteindre sans plus de connaissances 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;
        }
    }
}

3voto

TheSoftwareJedi Points 15921

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

2voto

Lasse V. Karlsen Points 148037

Avertissement C'est l'optimisation prématurée. Pour votre exemple des tableaux, utiliser le 3.5 méthodes d'extension. Jusqu'à ce que vous savez que vous avez un problème de performance dans cette région, vous devez utiliser le code de bibliothèque.


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

Ces va tirer un seul élément à la fois, et de produire le "plus" de l'élément, puis chercher un nouvel élément à partir de la source correspondante, jusqu'à ce que les deux sources sont épuisés. Dans le cas où l'élément actuel récupérée à partir de ces deux sources sont égaux, il va produire l'une à partir de la première source, et de les ignorer 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 les doublons dans la sortie. Si vous souhaitez supprimer ces doublons dans les listes 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'aucune de ces en interne l'utilisation d'une structure de données à conserver une copie des données, de sorte qu'ils seront bon marché si l'entrée est triée. Si vous ne pouvez pas ou ne voulez pas vous garantir que, vous devez utiliser l'3.5 méthodes d'extension que vous avez déjà trouvé.

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

1voto

petr k. Points 4890

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.

Prograide.com

Prograide est une communauté de développeurs qui cherche à élargir la connaissance de la programmation au-delà de l'anglais.
Pour cela nous avons les plus grands doutes résolus en français et vous pouvez aussi poser vos propres questions ou résoudre celles des autres.

Powered by:

X