【问题标题】:matching items from two lists (or arrays)匹配来自两个列表(或数组)的项目
【发布时间】:2009-01-07 05:35:23
【问题描述】:

我的工作有问题,希望可以减少到以下问题:我有两个List<int>s,我想看看ListA 中的任何ints 是否等于任何@987654324 @在ListB。 (如果这样可以让生活更轻松,它们可以是数组,但我认为List<> 有一些内置的魔法可能会有所帮助。)我确信这是一个 LINQ 友好的问题,但我在这里使用 2.0。

到目前为止,我的解决方案是通过 ListA 到达 foreach,然后通过 ListB 到达 foreach

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

当它们每三个项目长时实际上非常光滑,但现在它们有 200 长并且它们经常不匹配,所以我们得到了 N^2 比较的最坏情况。即使是 40,000 次比较也很快,但我想我可能会遗漏一些东西,因为 N^2 对于这个特定问题似乎很幼稚。

谢谢!

【问题讨论】:

    标签: c# .net arrays list


    【解决方案1】:

    使用LINQ,这很简单,因为您可以在Enumerable class 上调用Intersect extension method 来为您提供两个数组的集合交集:

    var intersection = ListA.Intersect(ListB);
    

    但是,这是 set 交集,这意味着如果 ListAListB 没有唯一值,您将不会获得任何副本。换句话说,如果您有以下情况:

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

    然后ListA.Intersect(ListB) 产生:

    { 0, 2 }
    

    如果你期待:

    { 0, 0, 2 }
    

    然后,您将不得不自己维护项目的计数,并在扫描两个列表时进行产量/递减。

    首先,您需要收集带有单个项目列表的Dictionary<TKey, int>

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

    从那里,您可以扫描 ListB 并在遇到 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);
        }
    }
    

    您可以将其包装在一个延迟执行的扩展方法中,如下所示:

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

    请注意,这两种方法都是O(N + M)(如果我在这里使用 Big-O 表示法,我深表歉意)其中N 是第一个数组中的项目数,M 是第二个数组中的项目。您只需扫描每个列表一次,并且假定获取哈希码并在哈希码上执行查找是一个O(1)(常量)操作。

    【讨论】:

    • Enumerable.Intersect 是否采取了类似的做法?
    • @palswim 有点,但不完全是。我已经更新了我的答案以反映Intersect,我将更新一个更全面的答案,其中包含一些要点。
    • @palswim 更新了答案以反映使用 Intersect 以及在集合与多集合上使用交叉点时满足预期。
    【解决方案2】:

    将整个 ListA 加载到 HashSet 实例中,然后针对 HastSet 测试 ListB 中的每个项目:我很确定这将是 O(N)。

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

    这是同一行:

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

    HashSet 在 .NET 3.5 中不存在,因此在 .NET 2.0 中您可以使用 Dictionary&lt;int,object&gt;(而不是使用 HashSet&lt;int&gt;),并且始终将 null 作为对象/值存储在 Dictionary 中,因为您只是对钥匙感兴趣。

    【讨论】:

    • Hashset 直到 .NET 3.5 才引入。
    • 散列一般不是一个坏主意。如有必要,实施一个并不难。
    • 在这种情况下,使用 .Net 2.0,您可以使用 Dictionary 而不是 HashSet (并且始终将 null 作为对象/值存储在 Dictionary 中,因为您只对键)。
    • 就我而言,这是最好的解决方案。谢谢@ChrisW
    【解决方案3】:

    不要遍历每个列表,而是看一下List.Contains 方法:

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

    【讨论】:

    • 这并不比原来的解决方案好:仍然是 O(N^2)
    • 教我在睡前发帖...在更深入地查看 Contains 方法时,它确实执行了列表的内部迭代。在这种情况下,一个 Dictionary 对象可能是更好的途径。
    【解决方案4】:

    Chris 通过散列给出了一个 O(N) 的解决方案。现在,根据常数因子(由于散列),可能值得通过排序考虑 O(N log(N)) 解决方案。根据您的用例,您可以考虑几种不同的变体。

    1. 对 ListB 排序 ( O(N log(N) ),并使用搜索算法解析 ListA 中的每个元素(同样是 O(N) * O(log(N)))。

    2. 对 ListA 和 ListB 进行排序 (O(N log(N)),并使用 O(N) 算法比较这些列表是否存在重复项。

    如果要多次使用这两个列表,则首选第二种方法。

    【讨论】:

      【解决方案5】:

      使用 BinarySearch 方法而不是遍历内部循环中的所有元素怎么样?

      【讨论】:

      猜你喜欢
      • 2020-10-21
      • 2015-08-04
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2018-03-01
      • 2021-02-23
      相关资源
      最近更新 更多