【问题标题】:Find sequence in IEnumerable<T> using Linq使用 Linq 在 IEnumerable<T> 中查找序列
【发布时间】:2010-08-24 23:23:56
【问题描述】:

使用 LINQ 在IEnumerable&lt;T&gt; 中查找序列的最有效方法是什么

我希望能够创建一个允许以下调用的扩展方法:

int startIndex = largeSequence.FindSequence(subSequence)

匹配必须相邻且有序。

【问题讨论】:

  • largeSequence 有多大?这是实际使用还是概念性的?因为我可以想到几种方法,它们在相对较小(几千)的记录上会很好,但不一定很漂亮或在更大的环境中工作。
  • 我更喜欢可以很好地适应大序列的东西,实际的应用程序很小(只有几百个元素)但是它将进入我们的实用程序类,因此可以用于更大的序列未来。
  • 您希望 FindSequence 返回什么?索引?真假?子序列?要匹配的元素是否都必须有序且相邻?
  • @richard 我已经更新了问题以澄清
  • 是的,顺序和邻接的问题使它成为字符串搜索的概括。如果您不再将字符串视为固有地基于特定的 char 类型(在某些语言中它们不是),那么字符串与任何其他集合之间的主要实际区别之一是对集合进行排序往往使其至少同样有用如果不是更多,但对字符串进行排序会使其无用(“hello world”!=“dehllloorw”)。相关地,子序列和子字符串本质上是相同的操作。

标签: c# .net linq


【解决方案1】:

您可以使用这个名为 Sequences 的库来执行此操作(免责声明:我是作者)。

它有一个 IndexOfSlice 方法,可以完全满足您的需求 - 它是 Knuth-Morris-Pratt algorithmimplementation

int startIndex = largeSequence.AsSequence().IndexOfSlice(subSequence);

【讨论】:

  • 这太棒了!正是我需要的。谢谢。
【解决方案2】:

我知道这是一个老问题,但我需要这个确切的方法,我这样写:

public static int ContainsSubsequence<T>(this IEnumerable<T> elements, IEnumerable<T> subSequence) where T: IEquatable<T>
{
    return ContainsSubsequence(elements, 0, subSequence);
}

private static int ContainsSubsequence<T>(IEnumerable<T> elements, int index, IEnumerable<T> subSequence) where T: IEquatable<T>
{
    // Do we have any elements left?
    bool elementsLeft = elements.Any();

    // Do we have any of the sub-sequence left?
    bool sequenceLeft = subSequence.Any();

    // No elements but sub-sequence not fully matched
    if (!elementsLeft && sequenceLeft)
        return -1; // Nope, didn't match

    // No elements of sub-sequence, which means even if there are
    // more elements, we matched the sub-sequence fully
    if (!sequenceLeft)
        return index - subSequence.Count(); // Matched!

    // If we didn't reach a terminal condition,
    // check the first element of the sub-sequence against the first element
    if (subSequence.First().Equals(e.First()))
        // Yes, it matched - move onto the next. Consume (skip) one element in each
        return ContainsSubsequence(elements.Skip(1), index + 1 subSequence.Skip(1));
    else
        // No, it didn't match. Try the next element, without consuming an element
        // from the sub-sequence
        return ContainsSubsequence(elements.Skip(1), index + 1, subSequence);
}

更新为不仅在子序列匹配时返回,而是在原始序列中开始的位置。

这是 IEnumerable 上的扩展方法,完全惰性,提前终止,并且比当前投票的答案更加 linq-ified。但是请注意(正如@wai-ha-lee 指出的那样)它 是递归的,并创建了 lot 的枚举数。在适用的情况下使用它(性能/内存)。这对我的需求来说很好,但是 YMMV。

【讨论】:

  • 递归的 - 你不是创建了 lot 的迭代器来做到这一点吗?
  • 没问题。当我看到您在同一序列上使用 Any()Skip(...) 但随后看到问题要求 LINQ 答案时,我将建议直接操作 IEnumerator
【解决方案3】:

这是一个在序列中查找子序列的算法的实现。我将方法称为IndexOfSequence,因为它使意图更加明确,并且类似于现有的IndexOf 方法:

public static class ExtensionMethods
{
    public static int IndexOfSequence<T>(this IEnumerable<T> source, IEnumerable<T> sequence)
    {
        return source.IndexOfSequence(sequence, EqualityComparer<T>.Default);
    }

    public static int IndexOfSequence<T>(this IEnumerable<T> source, IEnumerable<T> sequence, IEqualityComparer<T> comparer)
    {
        var seq = sequence.ToArray();

        int p = 0; // current position in source sequence
        int i = 0; // current position in searched sequence
        var prospects = new List<int>(); // list of prospective matches
        foreach (var item in source)
        {
            // Remove bad prospective matches
            prospects.RemoveAll(k => !comparer.Equals(item, seq[p - k]));

            // Is it the start of a prospective match ?
            if (comparer.Equals(item, seq[0]))
            {
                prospects.Add(p);
            }

            // Does current character continues partial match ?
            if (comparer.Equals(item, seq[i]))
            {
                i++;
                // Do we have a complete match ?
                if (i == seq.Length)
                {
                    // Bingo !
                    return p - seq.Length + 1;
                }
            }
            else // Mismatch
            {
                // Do we have prospective matches to fall back to ?
                if (prospects.Count > 0)
                {
                    // Yes, use the first one
                    int k = prospects[0];
                    i = p - k + 1;
                }
                else
                {
                    // No, start from beginning of searched sequence
                    i = 0;
                }
            }
            p++;
        }
        // No match
        return -1;
    }
}

我没有完全测试它,所以它可能仍然包含错误。我只是对著名的极端案例进行了一些测试,以确保我没有落入明显的陷阱。到目前为止似乎工作正常......

我认为复杂度接近 O(n),但我不是大 O 表示法的专家,所以我可能是错的......至少它只枚举源序列一次,而不会返回,所以它应该是相当有效的。

【讨论】:

    【解决方案4】:

    你说你希望能够使用的代码不是 LINQ,所以我不明白为什么需要用 LINQ 来实现它。

    这与子字符串搜索本质上是相同的问题(实际上,顺序很重要的枚举是“字符串”的概括)。

    由于计算机科学长期以来一直在频繁考虑这个问题,所以你要站在巨人的肩膀上。

    一些合理的起点是:

    http://en.wikipedia.org/wiki/Knuth%E2%80%93Morris%E2%80%93Pratt_algorithm

    http://en.wikipedia.org/wiki/Boyer%E2%80%93Moore_string_search_algorithm

    http://en.wikipedia.org/wiki/Rabin-karp

    即使只是维基百科文章中的伪代码,也足以轻松移植到 C#。查看不同情况下的性能描述,并确定您的代码最有可能遇到哪些情况。

    【讨论】:

      【解决方案5】:

      更新: 鉴于问题的澄清,我下面的回答并不适用。出于历史目的而保留它。

      您可能想要使用 mySequence.Where()。然后关键是优化谓词以在您的环境中正常工作。根据您的要求和典型的使用模式,这可能会有很大差异。

      很可能适用于小型集合的方法不适用于更大的集合,具体取决于 T 的类型。

      当然,如果 90% 的使用是针对小型集合,那么针对异常值大型集合进行优化似乎有点 YAGNI。

      【讨论】:

      • 我想看看你将如何使用 where,它只需要一个元素来检查序列匹配。你能举个例子吗?
      • 您是对的,在重新阅读您的问题并看到更新后,我的回答不适用。
      猜你喜欢
      • 1970-01-01
      • 2012-09-17
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2010-12-26
      • 1970-01-01
      相关资源
      最近更新 更多