使用 LINQ 通过一个查询从数组中获取前五个元素和后五个元素

Posted

技术标签:

【中文标题】使用 LINQ 通过一个查询从数组中获取前五个元素和后五个元素【英文标题】:Take the first five elements and the last five elements from an array by one query using LINQ 【发布时间】:2015-10-21 12:05:56 【问题描述】:

最近一位同事问我:是否可以通过一次查询从数组中获取前五个元素和后五个元素?

int[] someArray =  0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18 ;

我尝试过的:

int[] someArray = 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18 ;
var firstFiveResults = someArray.Take(5);
var lastFiveResults = someArray.Skip(someArray.Count() - 5).Take(5);
var result = firstFiveResults;
result = result.Concat(lastFiveResults);

一个查询是否可以只取前五个元素和后五个元素?

【问题讨论】:

你试过了吗 var result = someArray.Take(5).Union(someArray.Skip(someArray.Count() - 5).Take(5)); ? someArray.Take(5).Concat(someArray.Reverse().Take(5)); 怎么样? @MitatKoyuncu, Union 删除可能是也可能不是您想要的重复条目。如果你确实想要输出 10 个结果,请使用 Concat @StepUp,同样,如果someArray 的输入少于 10 个或少于 5 个怎么办。您的问题不够具体,无法提供一个好的、可靠的答案。 @StepUp,没有什么是愚蠢的问题 :) 我的意思是你想要为你创建的方法的参数是什么类型的?例如,它可以是int[]T[](如果是通用的),但是它也可以根据集合类常用的几个接口之一来定义。例如,大部分 linq 是在 IEnumerable<T> 上定义的,这是非常通用的。您也可以在 IReadOnlyList<T>IList<T>ICollection<T> 上定义它...您选择的“类型”决定了某人可以传递给该方法的有效集合,它还限制了实现可以使用的技术。 【参考方案1】:

您可以使用带有 lambda 的 .Where 方法,该方法接受元素索引作为其第二个参数:

int[] someArray =  0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18 ;

int[] newArray = someArray.Where((e, i) => i < 5 || i >= someArray.Length - 5).ToArray();

foreach (var item in newArray) 

    Console.WriteLine(item);

输出:

0, 1, 2, 3, 4, 14, 15, 16, 17, 18

【讨论】:

如果输入序列少于 5 个项目,此答案是唯一不会抛出的答案之一。但是 OP 从未为这种情况指定行为——即重叠的头部和尾部是否应该在输出中重复? 如果someArray 的长度在 5..9 区间内,此解决方案的结果与我的解决方案不同(如下)。提问者能否保证数组总是足够长?在数组长度小于 5 的情况下,我的解决方案会抛出异常,而上述解决方案会产生一次所有条目。 只是一个编码的东西,但我更喜欢(_, i) =&gt; i &lt; 5 || i &gt;= someArray.Length - 5_ 表示未使用该变量。 &gt;= 允许 5 对应于从列表末尾获取的元素数。 Fabjan,这并不是要批评您的回答。正如@Discosultan 所说,您的解决方案(在数组长度低于 10 的情况下)可能是一个自然的解决方案。我只是想提一下,当数组的条目少于 10 个时,对于“正确”的内容有几种可能的解释。如果数组非常长,比如 100,000,000 个条目,您的方法将不得不多次评估 Func&lt;,,&gt;passed 到 Where。在这种情况下,我的解决方案会更快。由于这可能是微优化,因此可能完全无关紧要。【参考方案2】:

ArraySegment&lt;&gt; 的解决方案(需要 .NET 4.5 (2012) 或更高版本):

var result = new ArraySegment<int>(someArray, 0, 5)
  .Concat(new ArraySegment<int>(someArray, someArray.Length - 5, 5));

还有Enumerable.Range的解决方案:

var result = Enumerable.Range(0, 5).Concat(Enumerable.Range(someArray.Length - 5, 5))
  .Select(idx => someArray[idx]);

这两种解决方案都避免遍历数组的“中间”(索引 5 到 13)。

【讨论】:

【参考方案3】:

如果您不是和同事一起玩代码拼图,而只是想根据您的条件创建一个新数组,我根本不会使用查询来执行此操作,而是使用 Array.copy。

需要考虑三种不同的情况:

源数组的项少于 5 个 源数组有 5 到 9 个项目 源数组有 10 个或更多项

第三种是简单的情况,因为第一个和最后 5 个元素是不同的且定义明确的。

另外两个需要更多的思考。我将假设你想要以下,检查这些假设:

如果源数组的项少于 5 个,您将需要一个包含 2 *(数组长度)项的数组,例如 [1, 2, 3] 变为 [1, 2, 3, 1, 2, 3]

如果源数组有 5 到 9 个项目,您将需要一个正好包含 10 个项目的数组,例如 [1, 2, 3, 4, 5, 6] 变为 [1, 2, 3, 4 , 5, 2, 3, 4, 5, 6]

一个演示程序是

public static void Main()

    Console.WriteLine(String.Join(", ", headandtail(new int[]1, 2, 3)));
    Console.WriteLine(String.Join(", ", headandtail(new int[]1, 2, 3, 4, 5, 6)));
    Console.WriteLine(String.Join(", ", headandtail(new int[]1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11)));


private static T[] headandtail<T>(T[] src) 
    int runlen = Math.Min(src.Length, 5);
    T[] result = new T[2 * runlen];
    Array.Copy(src, 0, result, 0, runlen);
    Array.Copy(src, src.Length - runlen, result, result.Length - runlen, runlen);
    return result;

在 O(1) 中运行;

如果您正在和同事一起玩代码拼图,那么所有的乐趣都在拼图中,不是吗?

这很简单。

src.Take(5).Concat(src.Reverse().Take(5).Reverse()).ToArray();

这在 O(n) 中运行。

【讨论】:

谢谢。太棒了。但是,主要标准是一行查询代码,而不是执行速度。但是,我真的很感谢您的出色算法!:) 从我这里拿一分!【参考方案4】:

试试这个:

var result = someArray.Where((a, i) => i < 5 || i >= someArray.Length - 5);

【讨论】:

这是 Fabjan 答案的副本,也不太广泛。但是使用&gt;= 使 5 对应于所取元素的实际数量是很聪明的,我也会用_ 替换a 以指示未使用的变量。【参考方案5】:

这应该可以工作

someArray.Take(5).Concat(someArray.Skip(someArray.Count() - 5)).Take(5);

【讨论】:

这和 OP 写的有什么不同? 这是相当低效的。对Count() 的调用执行源的完整枚举,Skip 做了很多工作。如果函数要在IEnumerable&lt;T&gt; 上工作,最好只枚举一次源。 @Drew 在这种情况下,源是一个固定大小的数组,所以我假设Count() 使用重载来简单地返回数组的大小。【参考方案6】:

试试这个:

int[] someArray = 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18 ;
var firstFiveResults = someArray.Take(5);
var lastFiveResults = someArray.Reverse().Take(5).Reverse();
var result = firstFiveResults;
result = result.Concat(lastFiveResults);

第二个 Reverse() 将数字重新排序,因此您不会得到 18,17,16,15,14

【讨论】:

两个Reverse() 将比Skip(...) 效率低。 @JeppeStigNielsen - 这完全取决于实现。 @NPSF3000 当然可以。但令我惊讶的是,someArray.Reverse().Take(5).Reverse().ToArray() 对于大型数组实际上比someArray.Skip(someArray.Length - 5).ToArray()。我检查了源代码,结果发现当传递的IEnumerable&lt;T&gt; 具有附加结构时,Skip 没有进行任何优化,因此它以缓慢的方式迭代。而Reverse 创建一个内部结构Buffer&lt;T&gt;,其实例构造函数在确定源实现ICollection&lt;T&gt; 时使用优化CopyTo(T[], int)。所以我在实际实现方面错了! @JeppeStigNielsen 呵呵,太搞笑了。当然,您只是在使用一种可能的 LINQ 实现,而给 Skip 带来性能提升是微不足道的。【参考方案7】:

请试试这个:

var result = someArray.Take(5).Union(someArray.Skip(someArray.Count() - 5).Take(5));

【讨论】:

Union 将丢弃重复项。该问题并不表示需要这种行为。 编辑:那么你对int[] someArray = 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, ;有什么期望? @JeppeStigNielsen 你是对的。如果数组有重复记录,则无法正常工作。

以上是关于使用 LINQ 通过一个查询从数组中获取前五个元素和后五个元素的主要内容,如果未能解决你的问题,请参考以下文章

如何获取字符串的前五个字符

读取一个文件,获取其中出现次数最多的前五个字符以及次数

从 Spotify Web 服务返回前五个值

如何从arraylist中获取其中的一个元素

读取一个文件,获取其中出现次数最多的前五个字符以及次数

如何使用 LINQ 的 AsQueryable() 从 MongoDB 的内部数组中获取数据?