2009-01-07 22 views
16

Tengo un problema con mi trabajo que con suerte se reduce a lo siguiente: Tengo dos List<int> s, y quiero ver si alguno de los int s en ListA son iguales a cualquier int en ListB. (Pueden ser matrices si eso hace la vida más fácil, pero creo que List<> tiene algo de magia incorporada que podría ayudar.) Y estoy seguro de que este es un problema LINQ, pero estoy trabajando en 2.0 aquí.elementos coincidentes de dos listas (o matrices)

Mi solución ha sido hasta ahora a través de foreach ListaA, y luego a través foreach ListaB,

foreach (int a in ListA) 
{ 
    foreach (int b in ListB) 
    { 
     if (a == b) 
     { 
      return true; 
     } 
    } 
} 

que en realidad era bastante resbaladiza cuando eran cada tres artículos de largo, pero ahora son 200 de largo y se con frecuencia no coinciden, entonces obtenemos el peor caso de comparaciones N^2. Incluso 40,000 comparaciones pasan bastante rápido, pero creo que me puede faltar algo, ya que N^2 parece bastante ingenuo para este problema en particular.

Gracias!

Respuesta

31

Con LINQ, esto es trivial, como se puede llamar a la Intersect extension method en el Enumerable class para darle la intersección de conjuntos de las dos matrices:

var intersection = ListA.Intersect(ListB); 

Sin embargo, este es el conjunto intersección, es decir, si ListA y ListB no tienen valores únicos, no obtendrá ninguna copia. En otras palabras, si tiene lo siguiente:

var ListA = new [] { 0, 0, 1, 2, 3 }; 
var ListB = new [] { 0, 0, 0, 2 }; 

Entonces ListA.Intersect(ListB) produce:

{ 0, 2 } 

Si usted está esperando:

{ 0, 0, 2 } 

Entonces vas a tener que mantener una cuente los artículos usted mismo y ceda/disminuya mientras escanea las dos listas.

En primer lugar, usted quiere recoger una Dictionary<TKey, int> con las listas de los artículos individuales:

var countsOfA = ListA.GroupBy(i => i).ToDictionary(g => g.Key, g => g.Count()); 

A partir de ahí, puede escanear ListB y el lugar que en una lista cuando se encuentra con un elemento en countsOfA :

// The items that match. 
IList<int> matched = new List<int>(); 

// Scan 
foreach (int b in ListB) 
{ 
    // The count. 
    int count; 

    // If the item is found in a. 
    if (countsOfA.TryGetValue(b, out count)) 
    { 
     // This is positive. 
     Debug.Assert(count > 0); 

     // Add the item to the list. 
     matched.Add(b); 

     // Decrement the count. If 
     // 0, remove. 
     if (--count == 0) countsOfA.Remove(b); 
    } 
} 

Usted puede terminar con esto en un método de extensión que se aplaza la ejecución de este modo:

public static IEnumerable<T> MultisetIntersect(this IEnumerable<T> first, 
    IEnumerable<T> second) 
{ 
    // Call the overload with the default comparer. 
    return first.MultisetIntersect(second, EqualityComparer<T>.Default); 
} 

public static IEnumerable<T> MultisetIntersect(this IEnumerable<T> first, 
    IEnumerable<T> second, IEqualityComparer<T> comparer) 
{ 
    // Validate parameters. Do this separately so check 
    // is performed immediately, and not when execution 
    // takes place. 
    if (first == null) throw new ArgumentNullException("first"); 
    if (second == null) throw new ArgumentNullException("second"); 
    if (comparer == null) throw new ArgumentNullException("comparer"); 

    // Defer execution on the internal 
    // instance. 
    return first.MultisetIntersectImplementation(second, comparer); 
} 

private static IEnumerable<T> MultisetIntersectImplementation(
    this IEnumerable<T> first, IEnumerable<T> second, 
    IEqualityComparer<T> comparer) 
{ 
    // Validate parameters. 
    Debug.Assert(first != null); 
    Debug.Assert(second != null); 
    Debug.Assert(comparer != null); 

    // Get the dictionary of the first. 
    IDictionary<T, long> counts = first.GroupBy(t => t, comparer). 
     ToDictionary(g => g.Key, g.LongCount(), comparer); 

    // Scan 
    foreach (T t in second) 
    { 
     // The count. 
     long count; 

     // If the item is found in a. 
     if (counts.TryGetValue(t, out count)) 
     { 
      // This is positive. 
      Debug.Assert(count > 0); 

      // Yield the item. 
      yield return t; 

      // Decrement the count. If 
      // 0, remove. 
      if (--count == 0) counts.Remove(t); 
     } 
    } 
} 

Tenga en cuenta que estos dos enfoques son (y me disculpo si estoy carnicería Big-O notación aquí) O(N + M) donde N es el número de elementos de la primera serie, y M es el número de elementos de la segunda matriz . Debe escanear cada lista solo una vez, y se supone que obtener los códigos hash y realizar búsquedas en los códigos hash es una operación O(1) (constante).

+0

¿[Enumerable.Intersect] (http://stackoverflow.com/a/10627437/393280) adopta un enfoque similar? – palswim

+0

@palswim Ligeramente, pero no exactamente. He actualizado mi respuesta para que refleje 'Intersecar' y la actualizaré con una respuesta más completa que cuenta un poco los conteos. – casperOne

+0

@palswim Se actualizó la respuesta para reflejar el uso de 'Intersecar', así como las expectativas de la reunión cuando se usan las intersecciones en un conjunto frente a un conjunto múltiple. – casperOne

0

¿Qué le parece usar el método BinarySearch en lugar de iterar sobre todos los elementos en el bucle interno?

+1

¿BinarySearch no depende de la lista que se está ordenando? http://msdn.microsoft.com/en-us/library/w4e7fxsh.aspx –

7

Cargue el conjunto de ListA en una instancia de HashSet y luego pruebe el elemento foreach en ListB frente a HastSet: estoy bastante seguro de que esto sería O (N).

//untested code ahead 
HashSet<int> hashSet = new HashSet<int>(ListA); 
foreach (int i in ListB) 
{ 
    if (hashSet.Contains(i)) 
     return true; 
} 

Aquí es lo mismo en una línea:

return new HashSet<int>(ListA).Overlaps(ListB); 

HashSet no existe en .NET 3.5, por lo que en .NET 2.0 se puede utilizar Dictionary<int,object> (en lugar de utilizar HashSet<int>), y siempre almacenar null como el objeto/valor en el diccionario, ya que solo está interesado en las claves.

+0

Hashset no se introdujo hasta .NET 3.5. – casperOne

+0

Hashing en general no es una mala idea. No es difícil implementar uno si es necesario. – PolyThinker

+1

En ese caso, usando .Net 2.0, puede usar Dictionary en lugar de HashSet (y siempre almacenar null como el objeto/valor en el Diccionario, ya que solo está interesado en las claves). – ChrisW

3

En lugar de iteración a través de cada lista, echar un vistazo al método List.Contains:

foreach (int a in ListA) 
{ 
    if (ListB.Contains(a)) 
    return true; 
} 
+0

Eso no es mejor que la solución original: todavía O (N^2) – ChrisW

+1

Enséñame a publicar justo antes de acostar ... al mirar el método Contiene con más profundidad, de hecho realiza una iteración interna de la lista. En este caso, un objeto de diccionario es probablemente la mejor ruta. –

+0

Eso soluciona muchas cosas de una manera agradable y simple. Gracias. – tanzer

2

Chris da una O (N) solución de hashing. Ahora, dependiendo del factor constante (debido a hashing), podría valer la pena considerar una solución O (N log (N)) por clasificación. Hay algunas variantes diferentes que puede considerar dependiendo de su caso de uso.

  1. Ordenar ListaB (O (log N (N)), y utilizar un algoritmo de búsqueda para analizar cada elemento de Lista (que de nuevo es O (N) * O (log (N))).

  2. Ordenar tanto lista y ListaB (O (log N (N)), y utilizar un O (N) algoritmo para comparar estas listas de duplicados.

Si ambas listas van a ser utilizados más de una vez, se prefiere el segundo método.

Cuestiones relacionadas