【问题标题】:Is there a LINQ alternative operator to Where() operator with List.Contains() method inside是否有一个 LINQ 替代运算符来代替 Where() 运算符,其中包含 List.Contains() 方法
【发布时间】:2013-09-30 21:23:26
【问题描述】:

使用 LINQ,有没有更快的替代 Where() 方法的方法,在谓词中使用 List<T>.Contains(),从而得到完全相同相同的结果?

这是一个例子:

List<int> a = ...
List<int> b = ...

var result = a.Where(x => b.Contains(x)); //very slow

我发现的另一种方法是使用Intersect() 方法:

var result = a.Intersect(b); 

result 变量中,a 值顺序被保留。 但是,如果 a 中的值包含重复值,则它不会提供完全相同的结果,因为 Intersect() 运算符只返回不同的值。

另一种方式:

var result = a.Join(b, x => x, y => y, (x, y) => x);

如果b 包含重复项,则结果也不相同。

还有其他可能吗?

我想避免的:

  • 创建我自己的 LINQ 扩展方法
  • 在第一个列表上创建一个单独的HashSet,并在Where() 内部使用Contains()

【问题讨论】:

  • a.Where(x =&gt; b.Contains(a)) 应该是a.Where(x =&gt; b.Contains(x))
  • 不幸的是,这在List&lt;T&gt; 上总是很慢,因为Contains 是一个 O(n) 操作。您可以尝试HashSet&lt;T&gt;,但请注意,为了更快,您需要覆盖T 类型的GetHashCodeEquals(假设T 是非框架类型)
  • @Mgetz,这是我在上一部分中使用Dictionary 提出的建议。但你是对的,使用HashSet 就足够了。
  • 您为什么反对将第二组中的项目放入HashSet
  • @Servy :我正在转换已经使用大量 Where() + Contains() 查询的代码,并且我想避免在其周围添加新变量。

标签: c# linq optimization


【解决方案1】:

从语义上讲,您想要的是一个左内连接。 LINQ Join 运算符执行 inner join,这很接近,但并不完全相同。幸运的是,您可以使用GroupJoin 来执行左连接

var query = from n in a
            join k in b
            on n equals k into matches
            where matches.Any()
            select n;

另一种选择是将第二个序列中的项目放入HashSet,这比List 更有效地搜索。 (这类似于 Join/GroupJoin 将在内部执行的操作。)

var set = new HashSet<int>(b);
var query = a.Where(n => set.Contains(n));

另一种选择是使用Join,就像您一样,但只需先从b 中删除所有重复项,因为如果没有重复项,那么它会执行您想要的操作:

var result = a.Join(b.Distinct(), x => x, y => y, (x, y) => x);

【讨论】:

  • +1 如果两个集合都有重复项,则需要“双组加入”。要么自己实现,要么加入两个普通 GroupBy 的结果。
【解决方案2】:

为了更快和重复,我会使用传统的“for”。

已编辑
我写了一个测试代码考虑:

  • 包含 1000 个随机整数的列表。
  • 每种方法 200 次测试。
  • 结果的 1、2、4 和 8 次使用表明需要将 LINQ 的IEnumerable&lt;int&gt; 结果转换为更好的数据结构,如List&lt;int&gt;,如果您多次使用该结果。

结果如下:

1 uses per result
Tigrou-Where        : count=  93,  3.167,0ms
Tigrou-Intersect    : count=  89,    116,0ms
Tigrou-Join         : count=  96,    179,0ms
Servy-GroupJoin     : count=  93,    262,0ms
Servy-HashSet       : count=  93,     71,0ms
Servy-JoinDisctinct : count=  93,    212,0ms
JoseH-TheOldFor     : count=  93,     72,0ms

2 uses per result
Tigrou-Where        : count=  93,  6.007,0ms
Tigrou-Intersect    : count=  89,    182,0ms
Tigrou-Join         : count=  96,    293,0ms
Servy-GroupJoin     : count=  93,    455,0ms
Servy-HashSet       : count=  93,     99,0ms
Servy-JoinDisctinct : count=  93,    407,0ms
JoseH-TheOldFor     : count=  93,     73,0ms

4 uses per result
Tigrou-Where        : count=  93, 11.866,0ms
Tigrou-Intersect    : count=  89,    353,0ms
Tigrou-Join         : count=  96,    565,0ms
Servy-GroupJoin     : count=  93,    899,0ms
Servy-HashSet       : count=  93,    165,0ms
Servy-JoinDisctinct : count=  93,    786,0ms
JoseH-TheOldFor     : count=  93,     73,0ms

8 uses per result
Tigrou-Where        : count=  93, 23.831,0ms
Tigrou-Intersect    : count=  89,    724,0ms
Tigrou-Join         : count=  96,  1.151,0ms
Servy-GroupJoin     : count=  93,  1.807,0ms
Servy-HashSet       : count=  93,    299,0ms
Servy-JoinDisctinct : count=  93,  1.570,0ms
JoseH-TheOldFor     : count=  93,     81,0ms

代码是:

class Program
{
    static void Main(string[] args)
    {
        Random random = new Random(Environment.TickCount);
        var cases = 1000;
        List<int> a = new List<int>(cases);
        List<int> b = new List<int>(cases);
        for (int c = 0; c < cases; c++)
        {
            a.Add(random.Next(9999));
            b.Add(random.Next(9999));
        }

        var times = 100;
        var usesCount = 1;

        Console.WriteLine("{0} times", times);
        for (int u = 0; u < 4; u++)
        {
            Console.WriteLine();
            Console.WriteLine("{0} uses per result", usesCount);
            TestMethod(a, b, "Tigrou-Where", Where, times, usesCount);
            TestMethod(a, b, "Tigrou-Intersect", Intersect, times, usesCount);
            TestMethod(a, b, "Tigrou-Join", Join, times, usesCount);
            TestMethod(a, b, "Servy-GroupJoin", GroupJoin, times, usesCount);
            TestMethod(a, b, "Servy-HashSet", HashSet, times, usesCount);
            TestMethod(a, b, "Servy-JoinDisctinct", JoinDistinct, times, usesCount);
            TestMethod(a, b, "JoseH-TheOldFor", TheOldFor, times, usesCount);
            usesCount *= 2;
        }

        Console.ReadLine();
    }

    private static void TestMethod(List<int> a, List<int> b, string name, Func<List<int>, List<int>, IEnumerable<int>> method, int times, int usesCount)
    {
        var stopwatch = new Stopwatch();
        stopwatch.Start();
        int count = 0;
        for (int t = 0; t < times; t++)
        {
            // Process
            var result = method(a, b);
            // Count
            for (int u = 0; u < usesCount; u++)
            {
                count = 0;
                foreach (var item in result)
                {
                    count++;
                }
            }
        }
        stopwatch.Stop();
        Console.WriteLine("{0,-20}: count={1,4}, {2,8:N1}ms", 
            name, count, stopwatch.ElapsedMilliseconds);
    }

    private static IEnumerable<int> Where(List<int> a, List<int> b)
    {
        return a.Where(x => b.Contains(x));
    }

    private static IEnumerable<int> Intersect(List<int> a, List<int> b)
    {
        return a.Intersect(b); 
    }

    private static IEnumerable<int> Join(List<int> a, List<int> b)
    {
        return a.Join(b, x => x, y => y, (x, y) => x);
    }

    private static IEnumerable<int> GroupJoin(List<int> a, List<int> b)
    {
        return from n in a
               join k in b
               on n equals k into matches
               where matches.Any()
               select n;
    }

    private static IEnumerable<int> HashSet(List<int> a, List<int> b)
    {
        var set = new HashSet<int>(b);
        return a.Where(n => set.Contains(n));
    }

    private static IEnumerable<int> JoinDistinct(List<int> a, List<int> b)
    {
        return a.Join(b.Distinct(), x => x, y => y, (x, y) => x);
    }

    private static IEnumerable<int> TheOldFor(List<int> a, List<int> b)
    {
        var result = new List<int>();
        int countA = a.Count;
        var setB = new HashSet<int>(b);
        for (int loopA = 0; loopA < countA; loopA++)
        {
            var itemA = a[loopA];
            if (setB.Contains(itemA))
                result.Add(itemA);
        }
        return result;
    }
}

在代码中更改一行以将结果转换为List&lt;int&gt;,然后再使用它会引发 8 次使用:

8 uses per result
Tigrou-Where        : count=  97,  2.974,0ms
Tigrou-Intersect    : count=  91,     91,0ms
Tigrou-Join         : count= 105,    150,0ms
Servy-GroupJoin     : count=  97,    224,0ms
Servy-HashSet       : count=  97,     74,0ms
Servy-JoinDisctinct : count=  97,    223,0ms
JoseH-TheOldFor     : count=  97,     75,0ms

所以,我认为赢家是:带有一点变体的 Servy-HashSet 方法:

var set = new HashSet<int>(b);
var result = a.Where(n => set.Contains(n)).ToList();

【讨论】:

  • 您打算如何使用for 循环来完成此任务?如果您计划有两个嵌套循环,那么您的性能将与他的第一个解决方案差不多,即糟糕,因为它本质上是一个糟糕的算法。
  • 你有证据证明它更快吗?
  • LINQ 比基于循环的算法慢很多。减速可能很明显,但不会改变渐近复杂度。
  • a.Where(x => b.Contains(x)) 有两个嵌套循环,一个用于 Where 另一个用于 Contains,加上调用从 lambda 表达式创建的匿名方法和调用 Contains 方法对于每个 x。
  • @JoséHurtado 问题仍然存在,您打算如何没有两个嵌套循环?
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 2019-01-14
  • 2013-07-27
  • 2016-09-20
  • 2011-12-29
  • 2021-10-16
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多