如何高效地比较大整数列表和小整数列表?

29

目前我有一个包含100万个整数的列表list,我会把每个integer与2000个被禁用的integer进行比较。这个过程大约需要2分钟。

for(int i = 0; i< MillionIntegerList.Length ; i++)
{
    for(int blacklisted = 0; blacklisted < TwoThousandIntegerList.Length ; blacklisted++)
        if(i==blacklisted)
            i = 0; //Zero is a sentinel value 
}

这将总共进行2,000,000,000次迭代(循环)。我是否有没看到的更好的方法?谢谢


实际上,这个包含一百万个整数的列表已经排序好了,而黑名单却没有。 - theUser
但是黑名单排序的成本与您想要做的事情相比微不足道。 - Thomash
C#没有内置/简单的二分查找方法吗?因为较大的列表已经排序,根据哈希函数的复杂度,这可能比Jon Skeet的答案更快。 - Izkata
2
不要忘记,你发布的代码实际上无法工作(不能分配给'i',因为它是“foreach迭代变量”)。 - We Are All Monica
8个回答

50

现在有三个选项-前两个比较通用,不依赖于 MillionIntegerList 已经排序(最初没有指定)。第三种情况适用于大型列表已经排序的情况。

选项1

是的,有一个更好的方法,可以使用LINQ:

var common = MillionIntegerList.Intersect(TwoThousandIntegerList).ToList();

这将使用通过TwoThousandIntegerList构建的HashSet<int>,然后在其中查找MillionIntegerList的每个元素 - 这比每次遍历整个TwoThousandIntegerList要高效得多。

如果您只想要非黑名单中的元素,则需要:

var valid = MillionIntegerList.Except(TwoThousandIntegerList).ToList();

请注意,如果您只需对结果进行一次迭代,则应删除ToList调用 - 我已将其包含在内以实现结果的材料化,以便可以以低廉的成本多次检查。如果您只是迭代,IntersectExcept的返回值将只会stream结果,这将在内存使用方面更加便宜。 选项2 如果您不想依赖于LINQ to Objects的实现细节,但仍想采用基于哈希的方法:
var hashSet = new HashSet<int>(TwoThousandIntegerList);
hashSet.IntersectWith(MillionIntegerList);
// Now use hashSet

选项3

利用大列表已排序的方法肯定是有用的。

假设您不介意先将黑名单列表排序,那么您可以编写一个流式(并且通用)实现,例如(未经测试):

// Note: to use this, you'd need to make sure that *both* sequences are sorted.
// You could either sort TwoThousandIntegerList in place, or use LINQ's OrderBy
// method.

public IEnumerable<T> SortedIntersect<T>(this IEnumerable<T> first,
    IEnumerable<T> second) where T : IComparable<T>
{
    using (var firstIterator = first.GetEnumerator())
    {
        if (!firstIterator.MoveNext())
        {
            yield break;
        }

        using (var secondIterator = second.GetEnumerator())
        {
            if (!secondIterator.MoveNext())
            {
                yield break;
            }
            T firstValue = firstIterator.Current;
            T secondValue = secondIterator.Current;

            while (true)
            {
                int comparison = firstValue.CompareTo(secondValue);
                if (comparison == 0) // firstValue == secondValue
                {
                    yield return firstValue;
                }
                else if (comparison < 0) // firstValue < secondValue
                {
                    if (!firstIterator.MoveNext())
                    {
                        yield break;
                    }
                    firstValue = firstIterator.Current;
                }
                else // firstValue > secondValue
                {
                    if (!secondIterator.MoveNext())
                    {
                        yield break;
                    }
                    secondValue = secondIterator.Current;
                }  
            }                
        }
    }
}

(如果您希望,可以使用 IComparer<T> 来代替依赖于 T 可比较。)

2
@ErenErsönmez:不行,因为那会构建一个更大集合的哈希集。当涉及到Intersect时,MSDN中的文档实际上是不正确的-有关更多信息,请参见http://msmvps.com/blogs/jon_skeet/archive/2010/12/30/reimplementing-linq-to-objects-part-16-intersect-and-build-fiddling.aspx。 - Jon Skeet
称我为疯子吧,但我不喜欢这个答案,因为它依赖于LINQ的“Intersect”方法的实现细节(大多数人不知道)。相反,像丹尼尔的答案一样,我更喜欢对黑名单进行排序(便宜快速),然后使用单个for循环来实现合并类型算法。但是,如果您确实使用“Intersect”,则应包含解释原因的注释。 - We Are All Monica
@jnylen:我之前没有注意到排序性 - 现在已经按照Daniel的建议以通用方式实现了。 - Jon Skeet
我也不喜欢使用Intersect/Except解决方案,因为(1)它没有利用到MillionIntegerList已经排序的事实,而哈希查找在最好情况下是O(c),但在最坏情况下是O(m),其中m是要放入哈希表的列表的大小,而且这是一个实现细节(而且错误地记录了!),哪个列表被放入哈希表是不确定的。我更喜欢对TwoThousandIntegerList进行排序,然后并行遍历两个列表,这样排序的复杂度是O(mlog(m)),遍历的复杂度是O(n)。 - Old Pro
1
@OldPro:当我回答这个问题时,我们并不知道 MillionIntegerList 是已排序的 - 这并没有在原始问题中提到。请注意,我的回答现在包括了原始方法,一种基于哈希的方法,它 不依赖于任何实现细节,以及排序后遍历的方法。 - Jon Skeet
显示剩余2条评论

17

由于较大的列表已经排序,您可能会通过对小列表进行排序(非常快)然后执行线性合并来获得最佳结果。您只需要查看大(和小)列表中的每个项目一次,而且无需创建后台中的散列表。

请参阅MergeSort的merge function部分,以获取如何执行此操作的想法。


希望你不介意 - 我已经在我的答案中添加了这部分的通用实现。 - Jon Skeet

5
我认为你需要的是Enumerable.Except方法(IEnumerable,IEnumerable)。

请查看这里


3

您的方法需要O(n*n) 的时间。 考虑以下优化:

  • 1)

    如果您的整数不太大,可以使用布尔数组(例如,如果最大可能的整数为1000000,则使用bool[] b = new bool[1000000])。 现在要将数字K添加到黑名单中,请使用b[K] = true。 检查很简单。这种方法使用的时间复杂度为O(n)。您也可以使用BitArray。

  • 2)

    整数可能足够大。 使用二叉搜索树来存储黑名单(例如SortedSet)。它具有O(logN)的插入和检索时间。 因此总体时间复杂度为O(N*logN)。 语法与List相同(Add(int K),Contains(int K)),会忽略重复项。


如果较大的列表已经排序并且可以通过索引(例如数组)便宜地访问其元素,则在其上实现二分搜索算法几乎是微不足道的,将成本降低到O(n*log(m)),同时避免将所有数据复制到另一个数据结构(如树或哈希映射表)中。+1 提到了二分搜索算法。 - JimmyB
是的。如果“黑名单”未来不会更改,则可以使用二分搜索对其进行排序并查找项目,而无需像树这样的其他结构。 .NET已经实现了排序(Array.Sort和list.Sort)和二进制搜索(Array.BinarySearch和list.BinarySearch)。 - Oleg Golovkov

1

既然长列表已经排序了,为什么不在其上进行二分查找呢?

foreach(integer blacklisted in TwoThousandIntegerList)
{
    integer i  = MillionIntegerList.binarySearch(blacklisted)
    if(i==blacklisted){
          //Do your stuff
    } 
}

这个解决方案只需要 O(m log n) 的时间,其中 m 是小列表的大小,n 是较长列表的大小。 注意: 这个解决方案假设 MillionIntegerList 没有重复值。

如果不是这种情况,那么你可以遍历重复项,因为它们必须位于连续的块中。为此,我将假设 MillionInterList 是一个记录列表,每个记录都有一个 value 和一个 index

foreach(integer blacklisted in TwoThousandIntegerList)
{
    integer index = MillionIntegerList.binarySearch(blacklisted)
    //Find the index of the first occurrence of blacklisted value
    while(index > 0 && MillionIntegerList[index - 1].value == blacklisted){
          --index;
    }
    while(MillionIntegerList[index].value == blacklisted){
          //Do your stuff
          ++index;
    } 
}

这个解决方案的成本为 O(m log n + mk),其中k是在MillionInterList中找到的每个黑名单整数的平均重复数量。


1

我认为最好的解决方案是使用Bloom过滤器,如果Bloom过滤器指示一个元素可能在黑名单中,只需检查它是否不是误报(如果黑名单已排序,则可以在O(Log(n))内完成)。

这个解决方案时间效率高,几乎不需要额外的空间,比使用哈希集合要好得多。

这是谷歌在Chrome中用于黑名单的解决方案。


3
让我解释一下我的反对投票。1)它不能解决问题(误报问题)。2)你没有提到这是概率性的,并且不能真正解决问题。3)Chrome实际上只使用该过滤器来决定是否检查一个网页服务来实际加入黑名单,这点你没有提到。 - ex0du5
虚假阳性非常罕见,所以这不应该是一个问题。 - Thomash
  1. 实际上,我并没有详细解释布隆过滤器的所有内容,但我认为任何人都可以点击链接并阅读维基百科文章。
- Thomash
1
所以谷歌浏览器正是做了我建议要做的事情! - Thomash

0
使用 HashSet 作为阻止列表。
foreach(integer i in MillionIntegerList)
{
        //check if blockedlist contains i
        //do what ever you like. 
}

-2

使用 Except 方法来操作列表。这样做可以起到作用。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接