Пересечение нескольких списков с помощью IEnumerable.Intersect()
У меня есть список списков, которые я хочу найти для такого пересечения:
var list1 = new List<int>() { 1, 2, 3 };
var list2 = new List<int>() { 2, 3, 4 };
var list3 = new List<int>() { 3, 4, 5 };
var listOfLists = new List<List<int>>() { list1, list2, list3 };
// expected intersection is List<int>() { 3 };
Есть ли способ сделать это с помощью IEnumerable.Intersect()?
EDIT:
Я должен был быть более ясным в этом: у меня действительно есть список списков, я не знаю, сколько их будет, три списка выше были просто примером, что у меня есть на самом деле IEnumerable<IEnumerable<SomeClass>>
Решение
Спасибо за отличные ответы. Оказалось, что существует четыре варианта решения этого вопроса: Список + агрегат (@Marcel Gosselin), Список + foreach (@JaredPar, @Gabe Moothart), HashSet + агрегат (@jesperll) и HashSet + foreach (@Tony the Pony). Я провел некоторое тестирование производительности этих решений (варьируя количество списков, количество элементов в каждом списке и случайное число max.
Оказывается, что для большинства ситуаций HashSet работает лучше, чем List (за исключением больших списков и небольшого размера случайных чисел, из-за природы HashSet, я думаю).
Я не мог найти никакой реальной разницы между методом foreach и агрегированным методом (метод foreach работает немного лучше.)
Для меня агрегированный метод действительно привлекателен (и я согласен с этим как принятый ответ), но я бы не сказал, что это наиболее читаемое решение.. Еще раз спасибо!
Ответы
Ответ 1
Как насчет:
var intersection = listOfLists
.Skip(1)
.Aggregate(
new HashSet<T>(listOfLists.First()),
(h, e) => { h.IntersectWith(e); return h; }
);
Таким образом, он оптимизируется с использованием одного и того же HashSet во всем и все еще в одном выражении. Просто убедитесь, что listOfLists всегда содержит хотя бы один список.
Ответ 2
Вы действительно можете использовать Intersect
дважды. Однако я считаю, что это будет более эффективно:
HashSet<int> hashSet = new HashSet<int>(list1);
hashSet.IntersectWith(list2);
hashSet.IntersectWith(list3);
List<int> intersection = hashSet.ToList();
Не проблема с небольшими наборами, конечно, но если у вас много больших наборов, это может быть значительным.
В основном Enumerable.Intersect
необходимо создать набор для каждого вызова - если вы знаете, что собираетесь делать больше операций с множеством, вы можете также сохранить это значение.
Как всегда, внимательно следите за производительностью и читабельностью - метод цепочки вызова Intersect
дважды очень привлекателен.
EDIT: для обновленного вопроса:
public List<T> IntersectAll<T>(IEnumerable<IEnumerable<T>> lists)
{
HashSet<T> hashSet = null;
foreach (var list in lists)
{
if (hashSet == null)
{
hashSet = new HashSet<T>(list);
}
else
{
hashSet.IntersectWith(list);
}
}
return hashSet == null ? new List<T>() : hashSet.ToList();
}
Или, если вы знаете, что он не будет пустым, и что Skip будет относительно дешевым:
public List<T> IntersectAll<T>(IEnumerable<IEnumerable<T>> lists)
{
HashSet<T> hashSet = new HashSet<T>(lists.First());
foreach (var list in lists.Skip(1))
{
hashSet.IntersectWith(list);
}
return hashSet.ToList();
}
Ответ 3
Попробуй, это работает, но я бы очень хотел избавиться от .ToList() в совокупности.
var list1 = new List<int>() { 1, 2, 3 };
var list2 = new List<int>() { 2, 3, 4 };
var list3 = new List<int>() { 3, 4, 5 };
var listOfLists = new List<List<int>>() { list1, list2, list3 };
var intersection = listOfLists.Aggregate((previousList, nextList) => previousList.Intersect(nextList).ToList());
Update:
Следуя за комментарием @pomber, можно избавиться от ToList()
внутри вызова Aggregate
и переместить его за пределы, чтобы выполнить его только один раз. Я не тестировал, был ли предыдущий код быстрее нового. Необходимое изменение состоит в том, чтобы указать параметр типового типа метода Aggregate
в последней строке, как показано ниже:
var intersection = listOfLists.Aggregate<IEnumerable<int>>(
(previousList, nextList) => previousList.Intersect(nextList)
).ToList();
Ответ 4
Вы можете сделать следующее
var result = list1.Intersect(list2).Intersect(list3).ToList();
Ответ 5
Это моя версия решения с методом расширения, который я назвал IntersectMany.
public static IEnumerable<TResult> IntersectMany<TSource, TResult>(this IEnumerable<TSource> source, Func<TSource, IEnumerable<TResult>> selector)
{
using (var enumerator = source.GetEnumerator())
{
if(!enumerator.MoveNext())
return new TResult[0];
var ret = selector(enumerator.Current);
while (enumerator.MoveNext())
{
ret = ret.Intersect(selector(enumerator.Current));
}
return ret;
}
}
Таким образом, использование будет примерно таким:
var intersection = (new[] { list1, list2, list3 }).IntersectMany(l => l).ToList();
Ответ 6
Это мое однострочное решение для списка List (ListOfLists) без функции пересечения:
var intersect = ListOfLists.SelectMany(x=>x).Distinct().Where(w=> ListOfLists.TrueForAll(t=>t.Contains(w))).ToList()
Это должно работать для .net 4 (или более поздней версии)
Ответ 7
После поиска "сети" и совсем не придумал что-то, что мне понравилось (или это сработало), я спал на нем и придумал это. Mine использует класс (SearchResult
), который имеет EmployeeId
в нем и что вещь, которая должна быть общей в списках. Я возвращаю все записи с EmployeeId
в каждом списке. Это не фантазия, но это просто и легко понять, только то, что мне нравится. Для небольших списков (мой случай) он должен выполняться просто отлично, и каждый может это понять!
private List<SearchResult> GetFinalSearchResults(IEnumerable<IEnumerable<SearchResult>> lists)
{
Dictionary<int, SearchResult> oldList = new Dictionary<int, SearchResult>();
Dictionary<int, SearchResult> newList = new Dictionary<int, SearchResult>();
oldList = lists.First().ToDictionary(x => x.EmployeeId, x => x);
foreach (List<SearchResult> list in lists.Skip(1))
{
foreach (SearchResult emp in list)
{
if (oldList.Keys.Contains(emp.EmployeeId))
{
newList.Add(emp.EmployeeId, emp);
}
}
oldList = new Dictionary<int, SearchResult>(newList);
newList.Clear();
}
return oldList.Values.ToList();
}
Вот пример, просто используя список int, а не класс (это была моя оригинальная реализация).
static List<int> FindCommon(List<List<int>> items)
{
Dictionary<int, int> oldList = new Dictionary<int, int>();
Dictionary<int, int> newList = new Dictionary<int, int>();
oldList = items[0].ToDictionary(x => x, x => x);
foreach (List<int> list in items.Skip(1))
{
foreach (int i in list)
{
if (oldList.Keys.Contains(i))
{
newList.Add(i, i);
}
}
oldList = new Dictionary<int, int>(newList);
newList.Clear();
}
return oldList.Values.ToList();
}
Ответ 8
Это простое решение, если ваши списки невелики. Если у вас более крупные списки, это не так, как выполнение хеш-набора:
public static IEnumerable<T> IntersectMany<T>(this IEnumerable<IEnumerable<T>> input)
{
if (!input.Any())
return new List<T>();
return input.Aggregate(Enumerable.Intersect);
}