2010-05-26 8 views
19

Tengo dos colecciones a y b. Me gustaría calcular el conjunto de elementos en a o b, pero no en ambos (una exclusiva lógica o). Con LINQ, puedo llegar a esto:LINQ y establecer diferencia

IEnumerable<T> Delta<T>(IEnumerable<T> a, IEnumerable<T> b) 
{ 
    return a.Except (b).Union (b.Except (a)); 
} 

Me pregunto si hay otras formas más eficientes o más compactas de la producción de la diferencia entre las dos colecciones.

Editar 1: Jon Skeet publicó una primera solución que no conserva el orden de los elementos basándose en HashSet. Me pregunto si hay otros enfoques que conservarían el orden de a y b en la salida.

+0

¿Qué pasa si aob contienen duplicados? –

+0

En mi caso, 'a' y' b' no contienen duplicados, así que esto no me preocupa. –

Respuesta

24

Uso HashSet<T> directamente - que tiene un método SymmetricExceptWith:

HashSet<T> data = new HashSet<T>(a); 
data.SymmetricExceptWith(b); 

EDIT: Si desea mantener el orden, aquí hay una alternativa:

HashSet<T> data = new HashSet<T>(a); 
data.IntersectWith(b); 
foreach (T t in a.Concat(b)) 
{ 
    if (!data.Contains(t)) 
    { 
     yield return t; 
    } 
} 

Esto tiene las siguientes diferencias importantes:

  • Ambos a y b son iterados más de dos veces. En algunos casos, eso podría ser algo muy malo: puede llamar al ToList en cada uno de ellos para comenzar a retener un búfer.
  • Si hay duplicados en a o b, se obtendrán varias veces. Si quisiera evitar esto, podría mantener un conjunto de valores ya producidos. En este punto, sería equivalente a:

    a.Concat(b).Except(a.Intersect(b)) 
    

Eso sigue siendo sólo dos operaciones de conjuntos en lugar de los tres en su código original, sin embargo.

+0

Gracias Jon por su rápida respuesta. HashSet funciona bien siempre que no esté interesado en el orden original de los artículos. ¿Qué pasa si quiero mantener el orden de los elementos en 'a' y' b' en la diferencia? –

+0

@Pierre: he editado mi respuesta con otras dos opciones. –

+0

Muchas gracias por su tiempo. En mi caso, 'a' y' b' no contienen duplicados, así que esto no es una preocupación. La expresión LINQ que usted propone es mucho más legible (y por lo tanto más fácil de mantener) que la pieza de código que involucra al 'HashSet'. ¡Me gusta! –

3

Dado a.Except (b) y b.Except (a) son disjuntos, se puede utilizar en lugar de concatunion, el ahorro de un operador de conjunto (y concat es más eficiente).

return a.Except (b).Concat (b.Except (a)); 

Esto todavía se ejecuta en cada lista dos veces.

+0

Gracias; tienes razón, 'Concat' será más rápido que' Union' ya que mis entradas son disjuntas; Había pasado por alto ese punto. –

0

Tuvimos una necesidad similar de un proyecto en mi empresa, por lo que escribió esta extensión:

public class EnumerablePair<T> : IReadOnlyCollection<T> 
{ 
    private IReadOnlyCollection<T> _Left; 
    private IReadOnlyCollection<T> _Right; 
    private IEnumerable<T> _Union; 
    private int _Count; 
    public EnumerablePair(IEnumerable<T> left, IEnumerable<T> right) 
    { 
     _Left = left?.ToList() ?? Enumerable.Empty<T>().ToList(); 
     _Right = right?.ToList() ?? Enumerable.Empty<T>().ToList(); 
     _Count = Left.Count + Right.Count; 
     _Union = Left.Union(Right); 
    } 

    public int Count => _Count; 
    public IReadOnlyCollection<T> Left { get => _Left; } 
    public IReadOnlyCollection<T> Right { get => _Right; } 

    public IEnumerator<T> GetEnumerator() 
    { 
     return _Union.GetEnumerator(); 
    } 

    IEnumerator IEnumerable.GetEnumerator() 
    { 
     return _Union.GetEnumerator(); 
    } 
} 

public static class EnumerableExtension 
{ 
    public static EnumerablePair<T> ExclusiveDisjunction<T>(this IEnumerable<T> leftOperand, IEnumerable<T> rightOperand, IEqualityComparer<T> comparer = null) 
    { 
     if (leftOperand == null) 
      throw new ArgumentNullException(nameof(leftOperand), $"{nameof(leftOperand)} is null."); 
     if (rightOperand == null) 
      throw new ArgumentNullException(nameof(rightOperand), $"{nameof(rightOperand)} is null."); 

     // TODO : Can be optimized if one of the IEnumerable parameters is empty. 

     bool leftIsBigger = leftOperand.Count() > rightOperand.Count(); 
     var biggestOperand = leftIsBigger ? leftOperand.ToList() : rightOperand.ToList(); 
     var smallestOperand = leftIsBigger ? rightOperand.ToList() : leftOperand.ToList(); 

     var except1 = biggestOperand.ToList(); 
     var except2 = Enumerable.Empty<T>().ToList(); 

     Func<T, T, bool> areEquals; 
     if (comparer != null) 
      areEquals = (one, theOther) => comparer.Equals(one, theOther); 
     else 
      areEquals = (one, theOther) => one?.Equals(theOther) ?? theOther == null; 

     foreach (T t in smallestOperand) 
      if (except1.RemoveAll(item => areEquals(item, t)) == 0) 
       except2.Add(t); 

     if (leftIsBigger) 
      return new EnumerablePair<T>(except1, except2); 
     return new EnumerablePair<T>(except2, except1); 
    } 
} 

Se comparan dos elementos de colecciones (utilizando un IEqualityComparer o no, a su elección).

  • El objeto devuelto, un EnumerablePair<T>, contiene objetos que están en leftOperand o rightOperand, pero no ambos (XOR).
  • EnumerablePair<T>.Left contiene objetos que están en leftOperand pero no en rightOperand.
  • EnumerablePair<T>.Right contiene objetos que están en rightOperand pero no en leftOperand.

Usted puede utilizar la extensión de la siguiente manera:

var xorList = list1.ExclusiveDisjunction(list2); 
var leftXor = xorList.Left; 
var rightXor = xorList.Right; 

xorList, y leftXorrightXor son IEnumerable<T>.