2008-09-28 15 views
37

Estoy usando .NET 3.5. Tengo dos matrices de cadenas, que pueden compartir uno o más valores:Combinar eficientemente matrices de cadenas en .NET, manteniendo distintos valores

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

que me gustaría una manera de combinarlos en una matriz sin valores duplicados:

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

puedo hacer esto con LINQ:

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

pero imagino que no es muy eficiente para arreglos grandes.

¿Hay una manera mejor?

Respuesta

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

de msdn: ". Este método excluye los duplicados de la devolución establecido Este es un comportamiento diferente al método Concat (TSource), que devuelve todo el elementos en las secuencias de entrada incluyendo duplicados ".

+2

Volví a este tema para publicar exactamente esta solución. ¡Es ideal en todos los sentidos, creo! –

+5

Un punto menor, pero el tipo de devolución de Unión es IEnumerable , por lo que necesitaría agregar un ToArray() para obtener la cadena [] –

+0

Esto sigue siendo útil 10 años después: D – Jen

1

Probablemente la creación de una tabla hash con sus valores como claves (solo agregue los que ya no están presentes) y la conversión de las claves a una matriz podría ser una solución viable.

2

Descargo de responsabilidad Esto es una optimización prematura. Para sus matrices de ejemplo, use los métodos de extensión 3.5. Hasta que sepa que tiene un problema de rendimiento en esta región, debe usar el código de la biblioteca.


Si usted puede ordenar las matrices, o que están ordenados cuando se llega a ese punto en el código, puede utilizar los métodos siguientes.

Esto extraerá un elemento de ambos y producirá el elemento "más bajo", luego buscará un nuevo elemento de la fuente correspondiente, hasta que se agoten ambas fuentes. En el caso en que el elemento actual obtenido de las dos fuentes sea igual, producirá el de la primera fuente y saltará en ambas fuentes.

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

Tenga en cuenta que si una de las fuentes contiene duplicados, es posible que vea duplicados en la salida. Si desea eliminar estos duplicados en las listas ya ordenados, utilizar el siguiente método:

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

Tenga en cuenta que ninguno de ellos utilizará internamente una estructura de datos para mantener una copia de los datos, por lo que será barato si la entrada esta ordenada Si no puede, o no lo garantizará, debe usar los métodos de extensión 3.5 que ya ha encontrado.

Aquí es código de ejemplo que llama a los métodos anteriores:

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 para pensar fuera de la caja: ¿y si están ordenados ?. Y para un montón de código. Por otra parte, el tiempo que se tarda en ordenarlos podría superar todo el propósito. De ahí el descargo de responsabilidad :) – Lucas

1

Usted no sabe cuál es el enfoque más rápido hasta que se mida. La forma de LINQ es elegante y fácil de entender.

Otra forma es implementar un conjunto como una matriz hash (Diccionario) y agregar todos los elementos de ambas matrices al conjunto. Luego use el método set.Keys.ToArray() para crear la matriz resultante.

3

.NET 3.5 introdujo la clase HashSet que podría hacer esto:

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

No está seguro de los resultados, pero debe superar el ejemplo LINQ que diste.

EDIT: Me erraron corregido. La implementación diferida de Concat y Distinct tiene una memoria clave Y una ventaja de velocidad.Concat/Distinct es aproximadamente un 10% más rápido y guarda múltiples copias de datos.

I confirmó a través de código:

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 

es la salida:

 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 realidad, espero que sea * menos * eficiente que el modo Concat/Distinct, ya que Union necesitará formar un segundo HashSet. –

12

¿Por qué imaginar que sería ineficiente? Por lo que yo sé, tanto Concat como Distinct se evalúan perezosamente, usando un HashSet detrás de escena para que Distinct realice un seguimiento de los elementos que ya han sido devueltos.

no estoy seguro de cómo te las arreglas para hacerlo más eficiente que el de un modo general :)

EDIT: distinta realidad utiliza Set (una clase interna) en lugar de HashSet, pero la esencia es sigue siendo correcto Este es un muy buen ejemplo de cuán ordenado es LINQ. La respuesta más simple es casi tan eficiente como se puede lograr sin más conocimiento de dominio.

el efecto es el equivalente 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; 
     } 
    } 
} 
Cuestiones relacionadas