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

20

我最近被一位同事问到:是否可能通过一个查询仅获取数组中的前五个元素和后五个元素?

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);

是否可能通过一个查询仅获取前五个和后五个元素?


2
你尝试过这样写吗? var result = someArray.Take(5).Union(someArray.Skip(someArray.Count() - 5).Take(5)); - Mitat Koyuncu
someArray.Take(5).Concat(someArray.Reverse().Take(5)); 这个怎么样? - Nilesh
@MitatKoyuncu,“Union”会删除可能不是您想要的重复条目。如果您确实希望输出10个结果,请使用“Concat”。 - Drew Noakes
1
@StepUp,另外,如果“someArray”少于10个或5个输入怎么办?你的问题不够具体,无法提供一个好的、健壮的答案。 - Drew Noakes
1
@StepUp,没有所谓的愚蠢问题 :) 我的意思是你想要创建的方法的参数类型是什么?例如,它可以是 int[]T[](如果是泛型),但也可以根据常用于集合类的几个接口之一来定义。例如,大多数 linq 都是在非常通用的 IEnumerable<T> 上定义的。您还可以将其定义为 IReadOnlyList<T>IList<T>ICollection<T>... 您选择的“类型”决定了某人可以传递给该方法的有效集合,它还限制了实现可以使用的技术。 - Drew Noakes
显示剩余2条评论
7个回答

29

您可以使用带有 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

3
这个答案是这里唯一一个不会因为输入序列少于5个项目而出错的。但是,提问者从未指定这种情况下的行为 - 也就是说,重叠的头部和尾部应该在输出中重复吗? - Drew Noakes
考虑到 Enumerable.Take 在序列中的项数少于请求的项数时不会抛出异常,如果没有另外指定,我认为 @Fabjan 的解决方案具有“正确的行为”。 - Jaanus Varus
如果 someArray 的长度在区间 5..9 中,这个解决方案与我的解决方案(如下)不会得到相同的结果。问者能否保证数组始终足够长?如果数组的长度小于 5,则我的解决方案会抛出异常,而上述解决方案会输出所有条目一次。 - Jeppe Stig Nielsen
2
只是一件编程的事情,但我更喜欢使用(_, i) => i < 5 || i >= someArray.Length - 5。下划线_表示该变量未被使用。>=允许5对应于从列表末尾取出的元素数量。 - Dorus
Fabjan,这并不是对你答案的批评。正如@Discosultan所说,在数组长度小于10的情况下,你的解决方案可能是自然的。我只是想提到,当数组条目少于10个时,“正确”的含义有几种可能的解释。如果数组非常长,比如有1亿个条目,你的方法将不得不多次评估传递给“Where”的Func<,,>。在这种情况下,我的解决方案会更快。由于这可能是微观优化,因此可能完全无关紧要。 - Jeppe Stig Nielsen

11

一个带有ArraySegment<>的解决方案(需要.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)。


6

如果你不是与同事一起玩代码谜题,而只想按照自己的标准创建一个新数组,我不会使用查询来做这件事,而是使用Array.copy。

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

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

第三种情况是简单的情况,因为前五个和后五个元素是明确且定义良好的。

其他两种情况需要更多的思考。我将假设您想要以下内容,请检查这些假设:

如果源数组少于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)。


1
谢谢。这太棒了。然而,主要的标准是一行查询代码,而不是执行速度。不过,我真的很欣赏你的厉害算法!:) 给你加一分! - StepUp

4

试试这个:

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

2
这是Fabjan答案的副本,也不够详细。然而,使用> =使得5对应于实际采取的元素数量是很聪明的,另外我会用_代替a来表示一个未使用的变量。 - Dorus

3

这应该可以工作

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

3
与 OP 所写的有何不同之处? - MakePeaceGreatAgain
这样做效率相当低下。对Count()的调用会对源进行完整枚举,而Skip则需要大量工作。如果该函数要在IEnumerable<T>上工作,最好只枚举一次源。 - Drew Noakes
1
@Drew 在这种情况下,源是一个固定大小的数组,因此我会假设 Count() 使用一种重载来简单地返回数组的大小。 - Peter

2

试试这个:

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(...) 还要低效。 - Jeppe Stig Nielsen
3
完全取决于实现方式。 - NPSF3000
1
@NPSF3000 当然可以。但令我惊讶的是,对于巨大的数组,someArray.Reverse().Take(5).Reverse().ToArray()竟然比someArray.Skip(someArray.Length - 5).ToArray()更快。我查看了源代码,发现当传递给 SkipIEnumerable<T> 具有额外结构时,它不进行优化,因此以缓慢的方式进行迭代。而Reverse则创建了一个名为Buffer<T>的内部结构,其实例构造函数在确定源实现了ICollection<T>时使用了优化的CopyTo(T[], int)。因此,我的错误在于对实际实现的误解! - Jeppe Stig Nielsen
@JeppeStigNielsen 哈哈哈,太有趣了。当然,你只是使用了 LINQ 的一种可能实现方式,给 Skip 提供性能提升也是微不足道的。 - NPSF3000

1

Please try this:

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, };有什么期望? - Jeppe Stig Nielsen
1
@JeppeStigNielsen 你是对的。如果数组有重复记录,它将无法正常工作。 - Mitat Koyuncu

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接