我正在尝试查找由数组中的两个或多个相邻数字组成的范围的索引和长度,该数组也可能存在于另一个数组中(但可能不是从相同的索引开始)。示例:
var source = new[] {6, 15, 8, 1, 2, 4, 11, 21};
var target = new[] {8, 1, 2, 4, 15, 11, 21, 6};
这两个数组不同,但是都具有两个相等的相邻数字[1, 2]
和[11, 21]
。我想编写一个可以在源数组中找到此类范围的起始索引和长度的函数(在这种情况下,索引3,第一个的长度为2,第二个索引的长度为2)。
我的最初方法可能是编写嵌套循环,该循环遍历两个数组中的每个字段并进行比较,但是这很快会变成大型数组的性能杀手。是否有任何有用的LINQ API或其他方法来查找这些值?
答案 0 :(得分:1)
这没有针对性能进行任何优化。我相信您可以使用某种类型的子字符串搜索优化(Boyer-Moore?)来提高性能。
通常,LINQ解决方案不会比过程解决方案更有效,尤其是在索引快速的数组上工作时。
使用两种扩展方法,您可以找到所有常见的子序列。首先,是一种扩展方法,可为序列生成所有可能的子序列:
public static IEnumerable<IEnumerable<T>> Subsequences<T>(this IEnumerable<T> src) =>
Enumerable.Range(0, src.Count() - 1)
.Select(k => src.Skip(k))
.SelectMany(s1s => Enumerable.Range(2, s1s.Count() - 1).Select(k => s1s.Take(k)));
这是通过跳过越来越多的开始项生成所有序列(至少长度为2),然后通过从末尾删除越来越多的项来生成这些序列的所有子序列来实现的。
使用两个参数Select
方法,您可以记住每个项目的位置以备后用。我将转换为List
,因此在调用Count
时子序列的生成效率更高:
var s1ps = s1.Select((n,i) => (n,i)).ToList().Subsequences();
var s2ps = s2.Select((n,i) => (n,i)).ToList().Subsequences();
现在,您可以从源代码中找到所有匹配的子序列。然后,您可以按匹配项在原始源中的开始位置进行分组,并保留最长的匹配项,并按结束位置进行分组,并保留最长的匹配项。为了解决这个问题,请使用扩展方法,该扩展方法通过键函数将IEnumerable
分组,然后根据值函数保持最大值:
public static IEnumerable<T> MaximumMatch<T, TKey>(this IEnumerable<T> src, Func<T,TKey> keyFn, Func<T,int> valueFn) =>
src.GroupBy(keyFn).Select(sg => sg.OrderByDescending(s => valueFn(s)).First());
应用此扩展两次后,您将获得所有最长的匹配子序列:
var ans = s1ps.SelectMany(as1p => s2ps.Where(as2p => as1p.Count() == as2p.Count()).Where(as2p => as1p.Select(sp => sp.n).SequenceEqual(as2p.Select(sp => sp.n))).Select(as2p => (as1p,as2p)))
.MaximumMatch(st => st.as1p.First().i, st => st.as1p.Count())
.MaximumMatch(st => st.as1p.Last().i, st => st.as1p.Count())
.Select(stg => new { s1begin = stg.as1p.First().i, s1end = stg.as1p.Last().i, s2begin = stg.as2p.First().i, s2end = stg.as2p.Last().i });
最后,获取每个匹配的子序列并将其投影到其起始和结束位置。