2009-01-07 14 views
16

Tôi đang gặp vấn đề với công việc của mình hy vọng giảm xuống như sau: Tôi có hai List<int> s và tôi muốn xem có bất kỳ số nào trong số int s trong ListA bằng bất kỳ int nào trong số ListB. (Họ có thể là mảng nếu điều đó làm cho cuộc sống dễ dàng hơn, nhưng tôi nghĩ rằng List<> có một số phép thuật tích hợp có thể giúp ích.) Và tôi chắc chắn đây là vấn đề thân thiện với LINQ, nhưng tôi đang làm việc ở đây 2.0.các mục phù hợp từ hai danh sách (hoặc mảng)

Giải pháp của tôi cho đến nay đã được foreach qua Lista, và sau đó foreach qua ListB,

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

đó là thực sự khá trơn khi họ còn mỗi ba mục dài, nhưng bây giờ họ đang dài 200 và họ thường xuyên không phù hợp, vì vậy chúng tôi nhận được trường hợp xấu nhất của các so sánh N^2. Ngay cả 40.000 so sánh đi bằng khá nhanh, nhưng tôi nghĩ rằng tôi có thể thiếu một cái gì đó, vì N^2 có vẻ khá ngây thơ cho vấn đề cụ thể này.

Cảm ơn!

Trả lời

31

Với LINQ, đây là tầm thường, như bạn có thể gọi Intersect extension method trên Enumerable class để cung cấp cho bạn giao bộ của hai mảng:

var intersection = ListA.Intersect(ListB); 

Tuy nhiên, đây là thiết ngã tư, có nghĩa là nếu ListAListB không có giá trị duy nhất trong đó, bạn sẽ không nhận được bất kỳ bản sao nào. Nói cách khác, nếu bạn đã điều sau đây:

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

Sau đó ListA.Intersect(ListB) sản xuất:

{ 0, 2 } 

Nếu bạn đang mong:

{ 0, 0, 2 } 

Sau đó, bạn sẽ phải duy trì một đếm các mục của bản thân và năng suất/giảm khi bạn quét hai danh sách.

Thứ nhất, bạn muốn thu thập một Dictionary<TKey, int> với danh sách các mặt hàng cá nhân:

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

Từ đó, bạn có thể quét ListB và đặt rằng trong một danh sách khi bạn gặp một mục trong 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); 
    } 
} 

Bạn có thể quấn này trong một phương pháp mở rộng mà trì hoãn thực hiện như sau:

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

Lưu ý rằng cả hai cách tiếp cận này là (và tôi xin lỗi nếu tôi đang ký hiệu Big-O ở đây) O(N + M) trong đó N là số mục trong mảng đầu tiên và M là số mục trong mảng thứ hai . Bạn phải quét từng danh sách một lần và giả định rằng việc nhận mã băm và thực hiện tra cứu trên mã băm là hoạt động O(1) (hằng số).

+0

[Enumerable.Intersect] (http://stackoverflow.com/a/10627437/393280) có cách tiếp cận tương tự không? – palswim

+0

@palswim Hơi, nhưng không chính xác. Tôi đã cập nhật câu trả lời của tôi để phản ánh 'Intersect' và tôi sẽ cập nhật với một câu trả lời kỹ lưỡng hơn có số lượng trong một chút. – casperOne

+0

@palswim Cập nhật câu trả lời để phản ánh bằng cách sử dụng 'Giao nhau' cũng như mong đợi cuộc họp khi sử dụng giao điểm trên một tập hợp so với số nhiều. – casperOne

0

Cách sử dụng phương pháp BinarySearch thay vì lặp qua tất cả các phần tử trong vòng lặp bên trong?

+1

Không tìm kiếm nhị phân dựa vào danh sách đang được sắp xếp? http://msdn.microsoft.com/en-us/library/w4e7fxsh.aspx –

7

Tải toàn bộ ListA vào một thể hiện HashSet và sau đó kiểm tra mục foreach trong ListB đối với HastSet: Tôi chắc chắn rằng đây sẽ là O (N).

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

Dưới đây là những điều tương tự trong một dòng:

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

HashSet không tồn tại trong .NET 3.5, vì vậy trong .NET 2.0, bạn có thể sử dụng Dictionary<int,object> (thay vì sử dụng HashSet<int>), và luôn luôn lưu trữ null làm đối tượng/giá trị trong Từ điển vì bạn chỉ quan tâm đến các khóa.

+0

Hashset không được giới thiệu cho đến khi .NET 3.5. – casperOne

+0

Hashing nói chung không phải là một ý tưởng tồi. Nó không khó để thực hiện một nếu cần thiết. – PolyThinker

+1

Trong trường hợp đó, sử dụng Net 2.0, bạn có thể sử dụng Từ điển thay vì HashSet (và luôn lưu trữ null làm đối tượng/giá trị trong Từ điển, vì bạn chỉ quan tâm đến các khóa). – ChrisW

3

Thay vì lặp qua mỗi danh sách, hãy nhìn vào các phương pháp List.Contains:

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

Đó là không tốt hơn so với giải pháp ban đầu: vẫn O (N^2) – ChrisW

+1

Dạy tôi đăng bài ngay trước khi đi ngủ ... khi nhìn vào phương pháp Chứa sâu hơn, nó thực sự thực hiện lặp lại nội bộ của danh sách. Trong trường hợp này, một đối tượng từ điển có lẽ là tuyến đường tốt hơn. –

+0

Điều đó giải quyết nhiều thứ một cách tốt đẹp và đơn giản. Cảm ơn. – tanzer

2

Chris đưa ra một O (N) giải pháp bằng cách băm. Bây giờ, tùy thuộc vào các yếu tố không đổi (do băm), nó có thể là giá trị xem xét một giải pháp O (N log (N)) bằng cách phân loại. Có một vài biến thể khác nhau mà bạn có thể xem xét tùy thuộc vào trường hợp sử dụng của bạn.

  1. Sắp xếp ListB (O (N log (N)), và sử dụng một thuật toán tìm kiếm để phân tích từng yếu tố trong Lista (đó là một lần nữa O (N) * O (log (N))).

  2. sắp xếp cả Lista và ListB (O (N log (N)), và sử dụng một O (N) thuật toán để so sánh các danh sách các bản sao.

Nếu cả hai danh sách sẽ được sử dụng nhiều hơn một lần, phương pháp thứ hai được ưu tiên.