【问题标题】:.NET Generics - Compare two lists and filter: best practice.NET 泛型 - 比较两个列表和过滤器:最佳实践
【发布时间】:2011-06-19 22:03:40
【问题描述】:

我有两个 T 类型的通用列表。两个列表都包含相同的类型,我想根据列表 2 中不存在的项目创建第三个列表(或列表 2 的过滤版本) 1,基于每个项目的ID。

每个列表都包含一个“包”对象,该对象具有一个 ID 属性。

现在我使用 For Each 循环来模拟代码,我知道这很糟糕(大 O 是常数时间),所以我想要一个更有效的方法。

根据项目要求,此代码在 VB 中,但我更喜欢 C# - 所以任何一个代码示例都适合我。

Private Sub RemoveStockPackagesFromSelection()

    Dim p As Package
    Dim packageList As List(Of Package) = New List(Of Package)
    Dim stockPackageList As List(Of Package) = New List(Of Package)
    Dim result As List(Of Package) = New List(Of Package)

    ' Fill list with User's Packages
    For i As Integer = 0 To ListBox2.Items.Count - 1
        p = New Package
        p.Id = CInt(ListBox2.Items(i).Value)
        p.Name = ListBox2.Items(i).Text
        packageList.Add(p)
    Next

    ' Fill list with Stock Packages to compare:
    Dim ds As DataSet = DAL.GetStandardPackages()

    For Each dr As DataRow In ds.Tables(0).Rows
        p = New Package
        p.Id = CInt(dr.Item("id"))
        stockPackageList.Add(p)
    Next

    ' Do Compare and Filter
    For Each p1 As Package In packageList
        For Each p2 As Package In stockPackageList
            If Not p1.Id = p2.Id Then
                result.Add(p2)
            End If
        Next
    Next

    ' Here is our new trimmed list:
    Response.Write(result.Count)

End Sub

什么是进行这种过滤的干净利落的 LINQ 或 Lamda 方式?我的方法的大 O 是什么,建议的方法的大 O 是什么(只是为了满足我的好奇心)。

谢谢

【问题讨论】:

  • 你试过你的代码了吗?它不像你描述的那样工作。
  • Shawn,对于这样的问题,您可以省略基本列表的填充。我们相信您有两个列表。
  • 这里有一些惊人的答案是在极短的时间内完成的。我喜欢这个论坛。我将使用 IEqualityComparer 和 Zebi 的 LINQ 查询构建一个示例,因为这主要是我正在寻找的,但我应用了有关使用 HashSets、Dictionaries 和 .Except() 扩展方法的建议。所有非常可靠和有趣的解决方案。我正在尝试掌握 Big O(这似乎是 CSE 当天的热门话题),因此感谢您的洞察力。很好的答案...
  • 时间(和内存)复杂性一直是并且很可能永远是 CS 中的热门话题。
  • 诚然,算法编程一直存在并且将永远存在,无论出现什么新语言 - Big O 将永远是一个“热门话题”。猜猜这有点愚蠢...... UndoSelfDepriatingComment() :)

标签: c# .net vb.net list generics


【解决方案1】:

LINQ 除外方法

这将是最干净的方式,正如 Maxim 和 svick 所建议的那样,但需要一个重写的等于 ID 的 Equals 方法,或者您必须提供一个比较器(请参阅 svicks 答案)。

var result = stockPackageList.Except(packageList).ToList();

资源 许多 LINQ 示例可以在 http://msdn.microsoft.com/en-us/vcsharp/aa336746 的 msdn 中找到


我会将答案的开头部分留作参考:

蛮力方式:

var result = stockPackageList
              .Where(x => packageList.All(package => x.Id != package.Id))
              .ToList();

应该可以解决问题。您只需要将 lambda 语法转换为 vb.net。

此查询将过滤来自 stockPackageList 的所有项目,这些项目的 ID 不存在于 packageList 的所有项目中。

您可以反转查询:

var result = stockPackageList
              .Where(x => packageList.Any(package => x.Id == package.Id) == false)
              .ToList();

如果packageList 中的任何项目具有匹配的 id,Any 查询将返回 true。这个查询应该运行得更快一些,因为它不必像All 那样遍历整个集合。

使用平等:

如果您的包对象实现了IEquatable<Package>,您可以将代码缩短为

var result = stockPackageList
              .Where(x => packageList.Contains(x) == false)
              .ToList();

使用哈希集:

如果你想使用哈希集,你可以这样做

var hash = new HashSet<string>(packageList.Select(x=>x.Id));
var result = stockPackageList.Where(x => hash.Contains(x.Id) == false).ToList();

正如 faester 和 Ivan Danilov 指出的那样,当列表变大时,这可以节省计算时间。

【讨论】:

  • 你的 LINQ 技能是一流的 Zebi;感谢您的解决方案。我期待能够像您在这里一样轻松快速地编写 lamda 语法。你能帮我理解人类语法中的内容吗? package => x => x.Id != package.Id - 我不熟悉查询中的双 => 运算符。
  • 对不起,双 => 是一个错字:(如果你想了解 linq,试试这个链接:msdn.microsoft.com/en-us/vcsharp/aa336746,我也会编辑我的答案。
  • 看看Except方法,也提供了示例作为答案。
【解决方案2】:

无法真正阅读您的 VB 代码,但如果您想获取 l2 中的项目而不是 l1 中的项目 -

她是一个示例 C# 代码

   public class SomeObject
    {
        public string ID { get; set; }
    }

    public class SomeObjectComparer : IEqualityComparer<SomeObject>
    {
        public bool Equals(SomeObject x, SomeObject y)
        {
            return x.ID == y.ID;
        }

        public int GetHashCode(SomeObject obj)
        {
            return obj.ID.GetHashCode();
        }
    }

    class Program
    {
        static void Main(string[] args)
        {
            List<SomeObject> l1, l2;
            // lists init ...

            IEqualityComparer<SomeObject> comparer = new SomeObjectComparer();

            List<SomeObject> l3 = l2.Except(l1, comparer).ToList();

        }
    }

【讨论】:

  • 除了在一般列表上很慢,因为每个比较都是线性的。
  • @Ivan Except 在内部使用 Set 操作
  • 这似乎是“最佳实践”方法——非常干净、易读且面向对象。我将在我的生产代码中使用它。非常感谢@Maxim!
【解决方案3】:

您的方法具有 O(m*n) 的渐近运行时间,其中 m 和 n 是您的集合的大小。

你应该争取 O(m lg n)。您当然需要搜索这两个集合,但是您可以在 O(n) 中构建其中一个的哈希集并在平均 O(1) 中执行查询,因此您应该将一个列表复制到哈希集并遍历第二个查找提高前者的价值。

    static void Sort()
    {
        List<Package> a = new List<Package>();
        List<Package> b = new List<Package>();

        Func<Package, int> idExtractor = x => x.ID;

        var hash = new HashSet<Package>(a, new IDComparer<Package, int>(idExtractor));

        a.AddRange(b.Where(x => !hash.Contains(x)));
    }

    class IDComparer<ObjectType, KeyType>
        : IEqualityComparer<ObjectType>
        where KeyType : IComparable
    {
        private Func<ObjectType, KeyType> idExtractor;

        public IDComparer(Func<ObjectType, KeyType> idExtractor)
        {
            this.idExtractor = idExtractor;
        }

        public bool Equals(ObjectType x, ObjectType y)
        {
            return idExtractor(x).Equals(idExtractor(y));
        }

        public int GetHashCode(ObjectType obj)
        {
            return idExtractor(obj).GetHashCode();
        }
    }

【讨论】:

  • 创建一个哈希集是 O(N),而不是 O(N lg N)。
  • @swick:对不起,你是对的。我混淆了我不应该混淆的树和哈希集。我猜 O(n lg n) 模糊合理的情况是哈希集是否需要在构建过程中调整大小。如果初始大小设置不正确,并且策略是每次超出容量时将大小加倍,您将需要 O(lg n) 这样的重建,并额外复制 n 个元素。但鉴于我不知道这是否是 .NET 哈希集中的策略,我可能应该假设 O(n)。
  • 这是 .Net 使用的策略,但它仍然使总时间复杂度 O(N)。见Wikipedia
  • @swick:现在我觉得自己很愚蠢,但也有点聪明。实际上,我在大学做了一个关于摊销分析的项目,解释了确切的问题。记忆是骗人的。再次感谢!
【解决方案4】:

你可以使用Except():

result = stockpackageList.Except(PackageList).ToList();

这假定Package 已重载Equals() 以比较Ids。如果没有,您必须使用IEqualityComparer,例如来自this answer的那个:

result = stockpackageList.Except(PackageList, 
                                 new KeyEqualityComparer<Package, int>(p => p.Id))
                         .ToList();

至于时间复杂度,您的解决方案无法正常工作,因此它的复杂度无关紧要。 Except() 的复杂度是 O(N+M),因为它首先从第一个集合中创建一个哈希集 (O(N)),然后尝试从第二个集合中删除每个项目 (O(M)),然后返回结果。

【讨论】:

    【解决方案5】:
    var dic = new Dictionary<string, Package>(p1.Count); // capacity here is important
    foreach (var p in p1) 
        dic.Add(p.Id, p);
    var result = p2.Where(p => !dic.ContainsKey(p.Id)).ToList();
    

    填充字典几乎 O(n) by p1.Count。每次搜索都是 O(1)。所以我们有一些接近线性复杂度的东西。

    【讨论】:

    • 填充字典是 O(N),这个 O(N log N) 的概念从何而来? Dictionary 实现为哈希表。
    • 这是我在 Add() 方法文档中对“几乎”一词的猜测。我用反射器检查了来源。我不得不承认 n log n 是不正确的,但它也不是 O(N)。根据哈希冲突计数,单个加法可以是从 O(1) 到 O(N) 的任何内容。修正了我的答案,谢谢。
    • @Ivan,是的,但重要的不是单个添加,而是总时间。假设散列函数具有均匀分布,这平均为 O(N)。
    • 要明确:对于最初知道将插入多少元素和良好哈希分布的 HashSet,它肯定是平均线性的。让我怀疑的是,字典会被一一填满,并且必须不时地对存储桶进行昂贵的调整大小。
    • @Ivan,是的,但是调整大小的工作方式与 List&lt;T&gt; 相同,也就是说,大多数添加是 O(1),其中一些是 O(当前大小),但每个加法是 O(1),所以总时间是 O(N)。
    猜你喜欢
    • 2012-09-25
    • 2015-11-16
    • 1970-01-01
    • 2010-09-11
    • 2017-08-01
    • 2018-05-01
    • 1970-01-01
    • 2020-01-18
    • 1970-01-01
    相关资源
    最近更新 更多