2008-09-28 6 views
37

J'utilise .NET 3.5. J'ai deux tableaux de chaînes, qui peuvent partager une ou plusieurs valeurs:Fusionner efficacement des tableaux de chaînes dans .NET, en conservant des valeurs distinctes

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 avec aucune valeur 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 grandes baies.

Y a-t-il un meilleur moyen?

Répondre

88
string[] result = list1.Union(list2).ToArray(); 

de msdn:. « Cette méthode exclut les doublons de retour fixé Ce comportement est différent de la méthode concat (TSource), qui renvoie tous les éléments dans les séquences d'entrée, y compris les doublons. "

+2

Je suis revenu à ce sujet pour poster exactement cette solution. C'est idéal dans tous les sens, je crois! –

+5

Un point mineur, mais le type de retour de Union est IEnumerable , donc vous devrez ajouter un ToArray() pour obtenir la chaîne [] –

+0

Ceci est toujours utile 10 ans après: D – Jen

1

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

2

Clause de non-responsabilité Ceci est une optimisation prématurée. Pour vos tableaux d'exemple, utilisez les méthodes d'extension 3.5. Jusqu'à ce que vous sachiez que vous avez un problème de performance dans cette région, vous devez utiliser le code de la 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.

Ils vont tirer un élément des deux, et produire l'élément "le plus bas", puis aller chercher un nouvel élément de la source correspondante, jusqu'à épuisement des deux sources. 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és, 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 de ces utilisera en interne une structure de données pour conserver une copie des données, donc ils seront pas cher si l'entrée est triée. Si vous ne pouvez pas, ou ne le garantirez pas, vous devriez utiliser les méthodes d'extension 3.5 que vous avez déjà trouvées.

exemple de code est ici 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); 
+0

+1 pour avoir trouvé la solution: et si elles sont triées? Et pour beaucoup de code. Ensuite, le temps qu'il faut pour les trier pourrait battre tout le but. D'où le disclaimer :) – Lucas

1

Vous ne savez pas quelle approche est la plus rapide jusqu'à ce que vous mesurez. La manière LINQ est élégante et facile à comprendre.

Une autre méthode consiste à implémenter un ensemble en tant que tableau de hachage (Dictionary) 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.

3

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

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

Pas sûr de la performance, mais il faut battre l'exemple que vous avez donné Linq.

EDIT: Je me suis corrigé. La mise en œuvre paresseuse de Concat et Distinct ont un avantage clé de mémoire et de vitesse.Concat/Distinct est environ 10% plus rapide et enregistre plusieurs copies de données.

Je confirmé par le 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:

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

En fait, je m'attendrais à ce que ce soit * moins * efficace que le mode Concat/Distinct, car Union devra former un second HashSet. –

12

Pourquoi pensez-vous imaginer que ce serait inefficace? Autant que je sache, Concat et Distinct sont évalués paresseusement, en utilisant un HashSet dans les coulisses de Distinct pour garder une trace des éléments qui ont déjà été retournés.

Je ne suis pas sûr comment vous parvenez à le rendre plus efficace que d'une manière générale :)

EDIT: Distinct utilise effectivement Set (une classe interne) au lieu de HashSet, mais l'essentiel est toujours correct. Ceci est un très bon exemple de LINQ. La réponse la plus simple est à peu près aussi efficace que possible 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; 
     } 
    } 
} 
Questions connexes