【问题标题】:High-speed string matching in C#C#中的高速字符串匹配
【发布时间】:2011-12-30 02:17:10
【问题描述】:

List<T> 中有大约 10,000 名员工的列表,ListBox 包含这些员工的子集,具体取决于文本框中的搜索词。

假设Staff 对象具有以下公开的属性:

string FirstName
string LastName
string MiddleName
   int StaffID
   int CostCentre

我可以写一个这样的函数:

bool staffMatchesSearch(Staff stf)
{
  if (tbSrch.Text.Trim() == string.Empty)
    return true; // No search = match always.

  string s = tbSrch.Text.Trim().ToLower();

  // Do the checks in the order most likely to return soonest:
  if (stf.LastName.ToLower().Contains(s))
    return true;
  if (stf.FirstName.ToLower().Contains(s))
    return true;
  if (stf.MiddleName.ToLower().Contains(s))
    return true;

  if (stf.CostCentre.ToString().Contains(s))
    return true; // Yes, we want partial matches on CostCentre
  if (stf.StaffID.ToString().Contains(s))
    return true; // And also on StaffID

  return false;
}

然后执行以下操作:

tbSrch_TextChanged(object sender, EventArgs e)
{
  lbStaff.BeginUpdate();
  lbStaff.Items.Clear();

  foreach (Staff stf in staff)
    if (staffMatchesSearch(stf))
      lbStaff.Items.Add(stf);

  lbStaff.EndUpdate();
}

每次用户更改tbSrch 框的内容时,都会重新评估过滤。

这行得通,而且它不是非常慢,但我想知道我是否可以让它更快?

我曾尝试将整个内容重写为多线程,但是只有 10,000 名员工,开销似乎带走了大部分好处。此外,还有许多其他错误,例如如果搜索“John”,用户首先按下“J”,它会缠绕线程,但是当用户按下“o”时,另一组在第一批之前被缠绕有机会返回他们的结果。很多时候,结果以混乱的顺序返回,并且会发生各种令人讨厌的事情。

我可以想到一些调整,可以使最好的情况明显更好,但也会使最坏的情况变得更糟。

您对如何改进有任何想法吗?


我已经实施的好建议及其结果:

  • ValueChanged 事件中添加延迟,这样如果用户在键盘上快速键入 5 个字符的名称,它只会在末尾执行 1 次搜索,而不是连续执行 5 次搜索。
  • 预评估 ToLower() 并存储在 Staff 类中(作为 [NonSerialized] 属性,因此它不会占用保存文件中的额外空间)。
  • Staff 中添加一个get 属性,它将所有搜索条件作为单个、长、小写的串联字符串返回。然后在上面运行一个Contains()。 (这个字符串存储在Staff 对象中,所以它只被构造一次。)

到目前为止,这些已将搜索时间从大约 140 毫秒降低到大约 60 毫秒(尽管这些数字非常主观,具体取决于执行的实际搜索和返回的结果数量)。

【问题讨论】:

  • 你真的想toString那些ints吗?好像你想要一个匹配字符串方法和匹配 int 方法......我的意思是,如果我在中心 13,我不应该出现,因为有人搜索中心 1 或中心 3......
  • 尝试实现 Boyer-Moore 系列字符串搜索算法之一?预处理搜索词或 Staff 对象并重用结果可以节省大量时间。
  • 您真的要每次都搜索每个员工属性的所有匹配项吗?作为用户,我希望一次只搜索一两个已知字段。
  • @glowcoder 这正是他们正在寻找的行为。
  • 你能等到输入两个字母后再搜索,还是只在输入星号时才搜索?

标签: c# string search


【解决方案1】:

1) 正如 cmets 中所指出的,您可能不应该 .ToString 数字字段 - 只需匹配数字

2) ToLower 调用是性能消耗。将这些属性的小写版本添加到 Staff 类,这样 ToLower 只需执行一次

3) 当用户输入另一个字符时,您不需要重新评估所有项。输入一个字符只会减少匹配的数量,所以只需要重新评估之前的匹配。

4) 使用计时器在用户键入和开始搜索之间引入延迟。如果他们正在快速输入多个字符,您不妨等到他们暂停半秒

5) 检查按下的键。如果是 NaN 则不要检查 int 属性。

【讨论】:

  • "当用户输入另一个字符时,您不需要重新评估所有项目。输入一个字符只会减少匹配的数量,所以只重新评估以前的匹配"这只有在他们输入时才有效末尾的附加字符。这是一种特殊情况的优化。可能值得做,但它不会加速所有操作,并且可能会使其他操作的逻辑更加复杂。
  • @MerlynMorgan-Graham 公平点,但这种特殊情况可能是最常见的情况,因此可能值得代码复杂性
  • 也许还有:检查按下的键 - 如果它的 NaN 则不要检查 int 属性。
  • @BlueChippy - 不错。我将此答案标记为 wiki,因此请随时将其添加到列表中
  • 感谢@RobertLevy 还想知道:ToLower() 您正在搜索的所有属性,以及 ToLower 按键上的所有输入字符串。那么搜索必须始终不区分大小写(因为所有内容都较低)。
【解决方案2】:

您可以将“SearchTerm”私有属性添加到 (FirstName + LastName + MiddleName + StaffID + CostCentre).ToLower() 的 Staff 对象,并改为检查 Contains()。这将使您不必每次都对每个字符串执行ToLower(),并将Contains() 检查的次数从5 减少到1。

【讨论】:

  • 减少 Contains 调用所带来的好处可以忽略不计(一个大调用的工作量与小调用一样多)
  • 是的,对 Contains() 的更改不会有太大的不同,我认为这种方法的主要好处是无需每次都继续调用 ToLower()。
  • 问题是,我故意将它分成多个小的ifs,以便在最好的情况下,它会迅速用return true 解救。如果我将所有可能的搜索词添加到 (string1 + string2 + ... + stringN).ToLower().Contains() 中,那么我必须连接、处理和搜索更长的字符串。我认为这会运行得更慢(???)。
  • 我进行了一系列测试,发现您的方法平均在大约 75-85 毫秒内给出结果,而我的方法平均大约需要 136 毫秒。我将把它与预先评估的ToLower() 结合起来看看效果如何!
  • 它又缩短了 20 毫秒。使用预求值的小写字符串,平均只需要 60ms 左右,相当不错!
【解决方案3】:

您可以尝试实现trie 或“前缀树”:

http://en.wikipedia.org/wiki/Trie

这将允许您搜索以该值开始的文本。

我相信suffix-trees 上的链接文章将允许您进行全文搜索,尽管它具有更高的存储要求。

确保在将所有数据插入结构时ToLower,这样您在查找时就不必进行不区分大小写的比较。

【讨论】:

    【解决方案4】:

    看到您所做的事情(主要来自@mikel 的回答中的 cmets),听起来您已经到了那里。我还没有看到可以提高速度的一个建议是使用StringComparison.OrdinalIgnoreCase 进行比较。在你的情况下,这意味着替换

    if (stf.LastName.ToLower().Contains(s))
      return true;
    

    if (stf.LastName.IndexOf(s, StringComparison.OrdinalIgnoreCase) >= 0)
      return true;
    

    这是MSDN link,讨论快速字符串比较。

    【讨论】:

    • 这是一个非常好的主意,我肯定会在将来使用它,但是对于这种情况,我认为它已经过时了,因为我现在正在预先计算小写版本。谢谢:)
    • 看起来很合理,虽然你应该看看这个关于大写和小写的问题:stackoverflow.com/questions/9033/hidden-features-of-c#12137
    【解决方案5】:
    using System;
    using System.Text.RegularExpressions;
    namespace PatternMatching1
    {
        class Program
        {
            static void Main(string[] args)
            {
                try
                {
                    Console.WriteLine("Please enter the first string.");
                    string str = Console.ReadLine(); ;
                    string replacestr = Regex.Replace(str, "[^a-zA-Z0-9_]+", " ");
    
    
    
                    Console.WriteLine("Please enter the second string.");
                    string str1 = Console.ReadLine(); ;
                    string replacestr1 = Regex.Replace(str1, "[^a-zA-Z0-9_]+", " ");
    
    
    
                    if (replacestr.Length == replacestr1.Length)
                    {
                        char[] cFirst = replacestr.ToLower().ToCharArray();
                        char[] cSecond = replacestr1.ToLower().ToCharArray();
    
                        Array.Sort<char>(cFirst);
                        Array.Sort<char>(cSecond);
    
                        if ((new string(cFirst)).Equals((new string(cSecond))))
                            Console.WriteLine("Both String Same");
                        else
                            Console.WriteLine("Both String Not Same");
                    }
                    else
                        Console.WriteLine("oopsss, something going wrong !!!! try again");
                }
                catch (Exception ex)
                {
                    Console.WriteLine(ex.Message);
                }
                Console.Read();
            }
        }
    }
    

    `

    【讨论】:

      猜你喜欢
      • 2012-07-24
      • 2018-09-29
      • 1970-01-01
      • 2012-09-14
      • 2017-10-11
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多