Bir koleksiyon verildiğinde, bu koleksiyonun son N öğesini almanın bir yolu var mı? Çerçevede bir yöntem yoksa, bunu yapmak için bir uzantı yöntemi yazmanın en iyi yolu nedir?
Bir koleksiyon verildiğinde, bu koleksiyonun son N öğesini almanın bir yolu var mı? Çerçevede bir yöntem yoksa, bunu yapmak için bir uzantı yöntemi yazmanın en iyi yolu nedir?
Yanıtlar:
collection.Skip(Math.Max(0, collection.Count() - N));
Bu yaklaşım, herhangi bir sıralamaya bağımlı olmadan öğe sırasını korur ve çeşitli LINQ sağlayıcıları arasında geniş bir uyumluluğa sahiptir.
Skip
Negatif bir numara ile arama yapmamaya dikkat etmek önemlidir . Varlık Çerçevesi gibi bazı sağlayıcılar, olumsuz bir argümanla sunulduğunda bir ArgumentException oluşturur. Bunu Math.Max
düzgün bir şekilde önleme çağrısı .
Aşağıdaki sınıf, statik yöntem, statik yöntem ve this
anahtar kelimenin kullanımı gibi uzatma yöntemleri için gerekli tüm özelliklere sahiptir .
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));
}
}
Performans hakkında kısa bir not:
Çağrı çağrısı Count()
belirli veri yapılarının numaralandırılmasına neden olabileceğinden, bu yaklaşım veri üzerinde iki geçişe neden olma riski taşır. Bu, enumerable'ların çoğunda bir sorun değil; aslında, Count()
operasyonları O (1) zamanda değerlendirmek için Listeler, Diziler ve hatta EF sorguları için optimizasyonlar zaten mevcuttur .
Ancak, yalnızca ileriye doğru numaralandırılabilir bir numara kullanmanız gerekiyorsa ve iki geçiş yapmaktan kaçınmak istiyorsanız, Lasse V. Karlsen veya Mark Byers'ın tanımladığı gibi tek geçişli bir algoritmayı düşünün . Bu yaklaşımların her ikisi de, numaralandırma sırasında öğeleri tutmak için geçici bir tampon kullanırlar; bu, koleksiyonun sonu bulunduğunda verilir.
List
s ve LinkedList
s kullanarak , James'in çözümü büyüklük sırasına göre olmasa da daha hızlı olma eğilimindedir. IEnumerable hesaplanırsa (Enumerable.Range aracılığıyla), James'in çözümü daha uzun sürer. Uygulama hakkında bir şey bilmeden veya değerleri farklı bir veri yapısına kopyalamaksızın tek bir geçiş garanti etmenin hiçbir yolunu düşünemiyorum.
coll.Reverse().Take(N).Reverse().ToList();
public static IEnumerable<T> TakeLast<T>(this IEnumerable<T> coll, int N)
{
return coll.Reverse().Take(N).Reverse();
}
GÜNCELLEME: clintp sorununu çözmek için: a) Yukarıda tanımladığım TakeLast () yöntemini kullanmak sorunu çözer, ancak ekstra yöntem olmadan gerçekten yapmak istiyorsanız, Enumerable.Reverse () bir uzantı yöntemi olarak kullanıldığında, bunu şu şekilde kullanmanız gerekmez:
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 () void döndürür ve derleyici bir IEnumerable döndüren Linq yerine bu yöntemi seçer çünkü derleyici hatası alıyorum. Öneriler?
N
kayıtlardan sonra siparişi önemsemiyorsanız, ikinciyi atlayabilirsiniz Reverse
.
Not : Linq Kullanımı adlı soru başlığınızı kaçırdım , bu yüzden cevabım aslında Linq kullanmıyor.
Tüm koleksiyonun tembel olmayan bir kopyasını önbelleğe almaktan kaçınmak istiyorsanız, bağlantılı bir liste kullanarak bunu yapan basit bir yöntem yazabilirsiniz.
Aşağıdaki yöntem, orijinal koleksiyonda bulduğu her bir değeri bağlantılı bir listeye ekler ve bağlantılı listeyi gereken öğe sayısına kısaltır. Koleksiyon boyunca yineleme yoluyla bağlantılı listeyi bu sayıda öğeye kesilmiş halde tuttuğundan, orijinal koleksiyondan yalnızca en fazla N öğenin bir kopyasını tutar.
Orijinal koleksiyondaki öğelerin sayısını bilmenizi veya birden fazla kez yinelemenizi gerektirmez.
Kullanımı:
IEnumerable<int> sequence = Enumerable.Range(1, 10000);
IEnumerable<int> last10 = sequence.TakeLast(10);
...
Genişletme yöntemi:
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;
}
}
Aşağıda numaralandırılabilen ancak yalnızca O (N) geçici depolama alanı kullanan bir yöntem verilmiştir:
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];
}
}
}
Kullanımı:
List<int> l = new List<int> {4, 6, 3, 6, 2, 5, 7};
List<int> lastElements = l.TakeLast(3).ToList();
Elemanları gördükleri gibi saklamak ve eski elemanların üzerine yenilerini yazmak için N boyutunda bir halka tamponu kullanarak çalışır. Numaralandırılabilenin sonuna ulaşıldığında halka tamponu son N elemanlarını içerir.
n
.
.NET Core 2.0+, LINQ yöntemini sağlar TakeLast()
:
https://docs.microsoft.com/en-us/dotnet/api/system.linq.enumerable.takelast
örnek :
Enumerable
.Range(1, 10)
.TakeLast(3) // <--- takes last 3 items
.ToList()
.ForEach(i => System.Console.WriteLine(i))
// outputs:
// 8
// 9
// 10
netcoreapp1.x
) için değil, yalnızca dotnetcore ( netcoreapp2.x
) ' ın v2.0 ve v2.1 sürümü için kullanılabilir . Belki net472
de desteklenmeyen tam çerçeveyi (ör. ) Hedeflemeniz mümkündür . (.net standart kütüphaneleri yukarıdakilerden herhangi biri tarafından kullanılabilir, ancak yalnızca hedef çerçeveye özgü bazı API'ları gösterebilir. bkz. docs.microsoft.com/en-us/dotnet/standard/frameworks )
Kimsenin bahsetmediğine şaşırdım, ancak SkipWhile öğenin dizinini kullanan bir yöntem var .
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)
Bu çözümün diğerlerine göre sunduğu tek algılanabilir fayda, IEnumerable'ı iki kez çaprazlayan iki ayrı işleme sahip olmak yerine, daha güçlü ve etkili bir LINQ sorgusu yapmak için bir yüklem ekleme seçeneğine sahip olabilmenizdir.
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));
}
RX Sisteminde EnumerableEx.TakeLast kullanın.Interactive assembly. @ Mark'ınki gibi bir O (N) uygulamasıdır, ancak bir halka tampon yapısı yerine bir kuyruk kullanır (ve tampon kapasitesine ulaştığında öğeleri ayıklar).
(Not: Bu IEnumerable sürümü - IObservable sürümü değil, ikisinin uygulanması hemen hemen aynı)
Queue<T>
, dairesel bir arabellek kullanılarak uygulanmıyor mu?
Monad'ın bir parçası olarak Rx'e dalmak sakıncası yoksa, şunları kullanabilirsiniz TakeLast
:
IEnumerable<int> source = Enumerable.Range(1, 10000);
IEnumerable<int> lastThree = source.AsObservable().TakeLast(3).AsEnumerable();
Verimliliği ve sadeliği birleştirmeye çalıştım ve bununla sonuçlandım:
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;
}
Performans hakkında: C # 'da, dairesel bir arabellekQueue<T>
kullanılarak uygulanır , böylece her döngüde nesne nesnesi yapılmaz (yalnızca kuyruk büyürken). Birisi bu uzantıyı çağırabilir çünkü kuyruk kapasitesi (özel oluşturucu kullanarak) ayarlamadı . Ekstra performans için, kaynak uygulamasının olup olmadığını kontrol edebilir ve evet ise, dizi dizinlerini kullanarak doğrudan son değerleri ayıklayabilirsiniz.count = int.MaxValue
IList<T>
Yukarıdaki tüm çözümler koleksiyon boyunca yineleme gerektirdiğinden, LINQ kullanarak bir koleksiyonun son N'sini almak biraz verimsizdir. TakeLast(int n)
içinde System.Interactive
de bu problem var.
Bir listeniz varsa, daha etkili bir şey aşağıdaki yöntemi kullanarak dilimlemektir
/// 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);
}
ile
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;
}
ve bazı test senaryoları
[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});
}
Bu soruya cevap vermek için geç olduğunu biliyorum. Ancak, IList <> türünde bir koleksiyonla çalışıyorsanız ve döndürülen koleksiyonun sırasını umursamıyorsanız, bu yöntem daha hızlı çalışır. Mark Byers cevapını kullandım ve küçük değişiklikler yaptım. Şimdi TakeLast yöntemi:
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];
}
}
}
Test için Mark Byers yöntemini ve kbrimington andswer kullandım . Bu test:
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();
İşte 10 element almak için sonuçlar:
1000001 element almak için sonuçlar:
İşte benim çözümüm:
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();
}
}
}
}
Kod biraz tıknaz, ancak bir yeniden kullanılabilir bileşen olarak, çoğu senaryoda olduğu kadar iyi performans göstermeli ve onu kullanan kodu güzel ve özlü tutacaktır. :-)
Benim TakeLast
için olmayanIList`1
, @Mark Byers ve @MackieChan'ın yanıtlarında olduğu gibi aynı halka tampon algoritmasına dayanmaktadır. Ne kadar benzer oldukları ilginç - benimkini tamamen bağımsız yazdım. Sanırım düzgün bir halka tamponu yapmanın sadece bir yolu var. :-)
@ Kbrimington'ın cevabına baktığımızda, IQuerable<T>
Entity Framework ile iyi çalışan yaklaşıma geri dönmek için ek bir kontrol eklenebilir - bu noktada sahip olduğum şeyin olmadığını varsayalım .
Bir koleksiyondan (dizi) son 3 öğenin nasıl alınacağının gerçek örneğinin altında:
// 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();
Tüm Aralığı Hatasız Almak İçin Bu Yöntemi Kullanma
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 ;
}
Dairesel tampon kullanımı ile biraz farklı uygulama. Kriterler yöntem dolaylarında iki kat daha hızlı kullanarak olanlardan daha olduğunu göstermektedir Kuyruk (uygulanması TakeLast içinde System.Linq bu size olsa bile, elemanların istenilen sayı ile birlikte büyüyen bir tampon ihtiyacı - ancak bir maliyet olmadan,) küçük koleksiyon büyük bellek ayırma alabilirsiniz.
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];
}
}