在不使用LINQ的情况下迭代“.StartsWith”的字符串

时间:2018-01-07 22:52:43

标签: c# performance linq uwp hashtable

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

我有哪些替代解决方案?有没有类似于字典(基于散列表的解决方案)的东西,但是这样我可以使用Key.StartsWith而无需在每个条目上进行迭代?如果没有,那么实现这一目标的最快和最有效的方法是什么?

3 个答案:

答案 0 :(得分:2)

你必须显示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元素。 假设您的用户名是唯一的,您可以将您的成员信息存储在字典中,并使用用户名作为密钥。 当然,这是对记忆性能的权衡。

答案 1 :(得分:0)

首先存储数据的位置并不十分清楚。是内存中还是数据库中的所有名称?

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

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

正如其他人所说,你可以构建一个trie来存储所有的名字,并且能够很快地搜索匹配,但是有一个缺点 - 构建trie需要你阅读所有的名字并创建整个数据结构很复杂。此外,您将仅限于给定的一组字符,并且必须单独处理意外的字符。

但是,您可以将名称分组为“存储桶”。首先从第一个字符开始,然后创建一个字符,其中字符作为键,名称列表作为值。现在你有效地缩小了每次跟踪搜索大约26次(假设英文字母)。但是不必停在那里 - 你可以在另一个级别上执行此操作,对于每个组中的第二个角色。然后是第三个等等。

每个级别都可以显着缩小每个组的范围,之后搜索速度会更快。但是当然还有构建数据结构的前期成本,因此您必须始终为您找到合适的权衡。前期更多工作=更快的搜索,更少的工作=更慢的搜索。

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

答案 2 :(得分:0)

使用BinarySearch

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

  1. 使用普通List<string>。您不需要HashTable或SortedList。但是,IEnumerable<string>无效;它必须是一个清单。

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

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

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

  5. 示例:

    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上的工作样本。