【问题标题】:C# fastest intersection of 2 sets of sorted numbers2组排序数字的C#最快交集
【发布时间】:2011-08-23 16:49:51
【问题描述】:

我在我的应用程序的时间关键部分计算 2 组排序数字的交集。这个计算是整个应用程序最大的瓶颈,所以我需要加快速度。

我尝试了很多简单的选项,目前正在使用这个:

foreach (var index in firstSet)
{
    if (secondSet.BinarySearch(index) < 0)
        continue;

    //do stuff
}

firstSetsecondSet 都是 List 类型。

我也尝试过使用 LINQ:

var intersection = firstSet.Where(t => secondSet.BinarySearch(t) >= 0).ToList();

然后循环遍历intersection

但是由于这两个集合都已排序,我觉得有更好的方法来做到这一点。请注意,我无法从集合中删除项目以使其更小。两组通常各包含大约 50 件物品。

请帮帮我,因为我没有太多时间来完成这件事。谢谢。

注意:我这样做了大约 530 万次。所以每一微秒都很重要。

【问题讨论】:

标签: c# .net intersection sortedset


【解决方案1】:

如果您有两个都已排序的集合,则可以实现比 LINQ 开箱即用的任何方法更快的交集。

基本上,保持两个IEnumerator&lt;T&gt; 光标打开,每组一个。在任何时候,以较小的价值前进。如果它们在任何时候都匹配,则将它们都推进,依此类推,直到到达任一迭代器的末尾。

这样做的好处是您只需对每个集合迭代一次,并且可以在 O(1) 内存中完成。

这是一个示例实现 - 未经测试,但它确实可以编译 :) 它假定两个传入的序列都是无重复的且已排序,都根据提供的比较器(传入Comparer&lt;T&gt;.Default):

(答案末尾有更多文字!)

static IEnumerable<T> IntersectSorted<T>(this IEnumerable<T> sequence1,
    IEnumerable<T> sequence2,
    IComparer<T> comparer)
{
    using (var cursor1 = sequence1.GetEnumerator())
    using (var cursor2 = sequence2.GetEnumerator())
    {
        if (!cursor1.MoveNext() || !cursor2.MoveNext())
        {
            yield break;
        }
        var value1 = cursor1.Current;
        var value2 = cursor2.Current;

        while (true)
        {
            int comparison = comparer.Compare(value1, value2);
            if (comparison < 0)
            {
                if (!cursor1.MoveNext())
                {
                    yield break;
                }
                value1 = cursor1.Current;
            }
            else if (comparison > 0)
            {
                if (!cursor2.MoveNext())
                {
                    yield break;
                }
                value2 = cursor2.Current;
            }
            else
            {
                yield return value1;
                if (!cursor1.MoveNext() || !cursor2.MoveNext())
                {
                    yield break;
                }
                value1 = cursor1.Current;
                value2 = cursor2.Current;
            }
        }
    }
}

编辑:如 cmets 中所述,在某些情况下,您的一个输入可能比另一个大得多,在这种情况下,您可以使用二进制搜索从较小集合中的每个元素节省大量时间更大的集合。然而,这需要随机访问更大的集合(这只是二进制搜索的先决条件)。您甚至可以通过使用 previous 结果中的匹配来为二进制搜索提供下限,使其比简单的二进制搜索稍好一些。因此,假设您在一个集合中查找值 1000、2000 和 3000,每个整数都从 0 到 19,999。在第一次迭代中,您需要查看整个集合 - 您的起始下/上索引将分别为 0 和 19,999。然而,当您在索引 1000 处找到匹配项后,下一步 步骤(您正在寻找 2000 的位置)可以从较低的索引 2000 开始。随着您的进步,您的范围需要搜索逐渐缩小。然而,这是否值得额外的实施成本是另一回事。

【讨论】:

  • 这和归并排序算法很像。
  • 它不会返回真正的交集,您假设列表具有相同的长度并且一个(或两个)不为空。
  • NOOOO,我对 Jon Skeet 的信念已经动摇,因为我相信存在错误。输入序列 {1,2} 和 {0,2} 会返回序列 {1,2} 但应该只返回 {2}。小错误是int value2 = cursor1.Current; 行当然1 应该是该行中的2
  • @Jonathan:我不认为我会假设其中任何一个。如果其中一个为空,则由于第一次调用MoveNext(),它将立即停止。之后,除非有匹配项,否则每次迭代只前进一个光标,在这种情况下,它会同时前进。试试看!
  • @Jon,是的,其余的似乎都很完美。你问我的确认真是太好了
【解决方案2】:

由于两个列表都已排序,因此您最多可以通过迭代它们一次来获得解决方案(您也可能会跳过一个列表的一部分,具体取决于它们包含的实际值)。

这个解决方案保留了一个指向我们尚未检查的列表部分的“指针”,并比较它们之间每个列表的第一个未检查的数字。如果一个小于另一个,则指向它所属列表的指针递增以指向下一个数字。如果它们相等,则将数字添加到交集结果中,并且两个指针都递增。

var firstCount = firstSet.Count;
var secondCount = secondSet.Count;
int firstIndex = 0, secondIndex = 0;
var intersection = new List<int>();

while (firstIndex < firstCount && secondIndex < secondCount)
{
    var comp = firstSet[firstIndex].CompareTo(secondSet[secondIndex]);
    if (comp < 0) {
        ++firstIndex;
    }
    else if (comp > 0) {
        ++secondIndex;
    }
    else {
        intersection.Add(firstSet[firstIndex]);
        ++firstIndex;
        ++secondIndex;
    }
}

以上是解决此特定问题的教科书 C 风格方法,鉴于代码的简单性,我会惊讶地看到更快的解决方案。

【讨论】:

  • 是的,这基本上是我方法的非流式版本 - 尽管您假设 CompareTo 总是返回 -1、0 或 1,而不是条件“小于 0” 、0 和“超过 0”。
  • @JonSkeet:是的。那里也有 C 风格的错误。 :) 已修复,感谢您发现它。
  • +1 是一个好方法。谢谢。你的更容易理解,如果我想将它与代码融合,那就太好了。例如,可以进行某种处理,而不是将值添加到交集列表。
【解决方案3】:

您正在为此类任务使用效率相当低的 Linq 方法,您应该选择 Intersect 作为起点。

var intersection = firstSet.Intersect(secondSet);

试试这个。如果你衡量它的性能,但仍然觉得它笨拙,请寻求进一步的帮助(或者也许遵循 Jon Skeet 的方法)。

【讨论】:

  • 我之前尝试过,但如果我没记错的话,它的效果最差,然后是上面的 2 个版本。无论如何,我认为 Jon Skeet 的解决方案是最快的。
  • @gligoran,这很好奇。我自己对 Where/Binary search 与 Intersect 的测试发现,大型数据集的性能有了巨大的提升,但我会让您使用自己的真实数据回忆自己的观察结果。
  • 我确实有一个大数据集,但是交集发生在算法的深处,所以我总是相交 2 组大约 50 个元素,但我必须做大约 530 万次。 AFAIK Intersect 不需要对集合进行排序,因此它会执行 2500 次双循环,如果我是正确的,Jon Skeet 的版本会在 50 次中执行。 Where/Binary 可能需要 50*log_2(50) 低于 300。但我可能错了。
  • @gligoran, Intersect 相当于将第一个集合加载到 HashSet(1 遍)中,然后检查它是否存在第二个集合的每个元素(1 遍)。 HashSet 中的查找将具有 O(1) 复杂度。它应该是相对最优的,但在处理已排序数据的情况下,不如 Jon 的方法那么最优。
【解决方案4】:

我使用的是 Jon 的方法,但需要在非常大的集合上执行此相交数十万次的批量操作,并且需要更高的性能。我遇到的情况是列表的大小严重不平衡(例如 5 和 80,000),并且希望避免迭代整个大列表。

我发现检测不平衡并更改为替代算法给了我对特定数据集的巨大好处:

public static IEnumerable<T> IntersectSorted<T>(this List<T> sequence1,
        List<T> sequence2,
        IComparer<T> comparer)
{
    List<T> smallList = null;
    List<T> largeList = null;

    if (sequence1.Count() < Math.Log(sequence2.Count(), 2))
    {
        smallList = sequence1;
        largeList = sequence2;
    }
    else if (sequence2.Count() < Math.Log(sequence1.Count(), 2))
    {
        smallList = sequence2;
        largeList = sequence1;
    }

    if (smallList != null)
    {
        foreach (var item in smallList)
        {
            if (largeList.BinarySearch(item, comparer) >= 0)
            {
                yield return item;
            }
        }
    }
    else
    {
        //Use Jon's method
    }
}

我仍然不确定你的收支平衡点,需要做更多的测试

【讨论】:

  • 我认为这是一种合理的方法,但是: * 你在 IEnumerables 上调用了 4 次 Count(),如果它们不是 List 或类似的,这可能会非常昂贵;更不用说多重枚举了。 * 这可以通过将第二个 if 设置为 else if 来改进。 * BinarySearch() 不适用于IEnumerable,因此仅当largeListListArray 时才有效;要么改变它的类型,要么在必要时强制转换它。
  • 更新为使用列表代替
【解决方案5】:

试试

firstSet.InterSect (secondSet).ToList ()

firstSet.Join(secondSet, o =&gt; o, id =&gt; id, (o, id) =&gt; o)

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 2022-01-21
    • 1970-01-01
    • 2011-01-24
    • 2014-10-19
    • 2017-07-21
    • 1970-01-01
    • 2013-02-10
    • 1970-01-01
    相关资源
    最近更新 更多