具有许多输入的C#高效子串

时间:2009-07-24 18:16:54

标签: c# .net string .net-2.0

假设我不想使用外部库或超过十几个额外的代码行(即清除代码,代码高尔夫代码),我可以比string.Contains更好地处理一组输入字符串和一组要检查的关键字?

显然,可以使用objString.Contains(objString2)进行简单的子字符串检查。但是,有许多众所周知的算法在特殊情况下能够比这更好,特别是如果一个人使用多个字符串。但是将这样的算法粘贴到我的代码中可能会增加长度和复杂性,所以我宁愿使用某种基于内置函数的快捷方式。

E.g。输入将是字符串的集合,正面关键字的集合和否定关键字的集合。输出将是第一个关键字集合的子集,所有关键字都至少包含1个肯定关键字但0个否定关键字。

哦,请不要将正则表达式作为建议的解决方案。

可能是我的要求是互斥的(没有多少额外的代码,没有外部库或正则表达式,比String.Contains更好),但我想我会问。

编辑:

很多人只提供愚蠢的改进,如果有的话,不会超过智能使用的召唤。有些人试图更智能地调用Contains,这完全忽略了我的问题。所以这是一个尝试解决问题的例子。 LBushkin的解决方案是一个提供解决方案的例子,该解决方案可能比标准包含的渐进式更好:

假设您有10,000个长度为5-15个字符的正面关键字,0个负面关键字(这似乎让人感到困惑),以及1个1,000,000个字符串。检查1,000,000个字符串是否包含至少一个肯定关键字。

我想一个解决方案是创建一个FSA。另一个是划分空格和使用哈希。

7 个答案:

答案 0 :(得分:2)

您对“消极和积极”关键字的讨论有些令人困惑 - 并且可以使用一些澄清来获得更完整的答案。

与所有与性能相关的问题一样 - 您应首先编写简单版本,然后对其进行概要分析以确定瓶颈所在 - 这些可能不直观且难以预测。说完了......

优化搜索的一种方法可能是(如果你总是在搜索“单词” - 而不是可能包含空格的短语),那就是从你的字符串中建立一个搜索索引。

搜索索引可以是排序数组(用于二进制搜索)或字典。字典可能会更快 - 因为字典是内部使用O(1)查找的哈希映射,字典自然会消除搜索源中的重复值 - 从而减少了您需要执行的比较数。

一般搜索算法是:

对于您要搜索的每个字符串:

  • 获取您正在搜索的字符串并将其标记为单个单词(由空格分隔)
  • 将令牌填充到搜索索引(排序数组或字典)
  • 在索引中搜索“否定关键字”,如果找到,则跳至下一个搜索字符串
  • 在索引中搜索“肯定关键字”,找到一个时,将其添加到词典中(您还可以跟踪该词出现频率的计数)

以下是在C#2.0中使用排序数组和二进制搜索的示例:

注意:您可以轻松地从string[]切换到List<string>,我会留给您。

string[] FindKeyWordOccurence( string[] stringsToSearch,
                               string[] positiveKeywords, 
                               string[] negativeKeywords )
{
   Dictionary<string,int> foundKeywords = new Dictionary<string,int>();
   foreach( string searchIn in stringsToSearch )
   {
       // tokenize and sort the input to make searches faster 
       string[] tokenizedList = searchIn.Split( ' ' );
       Array.Sort( tokenizedList );

       // if any negative keywords exist, skip to the next search string...
       foreach( string negKeyword in negativeKeywords )
           if( Array.BinarySearch( tokenizedList, negKeyword ) >= 0 )
               continue; // skip to next search string...

       // for each positive keyword, add to dictionary to keep track of it
       // we could have also used a SortedList, but the dictionary is easier
       foreach( string posKeyword in positiveKeyWords )
           if( Array.BinarySearch( tokenizedList, posKeyword ) >= 0 )
               foundKeywords[posKeyword] = 1; 
   }

   // convert the Keys in the dictionary (our found keywords) to an array...
   string[] foundKeywordsArray = new string[foundKeywords.Keys.Count];
   foundKeywords.Keys.CopyTo( foundKeywordArray, 0 );
   return foundKeywordsArray;
}

这是一个在C#3.0中使用基于字典的索引和LINQ的版本:

注意:这不是LINQ-y最常用的方法,我可以使用Union()和SelectMany()将整个算法编写为一个大的LINQ语句 - 但我发现这更容易理解。

public IEnumerable<string> FindOccurences( IEnumerable<string> searchStrings,
                                           IEnumerable<string> positiveKeywords,
                                           IEnumerable<string> negativeKeywords )
    {
        var foundKeywordsDict = new Dictionary<string, int>();
        foreach( var searchIn in searchStrings )
        {
            // tokenize the search string...
            var tokenizedDictionary = searchIn.Split( ' ' ).ToDictionary( x => x );
            // skip if any negative keywords exist...
            if( negativeKeywords.Any( tokenizedDictionary.ContainsKey ) )
                continue;
            // merge found positive keywords into dictionary...
            // an example of where Enumerable.ForEach() would be nice...
            var found = positiveKeywords.Where(tokenizedDictionary.ContainsKey)
            foreach (var keyword in found)
                foundKeywordsDict[keyword] = 1;
        }
        return foundKeywordsDict.Keys;
    }

答案 1 :(得分:1)

如果您添加此扩展方法:

public static bool ContainsAny(this string testString, IEnumerable<string> keywords)
{
    foreach (var keyword in keywords)
    {
        if (testString.Contains(keyword))
            return true;
    }
    return false;
}

然后这成为一行声明:

var results = testStrings.Where(t => !t.ContainsAny(badKeywordCollection)).Where(t => t.ContainsAny(goodKeywordCollection));

这不一定比执行包含检查更快,除了它会有效地执行它们,因为LINQ的结果流阻止了任何不必要的包含调用....此外,生成的代码是一个单行检查是好的

答案 2 :(得分:1)

如果您真的只是寻找以空格分隔的单词,那么这段代码将是一个非常简单的实现:

    static void Main(string[] args)
    {
        string sIn = "This is a string that isn't nearly as long as it should be " +
            "but should still serve to prove an algorithm";
        string[] sFor = { "string", "as", "not" };
        Console.WriteLine(string.Join(", ", FindAny(sIn, sFor)));
    }

    private static string[] FindAny(string searchIn, string[] searchFor)
    {
        HashSet<String> hsIn = new HashSet<string>(searchIn.Split());
        HashSet<String> hsFor = new HashSet<string>(searchFor);
        return hsIn.Intersect(hsFor).ToArray();
    }

如果你只想要一个是/否答案(正如我现在看到的那样),还有另一种hashset“Overlaps”的方法可能会更好地优化:

    private static bool FindAny(string searchIn, string[] searchFor)
    {
        HashSet<String> hsIn = new HashSet<string>(searchIn.Split());
        HashSet<String> hsFor = new HashSet<string>(searchFor);
        return hsIn.Overlaps(hsFor);
    }

答案 3 :(得分:0)

嗯,你可以在字符串上调用Split()方法。您可以使用Split()将输入字符串拆分为单词数组,然后使用关键字对单词进行一对一检查。我不知道是否或在什么情况下这比使用Contains()更快。

答案 4 :(得分:0)

首先摆脱包含否定词的所有字符串。我建议使用Contains方法执行此操作。我认为Contains()比分割,排序和搜索更快。

答案 5 :(得分:0)

在我看来,最好的方法是使用匹配字符串(正面和负面)并计算它们的哈希值。然后游行你的百万字符串计算n哈希(在你的情况下,对于长度为5-15的字符串,它是10)并匹配你的匹配字符串的哈希值。如果你得到哈希匹配,那么你做一个实际的字符串比较来排除误报。有很多好的方法可以通过按长度分配匹配字符串并根据特定存储桶的字符串大小创建哈希值来优化这一点。

所以你会得到类似的东西:

IList<Buckets> buckets = BuildBuckets(matchStrings);
int shortestLength = buckets[0].Length;
for (int i = 0; i < inputString.Length - shortestLength; i++) {
    foreach (Bucket b in buckets) {
        if (i + b.Length >= inputString.Length)
            continue;
        string candidate = inputString.Substring(i, b.Length);
        int hash = ComputeHash(candidate);

        foreach (MatchString match in b.MatchStrings) {
            if (hash != match.Hash)
                continue;
            if (candidate == match.String) {
                if (match.IsPositive) {
                    // positive case
                }
                else {
                    // negative case
                }
            }
        }
    }
}

答案 6 :(得分:0)

要优化Contains(),您需要正/负词的树(或trie)结构。

这应该加快一切(O(n)vs O(nm),n =字符串的大小,m =平均字大小)并且代码相对较小&amp;容易。