รับชุดมีวิธีการรับองค์ประกอบ N สุดท้ายของคอลเลกชันที่? หากไม่มีวิธีใดในกรอบสิ่งที่จะเป็นวิธีที่ดีที่สุดในการเขียนวิธีการขยายให้ทำเช่นนี้?
รับชุดมีวิธีการรับองค์ประกอบ N สุดท้ายของคอลเลกชันที่? หากไม่มีวิธีใดในกรอบสิ่งที่จะเป็นวิธีที่ดีที่สุดในการเขียนวิธีการขยายให้ทำเช่นนี้?
คำตอบ:
collection.Skip(Math.Max(0, collection.Count() - N));
วิธีนี้จะรักษาลำดับของรายการโดยไม่ต้องพึ่งพาการเรียงลำดับใด ๆ และมีความเข้ากันได้ในวงกว้างในผู้ให้บริการ LINQ หลายราย
เป็นสิ่งสำคัญที่จะต้องระวังไม่ให้โทรออกSkip
ด้วยหมายเลขติดลบ ผู้ให้บริการบางรายเช่น Entity Framework จะสร้าง ArgumentException เมื่อมีข้อโต้แย้งเชิงลบ การเรียกร้องเพื่อMath.Max
หลีกเลี่ยงปัญหานี้อย่างเป็นระเบียบ
คลาสด้านล่างมีสิ่งจำเป็นทั้งหมดสำหรับวิธีการขยายซึ่ง ได้แก่ : คลาสแบบคงที่วิธีแบบคงที่และการใช้this
คำหลัก
public static class MiscExtensions
{
// Ex: collection.TakeLast(5);
public static IEnumerable<T> TakeLast<T>(this IEnumerable<T> source, int N)
{
return source.Skip(Math.Max(0, source.Count() - N));
}
}
หมายเหตุสั้น ๆ เกี่ยวกับประสิทธิภาพ:
เนื่องจากการเรียกใช้Count()
สามารถทำให้เกิดการแจงนับของโครงสร้างข้อมูลบางอย่างวิธีการนี้มีความเสี่ยงที่ทำให้เกิดการส่งผ่านข้อมูลสองครั้ง นี่ไม่ใช่ปัญหาจริง ๆ กับการแจกแจงส่วนใหญ่ อันที่จริงแล้วการปรับให้เหมาะสมมีอยู่แล้วสำหรับรายการ, อาร์เรย์และแม้กระทั่งแบบสอบถามของ EF เพื่อประเมินการCount()
ดำเนินการในเวลา O (1)
อย่างไรก็ตามหากคุณต้องใช้การนับล่วงหน้าเท่านั้นและต้องการหลีกเลี่ยงการทำสองรอบให้พิจารณาอัลกอริทึม One-Pass เช่นLasse V. KarlsenหรือMark Byersอธิบาย ทั้งสองวิธีใช้บัฟเฟอร์ชั่วคราวเพื่อเก็บรายการในขณะที่การแจกแจงซึ่งจะให้ผลเมื่อพบจุดสิ้นสุดของการรวบรวม
List
s และLinkedList
s การแก้ปัญหาของเจมส์มีแนวโน้มที่จะเร็วขึ้นแม้ว่าจะไม่ใช่ลำดับความสำคัญก็ตาม หากคำนวณ IEnumerable ได้ (ผ่าน Enumerable.Range เป็นต้น) โซลูชันของ James ใช้เวลานานกว่า ฉันไม่สามารถคิดวิธีใด ๆ ที่จะรับประกันการผ่านครั้งเดียวโดยไม่ทราบว่าจะมีการนำไปใช้หรือคัดลอกค่าไปยังโครงสร้างข้อมูลอื่น
coll.Reverse().Take(N).Reverse().ToList();
public static IEnumerable<T> TakeLast<T>(this IEnumerable<T> coll, int N)
{
return coll.Reverse().Take(N).Reverse();
}
อัปเดต: เพื่อแก้ไขปัญหาของ clintp: a) การใช้วิธี TakeLast () ที่ฉันกำหนดไว้ด้านบนแก้ปัญหาได้ แต่ถ้าคุณต้องการทำโดยไม่มีวิธีพิเศษคุณก็ต้องจำไว้ว่าในขณะที่ Enumerable () สามารถ ใช้เป็นวิธีส่วนขยายคุณไม่จำเป็นต้องใช้วิธีดังกล่าว:
List<string> mystring = new List<string>() { "one", "two", "three" };
mystring = Enumerable.Reverse(mystring).Take(2).Reverse().ToList();
List<string> mystring = new List<string>() { "one", "two", "three" }; mystring = mystring.Reverse().Take(2).Reverse();
ฉันได้รับข้อผิดพลาดคอมไพเลอร์เพราะ. Reverse () ผลตอบแทนที่ได้เป็นโมฆะและคอมไพเลอร์เลือกวิธีการนั้นแทน Linq หนึ่งที่ส่งกลับ IEnumerable ข้อเสนอแนะ?
N
บันทึกล่าสุดคุณสามารถข้ามรายการที่สองReverse
ได้
หมายเหตุ : ฉันพลาดชื่อคำถามของคุณซึ่งกล่าวว่าใช้ Linqดังนั้นคำตอบของฉันไม่ได้ใช้ Linq
หากคุณต้องการหลีกเลี่ยงการแคชสำเนาทั้งคอลเลคชั่นที่ไม่ขี้เกียจคุณสามารถเขียนวิธีการง่ายๆที่ใช้รายการที่เชื่อมโยง
วิธีการต่อไปนี้จะเพิ่มแต่ละค่าที่พบในคอลเลกชันดั้งเดิมลงในรายการที่เชื่อมโยงและตัดรายการที่เชื่อมโยงลงไปตามจำนวนรายการที่ต้องการ เนื่องจากมันทำให้รายการที่เชื่อมโยงถูกตัดออกเป็นจำนวนไอเท็มนี้ตลอดเวลาผ่านการวนซ้ำผ่านคอลเลกชันมันจะเก็บสำเนาของไอเท็ม N ส่วนใหญ่จากคอลเล็กชันดั้งเดิมเท่านั้น
ไม่ต้องการให้คุณทราบจำนวนรายการในคอลเลกชันดั้งเดิมหรือวนซ้ำมากกว่าหนึ่งครั้ง
การใช้งาน:
IEnumerable<int> sequence = Enumerable.Range(1, 10000);
IEnumerable<int> last10 = sequence.TakeLast(10);
...
วิธีการขยาย:
public static class Extensions
{
public static IEnumerable<T> TakeLast<T>(this IEnumerable<T> collection,
int n)
{
if (collection == null)
throw new ArgumentNullException(nameof(collection));
if (n < 0)
throw new ArgumentOutOfRangeException(nameof(n), $"{nameof(n)} must be 0 or greater");
LinkedList<T> temp = new LinkedList<T>();
foreach (var value in collection)
{
temp.AddLast(value);
if (temp.Count > n)
temp.RemoveFirst();
}
return temp;
}
}
นี่คือวิธีการที่ใช้งานได้กับการนับจำนวนใด ๆ แต่ใช้หน่วยเก็บชั่วคราว O (N) เท่านั้น:
public static class TakeLastExtension
{
public static IEnumerable<T> TakeLast<T>(this IEnumerable<T> source, int takeCount)
{
if (source == null) { throw new ArgumentNullException("source"); }
if (takeCount < 0) { throw new ArgumentOutOfRangeException("takeCount", "must not be negative"); }
if (takeCount == 0) { yield break; }
T[] result = new T[takeCount];
int i = 0;
int sourceCount = 0;
foreach (T element in source)
{
result[i] = element;
i = (i + 1) % takeCount;
sourceCount++;
}
if (sourceCount < takeCount)
{
takeCount = sourceCount;
i = 0;
}
for (int j = 0; j < takeCount; ++j)
{
yield return result[(i + j) % takeCount];
}
}
}
การใช้งาน:
List<int> l = new List<int> {4, 6, 3, 6, 2, 5, 7};
List<int> lastElements = l.TakeLast(3).ToList();
มันทำงานโดยใช้วงแหวนบัฟเฟอร์ขนาด N เพื่อจัดเก็บองค์ประกอบตามที่เห็นพวกเขาเขียนทับองค์ประกอบเก่ากับองค์ประกอบใหม่ เมื่อถึงจุดสิ้นสุดของการนับได้ถึงบัฟเฟอร์แหวนประกอบด้วยองค์ประกอบ N ล่าสุด
n
แต่คุณควรให้แน่ใจว่ามันจะเป็นสิ่งที่ถูกต้องเมื่อคอลเลกชันที่มีองค์ประกอบน้อยกว่า
.NET Core 2.0+ ให้วิธีการ LINQ TakeLast()
:
https://docs.microsoft.com/en-us/dotnet/api/system.linq.enumerable.takelast
ตัวอย่าง :
Enumerable
.Range(1, 10)
.TakeLast(3) // <--- takes last 3 items
.ToList()
.ForEach(i => System.Console.WriteLine(i))
// outputs:
// 8
// 9
// 10
netcoreapp1.x
) แต่เฉพาะสำหรับ v2.0 & v2.1 ของ dotnetcore ( netcoreapp2.x
) เป็นไปได้ว่าคุณอาจกำหนดเป้าหมายเป็นเฟรมเวิร์กแบบเต็ม (เช่นnet472
) ซึ่งไม่รองรับเช่นกัน (. Lib มาตรฐานมาตรฐานสามารถใช้งานได้จากข้อใดข้อหนึ่งข้างต้น แต่อาจเปิดเผย API บางตัวที่เฉพาะเจาะจงกับเฟรมเวิร์กเป้าหมายดูdocs.microsoft.com/en-us/dotnet/standard/frameworks )
ฉันกำลังประหลาดใจที่ไม่มีใครได้พูดถึงเรื่องนี้ แต่ SkipWhile จะมีวิธีการที่ใช้ดัชนีองค์ประกอบของ
public static IEnumerable<T> TakeLastN<T>(this IEnumerable<T> source, int n)
{
if (source == null)
throw new ArgumentNullException("Source cannot be null");
int goldenIndex = source.Count() - n;
return source.SkipWhile((val, index) => index < goldenIndex);
}
//Or if you like them one-liners (in the spirit of the current accepted answer);
//However, this is most likely impractical due to the repeated calculations
collection.SkipWhile((val, index) => index < collection.Count() - N)
ประโยชน์ที่รับรู้ได้เพียงอย่างเดียวที่โซลูชันนี้นำเสนอเหนือผู้อื่นคือคุณสามารถมีตัวเลือกในการเพิ่มคำกริยาเพื่อสร้างเคียวรี LINQ ที่ทรงพลังและมีประสิทธิภาพมากกว่าแทนที่จะแยกการดำเนินการสองอย่างที่สำรวจ IEnumerable สองครั้ง
public static IEnumerable<T> FilterLastN<T>(this IEnumerable<T> source, int n, Predicate<T> pred)
{
int goldenIndex = source.Count() - n;
return source.SkipWhile((val, index) => index < goldenIndex && pred(val));
}
ใช้ EnumerableEx.TakeLast ในแอสเซมบลี System.Interactive ของ RX เป็นการใช้งาน O (N) เช่น @ Mark's แต่ใช้คิวแทนการสร้าง ring-buffer (และ dequeues รายการเมื่อถึงความจุบัฟเฟอร์)
(หมายเหตุ: นี่คือรุ่น IEnumerable - ไม่ใช่รุ่น IObservable แม้ว่าการใช้งานของทั้งสองจะเหมือนกันมาก)
Queue<T>
ใช้งานไม่ได้โดยใช้บัฟเฟอร์แบบวงกลมใช่ไหม
หากคุณกำลังจัดการกับคอลเลกชันที่มีคีย์ (เช่นรายการจากฐานข้อมูล) โซลูชันที่รวดเร็ว (เช่นเร็วกว่าคำตอบที่เลือก) จะเป็น
collection.OrderByDescending(c => c.Key).Take(3).OrderBy(c => c.Key);
หากคุณไม่สนใจที่จะจิ้ม Rx เป็นส่วนหนึ่งของ monad คุณสามารถใช้TakeLast
:
IEnumerable<int> source = Enumerable.Range(1, 10000);
IEnumerable<int> lastThree = source.AsObservable().TakeLast(3).AsEnumerable();
หากใช้ห้องสมุดบุคคลที่สามเป็นตัวเลือกMoreLinq จะกำหนดTakeLast()
ว่าจะทำสิ่งใด
ฉันพยายามรวมประสิทธิภาพและความเรียบง่ายเข้าด้วยกัน:
public static IEnumerable<T> TakeLast<T>(this IEnumerable<T> source, int count)
{
if (source == null) { throw new ArgumentNullException("source"); }
Queue<T> lastElements = new Queue<T>();
foreach (T element in source)
{
lastElements.Enqueue(element);
if (lastElements.Count > count)
{
lastElements.Dequeue();
}
}
return lastElements;
}
เกี่ยวกับประสิทธิภาพ: ใน C # Queue<T>
มีการใช้งานโดยใช้บัฟเฟอร์แบบวงกลมดังนั้นจึงไม่มีการสร้างอินสแตนซ์ของวัตถุในแต่ละลูป (เฉพาะเมื่อคิวโตขึ้น) ผมไม่ได้ตั้งความจุคิว (ใช้เฉพาะคอนสตรัค) count = int.MaxValue
เพราะใครบางคนอาจจะเรียกส่วนขยายนี้ด้วย เพื่อประสิทธิภาพที่เพิ่มขึ้นคุณอาจตรวจสอบว่ามีการนำซอร์สมาใช้หรือไม่IList<T>
และหากใช่ให้ดึงค่าสุดท้ายโดยตรงโดยใช้ดัชนีอาร์เรย์
มันไม่มีประสิทธิภาพเล็กน้อยที่จะใช้ N สุดท้ายของคอลเล็กชันโดยใช้ LINQ เนื่องจากโซลูชันทั้งหมดข้างต้นต้องการการวนซ้ำในคอลเลกชัน TakeLast(int n)
ในSystem.Interactive
ยังมีปัญหานี้
หากคุณมีรายการสิ่งที่มีประสิทธิภาพมากขึ้นที่จะทำคือชิ้นโดยใช้วิธีการดังต่อไปนี้
/// Select from start to end exclusive of end using the same semantics
/// as python slice.
/// <param name="list"> the list to slice</param>
/// <param name="start">The starting index</param>
/// <param name="end">The ending index. The result does not include this index</param>
public static List<T> Slice<T>
(this IReadOnlyList<T> list, int start, int? end = null)
{
if (end == null)
{
end = list.Count();
}
if (start < 0)
{
start = list.Count + start;
}
if (start >= 0 && end.Value > 0 && end.Value > start)
{
return list.GetRange(start, end.Value - start);
}
if (end < 0)
{
return list.GetRange(start, (list.Count() + end.Value) - start);
}
if (end == start)
{
return new List<T>();
}
throw new IndexOutOfRangeException(
"count = " + list.Count() +
" start = " + start +
" end = " + end);
}
กับ
public static List<T> GetRange<T>( this IReadOnlyList<T> list, int index, int count )
{
List<T> r = new List<T>(count);
for ( int i = 0; i < count; i++ )
{
int j=i + index;
if ( j >= list.Count )
{
break;
}
r.Add(list[j]);
}
return r;
}
และบางกรณีทดสอบ
[Fact]
public void GetRange()
{
IReadOnlyList<int> l = new List<int>() { 0, 10, 20, 30, 40, 50, 60 };
l
.GetRange(2, 3)
.ShouldAllBeEquivalentTo(new[] { 20, 30, 40 });
l
.GetRange(5, 10)
.ShouldAllBeEquivalentTo(new[] { 50, 60 });
}
[Fact]
void SliceMethodShouldWork()
{
var list = new List<int>() { 1, 3, 5, 7, 9, 11 };
list.Slice(1, 4).ShouldBeEquivalentTo(new[] { 3, 5, 7 });
list.Slice(1, -2).ShouldBeEquivalentTo(new[] { 3, 5, 7 });
list.Slice(1, null).ShouldBeEquivalentTo(new[] { 3, 5, 7, 9, 11 });
list.Slice(-2)
.Should()
.BeEquivalentTo(new[] {9, 11});
list.Slice(-2,-1 )
.Should()
.BeEquivalentTo(new[] {9});
}
ฉันรู้ว่ามันสายเกินไปที่จะตอบคำถามนี้ แต่ถ้าคุณกำลังทำงานกับคอลเลกชันประเภท IList <> และคุณไม่สนใจเกี่ยวกับคำสั่งของคอลเลกชันที่ส่งคืนแล้ววิธีนี้จะทำงานได้เร็วขึ้น ฉันใช้คำตอบของ Mark Byersแล้วทำการเปลี่ยนแปลงเล็กน้อย ดังนั้นตอนนี้วิธี TakeLast คือ:
public static IEnumerable<T> TakeLast<T>(IList<T> source, int takeCount)
{
if (source == null) { throw new ArgumentNullException("source"); }
if (takeCount < 0) { throw new ArgumentOutOfRangeException("takeCount", "must not be negative"); }
if (takeCount == 0) { yield break; }
if (source.Count > takeCount)
{
for (int z = source.Count - 1; takeCount > 0; z--)
{
takeCount--;
yield return source[z];
}
}
else
{
for(int i = 0; i < source.Count; i++)
{
yield return source[i];
}
}
}
สำหรับการทดสอบฉันได้ใช้วิธีการมาร์ค Byers และ andswer นี่คือการทดสอบ:
IList<int> test = new List<int>();
for(int i = 0; i<1000000; i++)
{
test.Add(i);
}
Stopwatch stopwatch = new Stopwatch();
stopwatch.Start();
IList<int> result = TakeLast(test, 10).ToList();
stopwatch.Stop();
Stopwatch stopwatch1 = new Stopwatch();
stopwatch1.Start();
IList<int> result1 = TakeLast2(test, 10).ToList();
stopwatch1.Stop();
Stopwatch stopwatch2 = new Stopwatch();
stopwatch2.Start();
IList<int> result2 = test.Skip(Math.Max(0, test.Count - 10)).Take(10).ToList();
stopwatch2.Stop();
และนี่คือผลลัพธ์สำหรับการรับ 10 องค์ประกอบ:
และสำหรับการรับผล 1000001 องค์ประกอบคือ:
นี่คือทางออกของฉัน:
public static class EnumerationExtensions
{
public static IEnumerable<T> TakeLast<T>(this IEnumerable<T> input, int count)
{
if (count <= 0)
yield break;
var inputList = input as IList<T>;
if (inputList != null)
{
int last = inputList.Count;
int first = last - count;
if (first < 0)
first = 0;
for (int i = first; i < last; i++)
yield return inputList[i];
}
else
{
// Use a ring buffer. We have to enumerate the input, and we don't know in advance how many elements it will contain.
T[] buffer = new T[count];
int index = 0;
count = 0;
foreach (T item in input)
{
buffer[index] = item;
index = (index + 1) % buffer.Length;
count++;
}
// The index variable now points at the next buffer entry that would be filled. If the buffer isn't completely
// full, then there are 'count' elements preceding index. If the buffer *is* full, then index is pointing at
// the oldest entry, which is the first one to return.
//
// If the buffer isn't full, which means that the enumeration has fewer than 'count' elements, we'll fix up
// 'index' to point at the first entry to return. That's easy to do; if the buffer isn't full, then the oldest
// entry is the first one. :-)
//
// We'll also set 'count' to the number of elements to be returned. It only needs adjustment if we've wrapped
// past the end of the buffer and have enumerated more than the original count value.
if (count < buffer.Length)
index = 0;
else
count = buffer.Length;
// Return the values in the correct order.
while (count > 0)
{
yield return buffer[index];
index = (index + 1) % buffer.Length;
count--;
}
}
}
public static IEnumerable<T> SkipLast<T>(this IEnumerable<T> input, int count)
{
if (count <= 0)
return input;
else
return input.SkipLastIter(count);
}
private static IEnumerable<T> SkipLastIter<T>(this IEnumerable<T> input, int count)
{
var inputList = input as IList<T>;
if (inputList != null)
{
int first = 0;
int last = inputList.Count - count;
if (last < 0)
last = 0;
for (int i = first; i < last; i++)
yield return inputList[i];
}
else
{
// Aim to leave 'count' items in the queue. If the input has fewer than 'count'
// items, then the queue won't ever fill and we return nothing.
Queue<T> elements = new Queue<T>();
foreach (T item in input)
{
elements.Enqueue(item);
if (elements.Count > count)
yield return elements.Dequeue();
}
}
}
}
รหัสเป็นบิตหนา แต่เป็นองค์ประกอบที่นำกลับมาใช้ใหม่ได้มันควรทำงานได้ดีเท่าที่จะทำได้ในสถานการณ์ส่วนใหญ่และมันจะทำให้รหัสที่ใช้นั้นดีและกระชับ :-)
ของฉันTakeLast
สำหรับที่ไม่ใช่ -IList`1
ขึ้นอยู่กับอัลกอริธึมบัฟเฟอร์บัฟเฟอร์แบบเดียวกันกับที่อยู่ในคำตอบของ @Mark Byers และ @MackieChan มันน่าสนใจว่ามันคล้ายกันแค่ไหน - ฉันเขียนของฉันเองอย่างสมบูรณ์ เดาว่ามีวิธีเดียวที่จะทำบัฟเฟอร์ของแหวนอย่างถูกต้อง :-)
เมื่อดูที่คำตอบของ @ kbrimington การตรวจสอบเพิ่มเติมสามารถเพิ่มลงในนี้IQuerable<T>
เพื่อย้อนกลับไปยังวิธีการที่ทำงานได้ดีกับ Entity Framework - โดยสมมติว่าสิ่งที่ฉันมี ณ จุดนี้ไม่ได้
ด้านล่างตัวอย่างจริงวิธีรับองค์ประกอบ 3 รายการล่าสุดจากคอลเลกชัน (อาร์เรย์):
// split address by spaces into array
string[] adrParts = adr.Split(new string[] { " " },StringSplitOptions.RemoveEmptyEntries);
// take only 3 last items in array
adrParts = adrParts.SkipWhile((value, index) => { return adrParts.Length - index > 3; }).ToArray();
ใช้วิธีนี้เพื่อรับช่วงทั้งหมดโดยไม่มีข้อผิดพลาด
public List<T> GetTsRate( List<T> AllT,int Index,int Count)
{
List<T> Ts = null;
try
{
Ts = AllT.ToList().GetRange(Index, Count);
}
catch (Exception ex)
{
Ts = AllT.Skip(Index).ToList();
}
return Ts ;
}
การใช้งานที่แตกต่างกันเล็กน้อยกับการใช้งานของบัฟเฟอร์แบบวงกลม การวัดประสิทธิภาพแสดงให้เห็นว่าวิธีนี้เร็วกว่าการใช้Queueสองเท่า(การใช้งานของTakeLastในSystem.Linq ) แต่ไม่ต้องเสียค่าใช้จ่าย - มันต้องการบัฟเฟอร์ที่เพิ่มขึ้นตามจำนวนองค์ประกอบที่ร้องขอแม้ว่าคุณจะมี ชุดเล็กคุณสามารถได้รับการจัดสรรหน่วยความจำขนาดใหญ่
public IEnumerable<T> TakeLast<T>(IEnumerable<T> source, int count)
{
int i = 0;
if (count < 1)
yield break;
if (source is IList<T> listSource)
{
if (listSource.Count < 1)
yield break;
for (i = listSource.Count < count ? 0 : listSource.Count - count; i < listSource.Count; i++)
yield return listSource[i];
}
else
{
bool move = true;
bool filled = false;
T[] result = new T[count];
using (var enumerator = source.GetEnumerator())
while (move)
{
for (i = 0; (move = enumerator.MoveNext()) && i < count; i++)
result[i] = enumerator.Current;
filled |= move;
}
if (filled)
for (int j = i; j < count; j++)
yield return result[j];
for (int j = 0; j < i; j++)
yield return result[j];
}
}