Нахождение симметричной разности с помощью LINQ - PullRequest
19 голосов
/ 26 мая 2010

У меня есть две коллекции a и b. Я хотел бы вычислить набор элементов либо в a, либо в b, но не в обоих (логический исключающий или). С LINQ я могу придумать это:

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

Интересно, существуют ли другие более эффективные или более компактные способы получения различий между двумя коллекциями.

Редактировать 1: Джон Скит опубликовал первое решение, которое не сохраняет порядок элементов, полагаясь на HashSet. Интересно, есть ли другие подходы, которые сохранили бы порядок a и b в выводе.

Ответы [ 3 ]

26 голосов
/ 26 мая 2010

Используйте HashSet<T> напрямую - у него есть метод SymmetricExceptWith:

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

РЕДАКТИРОВАТЬ: Если вы хотите сохранить порядок, вот альтернатива:

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

Это имеет следующие важные различия:

  • И a, и b повторяются дважды. В некоторых случаях это может быть очень плохо - вы можете вызвать ToList для каждого из них, чтобы начать с буфера.
  • Если в a или b есть дубликаты, они будут получены несколько раз. Если вы хотите избежать этого, вы можете сохранить набор уже полученных значений. На данный момент это будет эквивалентно:

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

Это все еще только две операции установки вместо трех в вашем исходном коде.

5 голосов
/ 26 мая 2010

Учитывая, что a.Except (b) и b.Except (a) не пересекаются, вы можете использовать concat вместо union, сохраняя оператор набора (и concat более эффективен).

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

Это все еще проходит через каждый список дважды.

0 голосов
/ 03 августа 2017

У нас была похожая потребность в проекте в моей компании, поэтому мы написали это расширение:

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

Сравнивает элементы двух коллекций (используя IEqualityComparer или нет, по вашему выбору).

  • Возвращенный объект, EnumerablePair<T>, содержит объекты, которые находятся в leftOperand или rightOperand, но не в обоих (XOR).
  • EnumerablePair<T>.Left содержит объекты, которые находятся в leftOperand, но не в rightOperand.
  • EnumerablePair<T>.Right содержит объекты, которые находятся в rightOperand, но не в leftOperand.

Вы можете использовать расширение следующим образом:

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

xorList, leftXor и rightXor равны IEnumerable<T>.

...