【问题标题】:Iterate over strings that ".StartsWith" without using LINQ在不使用 LINQ 的情况下迭代“.StartsWith”的字符串
【发布时间】:2023-03-21 22:50:01
【问题描述】:

我正在构建一个自定义文本框,以便在社交媒体环境中提及人。这意味着我检测到有人何时键入“@”并在联系人列表中搜索“@”符号后面的字符串。 最简单的方法是使用 LINQ,类似于Members.Where(x => x.Username.StartsWith(str)。问题是潜在结果的数量可能非常高(高达 50,000 左右),而性能在这种情况下极为重要。

我有哪些替代解决方案?是否有任何类似于字典(基于哈希表的解决方案)的东西,但这将允许我使用 Key.StartsWith 而无需遍历每个条目?如果没有,实现这一目标的最快和最有效的方法是什么?

【问题讨论】:

  • 您不必一次全部加载。您绝对不会显示包含 50000 个结果的下拉列表,最多可能只有 10 个。因此,只需搜索您的列表并获得前 10 个左右的匹配项。当然,还要确保搜索是异步的,并且列表是有序的(或者甚至可能按字母顺序排列到字典中)以加快搜​​索速度。
  • 看看en.wikipedia.org/wiki/Trie StartWith 对于大型数据集来说不够好,尤其是在每次击键时都需要调用它时。

标签: c# performance linq uwp hashtable


【解决方案1】:

使用BinarySearch

这是一种非常正常的情况,假设数据存储在内存中,这是一种非常标准的处理方式。

  1. 使用普通的List<string>。您不需要 HashTable 或 SortedList。但是,IEnumerable<string> 不起作用;它必须是一个列表。

  2. 预先对列表进行排序(使用 LINQ,例如OrderBy( s => s)),例如在初始化期间或检索它时。这是整个方法的关键。

  3. 使用BinarySearch 查找最佳匹配的索引。因为列表是排序的,所以二分搜索可以非常快速地找到最佳匹配,而无需像 Select/Where 那样扫描整个列表。

  4. 获取找到的索引之后的前 N ​​个条目。如果不是所有的 N 个条目都是合适的匹配,您可以选择截断列表,例如如果有人输入“AZ”并且“BA”之前只有一两个项目。

例子:

public static IEnumerable<string> Find(List<string> list, string firstFewLetters, int maxHits)
{
    var startIndex = list.BinarySearch(firstFewLetters);

    //If negative, no match. Take the 2's complement to get the index of the closest match.
    if (startIndex < 0)
    {
        startIndex = ~startIndex;  
    }

    //Take maxHits items, or go till end of list
    var endIndex = Math.Min( 
                             startIndex + maxHits - 1, 
                             list.Count-1 
                           ); 

    //Enumerate matching items
    for ( int i = startIndex; i <= endIndex; i++ )
    {
        var s = list[i];
        if (!s.StartsWith(firstFewLetters)) break;  //This line is optional
        yield return s;
    }
}

单击 here 获取 DotNetFiddle 上的工作示例。

【讨论】:

    【解决方案2】:

    首先不清楚数据存储在哪里。所有名称都在内存中还是在数据库中?

    如果您将它们存储在数据库中,您可以在 ORM 中使用StartsWith 方法,这将转换为数据库上的LIKE 查询,这样就可以完成它的工作。如果在列上启用全文,则可以进一步提高性能。

    现在假设所有的名字都已经在内存中了。请记住,计算机 CPU 速度非常快,因此即使循环 50 000 个条目也只需片刻。 StartsWith 方法经过优化,一旦遇到不匹配的字符就会返回false。找到真正匹配的应该很快。但你仍然可以做得更好。

    正如其他人建议的那样,您可以构建一个 trie 来存储所有名称并能够非常快速地搜索匹配项,但有一个缺点 - 构建 trie 需要您读取所有名称并创建整个数据结构很复杂。此外,您只能使用给定的字符集,并且必须单独处理意外字符。

    但是,您可以将名称分组到“桶”中。首先从第一个字符开始,创建一个以字符为键、名称列表为值的字典。现在您有效地缩小了每个后续搜索大约 26 次(假设是英文字母)。但不必止步于此 - 您可以在另一个级别上执行此操作,针对每个组中的第二个角色。然后是第三个,依此类推。

    在每个级别中,您都有效地缩小了每个组的范围,之后搜索速度会更快。但是构建数据结构当然有前期成本,所以你总是必须为你找到正确的权衡。更多的前期工作 = 更快的搜索,更少的工作 = 更慢的搜索。

    最后,当用户输入每个新字母时,她会缩小目标组。因此,您始终可以维护当前输入的相关名称集,并在每次连续击键时将其删除。这将使您不必每次都从头开始,并显着提高效率。

    【讨论】:

      【解决方案3】:

      您必须显示 50000 的下拉列表吗?如果您可以限制下拉列表,例如,您可以只显示前 10 个。

      var filteredMembers = new List<MemberClass>
      foreach(var member in Members)
      {
          if(member.Username.StartWith(str)) filteredMembers.Add(member);
          if(filteredMembers >= 10) break;
      }
      

      或者: 除了您的集合之外,您还可以尝试将所有成员的用户名存储到 Trie 中。这应该会给你一个更好的性能,然后循环遍历所有 50000 个元素。 假设您的用户名是唯一的,您可以将您的成员信息存储在字典中并使用用户名作为键。 这当然是内存与性能的权衡。

      【讨论】:

      • DAWG 将是人们可能会考虑的另一种方法,而不是 trie。
      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 2018-09-16
      • 1970-01-01
      • 1970-01-01
      • 2018-04-17
      • 2020-10-14
      • 1970-01-01
      相关资源
      最近更新 更多