Giao nhau của nhiều danh sách với IEnumerable.Intersect ()


83

Tôi có một danh sách các danh sách mà tôi muốn tìm giao điểm như sau:

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

Có cách nào để làm điều này với IEnumerable.Intersect () không?

CHỈNH SỬA: Lẽ ra tôi phải nói rõ hơn về điều này: Tôi thực sự có một danh sách các danh sách, tôi không biết sẽ có bao nhiêu danh sách, ba danh sách trên chỉ là một ví dụ, những gì tôi có thực sự là một IEnumerable<IEnumerable<SomeClass>>

GIẢI PHÁP

Cảm ơn cho tất cả các câu trả lời tuyệt vời. Hóa ra có bốn tùy chọn để giải quyết vấn đề này: Danh sách + tổng hợp (@Marcel Gosselin), Danh sách + foreach (@JaredPar, @Gabe Moothart), HashSet + tổng hợp (@jesperll) và HashSet + foreach (@Tony the Pony). Tôi đã thực hiện một số thử nghiệm hiệu suất trên các giải pháp này ( số lượng danh sách khác nhau , số phần tử trong mỗi danh sách và kích thước tối đa của số ngẫu nhiên .

Nó chỉ ra rằng trong hầu hết các tình huống, HashSet hoạt động tốt hơn Danh sách (ngoại trừ với danh sách lớn và kích thước số ngẫu nhiên nhỏ, do bản chất của HashSet, tôi đoán vậy.) Tôi không thể tìm thấy bất kỳ sự khác biệt thực sự nào giữa phương pháp foreach và tổng hợp (phương pháp foreach hoạt động tốt hơn một chút .)

Đối với tôi, phương pháp tổng hợp thực sự hấp dẫn (và tôi sẽ coi đó là câu trả lời được chấp nhận) nhưng tôi sẽ không nói đó là giải pháp dễ đọc nhất .. Một lần nữa, cảm ơn tất cả!

Câu trả lời:


72

Làm thế nào về:

var intersection = listOfLists
    .Skip(1)
    .Aggregate(
        new HashSet<T>(listOfLists.First()),
        (h, e) => { h.IntersectWith(e); return h; }
    );

Bằng cách đó, nó được tối ưu hóa bằng cách sử dụng cùng một HashSet xuyên suốt và vẫn trong một câu lệnh duy nhất. Chỉ cần đảm bảo rằng listOfLists luôn chứa ít nhất một danh sách.


1
Wow, Không thể nào mà tôi có thể tự mình nghĩ về giải pháp này. Một khi bạn có giải pháp, có vẻ như rõ ràng ..... hummmm, không, tôi sẽ để lại nhận xét chỉ để đảm bảo đồng nghiệp của tôi sẽ không nghĩ rằng tôi mất quá nhiều cỏ dại :)
Samuel

mô hình chức năng chiến thắng)
anatol

tại sao cần có Skip? Hỏi vì tôi không biết
Issa Fram

Bỏ qua ở đó vì phần tử đầu tiên được sử dụng cho vùng ban đầu của bộ băm. Bạn phải làm điều này, bởi vì nếu không đó là một loạt các giao điểm với một tập hợp trống.
SirPentor

Tôi hiểu giải pháp. Tôi đoán e là viết tắt của enumerator? Cho mình hỏi h là viết tắt của gì? Tôi đoán h là viết tắt của HashSet?
Quán

62

Bạn thực sự có thể sử dụng Intersecthai lần. Tuy nhiên, tôi tin rằng điều này sẽ hiệu quả hơn:

HashSet<int> hashSet = new HashSet<int>(list1);
hashSet.IntersectWith(list2);
hashSet.IntersectWith(list3);
List<int> intersection = hashSet.ToList();

Tất nhiên không phải là vấn đề với các bộ nhỏ, nhưng nếu bạn có nhiều bộ lớn thì điều đó có thể rất quan trọng.

Về cơ bản, Enumerable.Intersectcần tạo một tập hợp trên mỗi cuộc gọi - nếu bạn biết rằng bạn sẽ thực hiện nhiều thao tác tập hợp hơn, bạn cũng có thể giữ tập hợp đó.

Như mọi khi, hãy theo dõi chặt chẽ hiệu suất so với khả năng đọc - phương pháp chuỗi gọi Intersecthai lần rất hấp dẫn.

CHỈNH SỬA: Đối với câu hỏi cập nhật:

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

Hoặc nếu bạn biết nó sẽ không trống và Skip đó sẽ tương đối rẻ:

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

1
@Skeet "Tony the Pony"?
Gabe Moothart

Vâng, bước trước có ý nghĩa. Bất kỳ sự khác biệt nào về hiệu suất với phương pháp này so với phương pháp Tổng hợp trong câu trả lời của Marcel?
Oskar

@Oskar: Có, câu trả lời của tôi sử dụng một bộ băm duy nhất thay vì tạo một bộ mới mỗi lần. Tuy nhiên, bạn vẫn có thể sử dụng Aggregate với một bộ ... sẽ chỉnh sửa.
Jon Skeet

Ick ... chỉ cần cố gắng để làm việc ra một giải pháp tổng hợp, và nó icky vì HashSet.IntersectWith trả về null :(
Jon Skeet

1
Chào. Một câu hỏi liên quan đến IntersectAll()phương pháp của bạn (một số ít): có cách nào đơn giản để thêm bộ chọn làm tham số, để so sánh các giá trị (ví dụ Func<TResult, TKey> selector:) và vẫn sử dụng InsertectWith()không?
tigrou

28

Hãy thử cách này, nó hoạt động nhưng tôi thực sự muốn loại bỏ .ToList () trong tổng thể.

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

Cập nhật:

Theo nhận xét từ @pomber, bạn có thể thoát lệnh gọi ToList()bên trong Aggregatevà di chuyển lệnh gọi ra bên ngoài để thực thi nó chỉ một lần. Tôi đã không kiểm tra hiệu suất xem mã trước đó có nhanh hơn mã mới hay không. Thay đổi cần thiết là chỉ định tham số kiểu chung của Aggregatephương thức trên dòng cuối cùng như bên dưới:

var intersection = listOfLists.Aggregate<IEnumerable<int>>(
   (previousList, nextList) => previousList.Intersect(nextList)
   ).ToList();

Cảm ơn, tôi vừa thử và nó hoạt động! Trước đây, Hav chưa sử dụng Aggregate () nhưng tôi đoán đó là một thứ như thế này mà tôi đang tìm kiếm.
Oskar

Như tôi đã chỉ ra như một nhận xét về câu trả lời của Tony, tôi tin rằng giải pháp của anh ấy sẽ hoạt động tốt hơn.
Marcel Gosselin

3
Bạn có thể thoát khỏi những ToList () trong tổng nếu bạn sử dụng tổng hợp <IEnumerable <int >>
pomber

@pomber, tôi không thể tin rằng bình luận của bạn đã mất 3 năm mà không có sự ủng hộ. Hôm nay là ngày của bạn, bạn của tôi.
Sean

5

Đây là phiên bản giải pháp của tôi với một phương thức mở rộng mà tôi gọi là 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;
    }
}

Vì vậy, cách sử dụng sẽ như thế này:

var intersection = (new[] { list1, list2, list3 }).IntersectMany(l => l).ToList();

4

Bạn có thể làm như sau

var result = list1.Intersect(list2).Intersect(list3).ToList();

1
Cảm ơn, nhưng tôi thực sự có một danh sách các danh sách, không phải ba danh sách riêng biệt .. Tôi cần một cái gì đó hoạt động độc lập với số lượng danh sách có trong listOfLists.
Oskar

4
@Oskar Bạn có thể dễ dàng chạy nó trong một vòng lặp
Gabe Moothart

2

Đây là giải pháp một hàng của tôi cho Danh sách Danh sách (ListOfLists) không có chức năng giao nhau:

var intersect = ListOfLists.SelectMany(x=>x).Distinct().Where(w=> ListOfLists.TrueForAll(t=>t.Contains(w))).ToList()

Điều này sẽ hoạt động cho .net 4 (hoặc mới hơn)


0

Sau khi tìm kiếm trên mạng và không thực sự tìm ra thứ gì đó tôi thích (hoặc điều đó hiệu quả), tôi đã ngủ trên đó và nghĩ ra điều này. Của tôi sử dụng một lớp ( SearchResult) có một lớp EmployeeIdtrong đó và đó là điều tôi cần phổ biến trên các danh sách. Tôi trả lại tất cả các bản ghi có EmployeeIdtrong mọi danh sách. Nó không cầu kỳ, nhưng nó đơn giản và dễ hiểu, chỉ cần những gì tôi thích. Đối với danh sách nhỏ (trường hợp của tôi), nó sẽ hoạt động tốt — và bất kỳ ai cũng có thể hiểu được!

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

Đây là một ví dụ chỉ sử dụng danh sách các int, không phải một lớp (đây là cách triển khai ban đầu của tôi).

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

-1

Đây là một giải pháp đơn giản nếu danh sách của bạn nhỏ. Nếu bạn có danh sách lớn hơn, danh sách đó không hoạt động như bộ băm:

public static IEnumerable<T> IntersectMany<T>(this IEnumerable<IEnumerable<T>> input)
{
    if (!input.Any())
        return new List<T>();

    return input.Aggregate(Enumerable.Intersect);
}
Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.