You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

C#中快速判断字符串是否包含列表所有元素的最优方案

高效验证原列表元素在超大字符串中的存在性

针对你遇到的问题——需要快速验证数百个短语(含多词短语)是否全部存在于数千万字符级的超大字符串中,原方案的核心问题是每个短语单独调用string.Contains都会遍历整个大字符串,时间复杂度为O(N*M)(N是短语数,M是字符串长度),重复开销极大。以下是几种更高效的优化方案:

方案一:短语HashSet预构建(推荐,实现简单高效)

如果你的短语最大单词数不大(比如≤5),这个方法能大幅降低时间复杂度:通过拆分超大字符串为单词列表,预先生成所有可能的连续短语存入HashSet,之后只需O(1)查询每个短语是否存在。

代码示例

List<string> uniqueWords = new List<string> { "two", "three", "weather sunday" };
string final = "two and tomorrow\n\rtwo or wednesday\n\rtwo with thursday\n\rtwo without friday\n\rthree gone tomorrow\n\rthree weather saturday\n\rthree timely sunday";

// 1. 定义单词分隔符(根据实际场景调整,比如加上逗号、分号等)
var wordSeparators = new[] {' ', '\n', '\r', '\t'};

// 2. 拆分超大字符串为单词列表
var finalWords = final.Split(wordSeparators, StringSplitOptions.RemoveEmptyEntries);

// 3. 统计原列表中短语的最大单词数,减少不必要的短语生成
int maxWordCount = uniqueWords.Max(item => item.Split(wordSeparators, StringSplitOptions.RemoveEmptyEntries).Length);

// 4. 预构建所有可能的连续短语HashSet(支持忽略大小写)
HashSet<string> existingPhrases = new HashSet<string>(StringComparer.CurrentCultureIgnoreCase);
for (int i = 0; i < finalWords.Length; i++)
{
    StringBuilder sb = new StringBuilder();
    // 生成从当前单词开始,长度1到maxWordCount的连续短语
    for (int len = 1; len <= maxWordCount && i + len <= finalWords.Length; len++)
    {
        if (len > 1)
            sb.Append(' ');
        sb.Append(finalWords[i + len - 1]);
        existingPhrases.Add(sb.ToString());
    }
}

// 5. 快速检查所有短语是否存在,收集缺失项
var missingItems = uniqueWords.Where(item => !existingPhrases.Contains(item)).ToList();

// 输出结果:这里会得到["weather sunday"]
foreach (var item in missingItems)
{
    Console.WriteLine($"缺失短语:{item}");
}

优势

  • 实现简单,无需复杂算法
  • 时间复杂度降至O(WL + N)(W是单词数,L是最大短语长度,N是原列表元素数),远低于原方案的O(NM)
  • 查询阶段是O(1)哈希查找,速度极快

方案二:Aho-Corasick多模式匹配算法(适合超大量短语场景)

如果你的短语数量极多(比如上千个)或短语长度差异极大,Aho-Corasick算法是最优选择:它只需遍历一次超大字符串,就能同时匹配所有目标短语,时间复杂度为O(M + K)(M是字符串长度,K是所有短语的总字符数)。

代码示例(简化版AC自动机实现)

public class AcNode
{
    public Dictionary<char, AcNode> Children { get; } = new Dictionary<char, AcNode>();
    public AcNode Fail { get; set; }
    public HashSet<string> MatchedPhrases { get; } = new HashSet<string>();
}

public class AcAutomaton
{
    private readonly AcNode _root = new AcNode();

    public void AddPattern(string pattern)
    {
        var current = _root;
        foreach (char c in pattern)
        {
            if (!current.Children.TryGetValue(c, out var child))
            {
                child = new AcNode();
                current.Children[c] = child;
            }
            current = child;
        }
        current.MatchedPhrases.Add(pattern);
    }

    public void BuildFailLinks()
    {
        Queue<AcNode> queue = new Queue<AcNode>();
        foreach (var child in _root.Children.Values)
        {
            child.Fail = _root;
            queue.Enqueue(child);
        }

        while (queue.Count > 0)
        {
            var current = queue.Dequeue();
            foreach (var (c, child) in current.Children)
            {
                var failNode = current.Fail;
                while (failNode != null && !failNode.Children.ContainsKey(c))
                {
                    failNode = failNode.Fail;
                }
                child.Fail = failNode?.Children.GetValueOrDefault(c) ?? _root;
                child.MatchedPhrases.UnionWith(child.Fail.MatchedPhrases);
                queue.Enqueue(child);
            }
        }
    }

    public HashSet<string> FindAllMatches(string text, StringComparison comparison = StringComparison.Ordinal)
    {
        HashSet<string> matches = new HashSet<string>();
        var current = _root;
        foreach (char c in text)
        {
            char targetC = comparison == StringComparison.OrdinalIgnoreCase ? char.ToLowerInvariant(c) : c;
            while (current != null && !current.Children.ContainsKey(targetC))
            {
                current = current.Fail;
            }
            current = current?.Children.GetValueOrDefault(targetC) ?? _root;
            foreach (var phrase in current.MatchedPhrases)
            {
                matches.Add(phrase);
            }
        }
        return matches;
    }
}

// 使用示例
var automaton = new AcAutomaton();
foreach (var phrase in uniqueWords)
{
    // 忽略大小写的话,统一转小写存入(或在匹配时处理)
    automaton.AddPattern(phrase.ToLowerInvariant());
}
automaton.BuildFailLinks();

// 匹配超大字符串
var matchedPhrases = automaton.FindAllMatches(final.ToLowerInvariant());

// 收集缺失项
var missingItems = uniqueWords.Where(item => !matchedPhrases.Contains(item.ToLowerInvariant())).ToList();

优势

  • 最优时间复杂度,适合超大量短语或超长字符串场景
  • 只需遍历一次超大字符串,避免重复扫描

方案三:ReadOnlySpan常数项优化(快速改进原方案)

如果不想改动太大,可通过ReadOnlySpan<char>优化原方案的Contains操作,减少字符串拷贝开销,性能比原生string.Contains提升约20%-30%:

ReadOnlySpan<char> finalSpan = final.AsSpan();
var missingItems = uniqueWords.Where(item => !finalSpan.Contains(item.AsSpan(), StringComparison.CurrentCultureIgnoreCase)).ToList();

内容的提问来源于stack exchange,提问作者Kennetic

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.20 22:24:28