【问题标题】:Fast way of extraction of a list based on another list基于另一个列表提取列表的快速方法
【发布时间】:2013-03-06 11:45:02
【问题描述】:
List1: {"123456", "432978", "321675", …}  // containing 100,000 members

List2: {"7674543897", "1234568897", "8899776644",…} // containing 500,000 members

我想提取 List2 中前 6 位来自 List1 成员的所有项目,所以这里的字符串“1234568897”是有效的,因为它的前 6 位来自 List1 的第一项。 最快的方法是什么?

    foreach(string id in List1)
    {
    string result = List2.FirstOrDefault(x => x.Contains(id));
    if(result!=null)
      {
      //some works here
      }
}

这适用于少于 1000 个的组,但是当 List2 项目增长时,这需要很长时间

【问题讨论】:

  • 你已经尝试了什么?到目前为止,您为尝试设置了哪些计时机制和测试?
  • 使用单个 foreach 循环需要 5 分钟才能给出结果。我尝试过: List2.FirstOrDefault(x => x.Contains(id)) 并且 id 被放置在 foreach 循环中,遍历 List1 中的所有项目

标签: c#


【解决方案1】:

您可以使用Enumerable.Join,即quite efficient

var match = from str1 in List1
            join str2 in List2
            on str1 equals (str2.Length < 6 ? str2 : str2.Substring(0, 6))
            select str2;

Demo

编辑

由于@Oleksandr Pshenychnyy 认为这样大的集合会很慢,所以这里有一个演示,其中 list1 中有 100000 个随机字符串,list2 中有 500000 个字符串,范围与问题中的相同。它在 ideone 上执行时间为 600 毫秒:

http://ideone.com/rB6LU4

【讨论】:

  • 对于这么大的收藏品来说,这将是非常缓慢的方法
  • @OleksandrPshenychnyy:我已经用 list1 中的 100000 个随机字符串和 list2 中具有相同范围的 5000000 个字符串对其进行了测试,它在 700 毫秒内执行。我很快就会提供一个演示。
  • 我可以预计 Aho-Corasic 会在 10 毫秒左右执行(尽管我从未实现过它,因为它太复杂了 =))。我回答你只是因为问题是关于最快的算法,而不是最简单的实现。
  • @OleksandrPshenychnyy:我不知道你用的是哪台电脑。但是您的代码在 ideone 上出错(我假设超时或内存)。在我的服务器上,它需要的时间是我的 6 倍(3900 毫秒而不是 690 毫秒)。阅读我上面的链接,了解为什么 Enumerable.Join 很快。它在内部使用HashSets(非常快的查找性能)。
【解决方案2】:

这在很大程度上取决于您运行的硬件。 不过,您很可能正在进行过早的优化。简单地暴力破解它可能就足够快了。 500,000 * 100,000 是您的最大比较次数(即,如果没有匹配项)在合理规格的台式 PC 上确实不会花费很长时间。

如果您发现这对于您的目的来说太慢了,那么您可以使用一些技术来提高性能:

  1. 并行化它。这只会在多核硬件上显示出巨大的好处。从本质上讲,您需要一个调度程序类,它将 List2 中的数字提供给执行在 List1 中搜索匹配项的线程。请参阅Task Parellel Library

  2. 通过更智能来减少比较次数。对您的集合进行一些预分析,以改进它们的特征以进行比较步骤。例如将 List1 中的项目放入“桶”列表中,其中每个桶包含具有相同前 2 个数字的所有序列。例如存储桶 1 将包含以“00”开头的那些,存储桶 2“01”等。当您进行实际比较时,您只需比较少量字符串。从您的示例中,对于“1234568897”,我们将检查存储桶中的“12”,然后我们知道我们只需要与该存储桶中的字符串进行完整的字符串比较。

这种预处理实际上可能会使事情变慢,因此请确保您对代码进行概要分析。

【讨论】:

    【解决方案3】:

    实现您需要的最有效的方法是Aho-Corasick 的算法 - 如果您需要动态处理列表 2 的新元素。它基于前缀树,这是字符串搜索算法中常用的技术。这些算法会给你 O(所有字符串长度的总和)的复杂度

    另一个选项是Knuth-Morris-Pratt 算法,它会给你同样的复杂性,但最初只使用单个字符串进行搜索。

    但如果你对O((list2.Count*log(list2.Count) + list1.Count*log(list1.Count))*AverageStrLength) 没意见,我可以提出我的简单实现: 对两个列表进行排序。然后同时进行并选择匹配项。

    更新:当您已经对列表进行了排序时,此实现很好。然后它在大约 100 毫秒内执行。但是排序本身需要 3.5 秒,所以实现不如我最初预期的那么好。至于简单的实现,Tim 的解决方案更快。

    list1.Sort();
    list2.Sort();
    var result = new List<string>();
    for(int i=0, j=0; i<list1.Count; i++)
    {
        var l1Val = list1[i];
        for (; j < list2.Count && string.CompareOrdinal(l1Val, list2[j]) > 0; j++) ;
        for (; j < list2.Count && list2[j].StartsWith(l1Val); j++)
        {
            result.Add(list2[j]);
        }
    }
    

    这是我能提出的最简单有效的实现方式。

    【讨论】:

    • 这个“最简单有效的实现”比我的 "very slow" approach 慢 6 倍;)结果:“经过 3888,1323 毫秒。找到 52896 个匹配项”
    • 哎呀,对不起,一开始没有计算排序效率。当列表已经排序时速度很快=)。你是对的,总共差不多 4 秒 =(.
    • 这个 StartsWith 或 EndWith 或 Contains 都是一样的,而且它们在数量庞大时非常慢。 Tim Schmelter 的方法在我的笔记本电脑上运行了几次,平均为 750 毫秒,这太棒了!
    • 所有字符串操作都比较慢。至于 StartsWith vs Contains,第一个对于长字符串来说要快得多。正如我在 UPDATE 部分中所写,循环执行得很快,但我低估了排序时间。改用散列会更快,但 Tim 的解决方案以非常简单和有效的方式做到了。我投票给他=)。仅当您的列表经常更改并且您可以支持它们的排序顺序并遍历然后使用更复杂的 dataStructures 然后使用简单列表时,我的解决方案才可能有用。但现在看来情况并非如此。
    猜你喜欢
    • 2014-04-28
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2011-03-29
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多