C#_List<T>
_からアイテムをすばやく削除する方法を探しています。ドキュメントには、List.Remove()
およびList.RemoveAt()
操作が両方ともO(n)
であると記載されています
これは私のアプリケーションに深刻な影響を与えています。
いくつかの異なるremoveメソッドを作成し、それらをすべて500,000アイテムの_List<String>
_でテストしました。テストケースを以下に示します...
概要
各番号の文字列表現(「1」、「2」、「3」、...)のみを含む文字列のリストを生成するメソッドを作成しました。次に、リストの5番目の項目ごとにremove
を試みました。リストの生成に使用される方法は次のとおりです。
_private List<String> GetList(int size)
{
List<String> myList = new List<String>();
for (int i = 0; i < size; i++)
myList.Add(i.ToString());
return myList;
}
_
テスト1:RemoveAt()
RemoveAt()
メソッドのテストに使用したテストは次のとおりです。
_private void RemoveTest1(ref List<String> list)
{
for (int i = 0; i < list.Count; i++)
if (i % 5 == 0)
list.RemoveAt(i);
}
_
テスト2:Remove()
Remove()
メソッドのテストに使用したテストは次のとおりです。
_private void RemoveTest2(ref List<String> list)
{
List<int> itemsToRemove = new List<int>();
for (int i = 0; i < list.Count; i++)
if (i % 5 == 0)
list.Remove(list[i]);
}
_
テスト3:nullに設定し、ソートしてからRemoveRange
このテストでは、リストを1回ループして、削除するアイテムをnull
に設定しました。次に、リストをソートし(したがって、nullが上部に表示されます)、nullに設定された上部のすべてのアイテムを削除しました。注:これによりリストの順序が変更されたため、正しい順序に戻す必要がある場合があります。
_private void RemoveTest3(ref List<String> list)
{
int numToRemove = 0;
for (int i = 0; i < list.Count; i++)
{
if (i % 5 == 0)
{
list[i] = null;
numToRemove++;
}
}
list.Sort();
list.RemoveRange(0, numToRemove);
// Now they're out of order...
}
_
テスト4:新しいリストを作成し、すべての「良い」値を新しいリストに追加します
このテストでは、新しいリストを作成し、すべての保持アイテムを新しいリストに追加しました。次に、これらすべてのアイテムを元のリストに追加しました。
_private void RemoveTest4(ref List<String> list)
{
List<String> newList = new List<String>();
for (int i = 0; i < list.Count; i++)
{
if (i % 5 == 0)
continue;
else
newList.Add(list[i]);
}
list.RemoveRange(0, list.Count);
list.AddRange(newList);
}
_
テスト5:nullに設定してからFindAll()
このテストでは、削除するすべてのアイテムをnull
に設定し、FindAll()
機能を使用してnull
以外のすべてのアイテムを検索しました
_private void RemoveTest5(ref List<String> list)
{
for (int i = 0; i < list.Count; i++)
if (i % 5 == 0)
list[i] = null;
list = list.FindAll(x => x != null);
}
_
テスト6:nullに設定してからRemoveAll()
このテストでは、削除するすべてのアイテムをnull
に設定し、RemoveAll()
機能を使用してnull
以外のすべてのアイテムを削除しました
_private void RemoveTest6(ref List<String> list)
{
for (int i = 0; i < list.Count; i++)
if (i % 5 == 0)
list[i] = null;
list.RemoveAll(x => x == null);
}
_
クライアントアプリケーションと出力
_int numItems = 500000;
Stopwatch watch = new Stopwatch();
// List 1...
watch.Start();
List<String> list1 = GetList(numItems);
watch.Stop(); Console.WriteLine(watch.Elapsed.ToString());
watch.Reset(); watch.Start();
RemoveTest1(ref list1);
watch.Stop(); Console.WriteLine(watch.Elapsed.ToString());
Console.WriteLine();
// List 2...
watch.Start();
List<String> list2 = GetList(numItems);
watch.Stop(); Console.WriteLine(watch.Elapsed.ToString());
watch.Reset(); watch.Start();
RemoveTest2(ref list2);
watch.Stop(); Console.WriteLine(watch.Elapsed.ToString());
Console.WriteLine();
// List 3...
watch.Reset(); watch.Start();
List<String> list3 = GetList(numItems);
watch.Stop(); Console.WriteLine(watch.Elapsed.ToString());
watch.Reset(); watch.Start();
RemoveTest3(ref list3);
watch.Stop(); Console.WriteLine(watch.Elapsed.ToString());
Console.WriteLine();
// List 4...
watch.Reset(); watch.Start();
List<String> list4 = GetList(numItems);
watch.Stop(); Console.WriteLine(watch.Elapsed.ToString());
watch.Reset(); watch.Start();
RemoveTest4(ref list4);
watch.Stop(); Console.WriteLine(watch.Elapsed.ToString());
Console.WriteLine();
// List 5...
watch.Reset(); watch.Start();
List<String> list5 = GetList(numItems);
watch.Stop(); Console.WriteLine(watch.Elapsed.ToString());
watch.Reset(); watch.Start();
RemoveTest5(ref list5);
watch.Stop(); Console.WriteLine(watch.Elapsed.ToString());
Console.WriteLine();
// List 6...
watch.Reset(); watch.Start();
List<String> list6 = GetList(numItems);
watch.Stop(); Console.WriteLine(watch.Elapsed.ToString());
watch.Reset(); watch.Start();
RemoveTest6(ref list6);
watch.Stop(); Console.WriteLine(watch.Elapsed.ToString());
Console.WriteLine();
_
結果
_00:00:00.1433089 // Create list
00:00:32.8031420 // RemoveAt()
00:00:32.9612512 // Forgot to reset stopwatch :(
00:04:40.3633045 // Remove()
00:00:00.2405003 // Create list
00:00:01.1054731 // Null, Sort(), RemoveRange()
00:00:00.1796988 // Create list
00:00:00.0166984 // Add good values to new list
00:00:00.2115022 // Create list
00:00:00.0194616 // FindAll()
00:00:00.3064646 // Create list
00:00:00.0167236 // RemoveAll()
_
メモとコメント
最初の2つのテストでは、リストが削除されるたびにリストの順序が変更されるため、実際にはリストから5番目ごとのアイテムが削除されるわけではありません。実際、500,000個のアイテムのうち、83,334個のみが削除されました(100,000個であるはずです)。私はこれで大丈夫です-明らかにRemove()/ RemoveAt()メソッドはとにかく良いアイデアではありません。
リストから5番目の項目を削除しようとしましたが、realityにはそのようなパターンはありません。削除されるエントリはランダムです。
この例では_List<String>
_を使用しましたが、常にそうとは限りません。 _List<Anything>
_である可能性があります
リストに項目を最初から入れないことはnotオプションです。
他の方法(3-6)はすべて、と比較してはるかに優れたパフォーマンスを示しました、しかし、私は少し心配しています-3、5、および6では、値をnull
をクリックし、このセンチネルに従ってすべてのアイテムを削除します。リスト内のアイテムの1つがnull
になり、意図せずに削除されるシナリオを想定できるため、このアプローチは好きではありません。
私の質問は:_List<T>
_から多くのアイテムをすばやく削除する最良の方法は何ですか?私が試したアプローチのほとんどは、本当にく、潜在的に危険に見えます。 List
は間違ったデータ構造ですか?
今、私は新しいリストを作成し、新しいリストに良いアイテムを追加することに傾いていますが、もっと良い方法があるはずです。
リストは、削除に関しては効率的なデータ構造ではありません。削除は隣接するエントリの参照の更新を必要とするだけなので、二重リンクリスト(LinkedList)を使用することをお勧めします。
新しいリストを作成してよければ、項目をnullに設定する必要はありません。例えば:
// This overload of Where provides the index as well as the value. Unless
// you need the index, use the simpler overload which just provides the value.
List<string> newList = oldList.Where((value, index) => index % 5 != 0)
.ToList();
ただし、LinkedList<T>
やHashSet<T>
などの代替データ構造を見たい場合があります。データ構造から必要な機能に本当に依存します。
HashSet
、LinkedList
、またはDictionary
の方がはるかに良いと思います。
順序が重要でない場合は、単純なO(1) List.Removeメソッドがあります。
public static class ListExt
{
// O(1)
public static void RemoveBySwap<T>(this List<T> list, int index)
{
list[index] = list[list.Count - 1];
list.RemoveAt(list.Count - 1);
}
// O(n)
public static void RemoveBySwap<T>(this List<T> list, T item)
{
int index = list.IndexOf(item);
RemoveBySwap(list, index);
}
// O(n)
public static void RemoveBySwap<T>(this List<T> list, Predicate<T> predicate)
{
int index = list.FindIndex(predicate);
RemoveBySwap(list, index);
}
}
このソリューションはメモリトラバーサルに適しているため、最初にインデックスを見つける必要がある場合でも、非常に高速です。
ノート:
リストの最後からいつでもアイテムを削除できます。リストの削除は、デクリメントカウントのみであるため、最後の要素で実行される場合はO(1)です。関連する次の要素のシフトはありません。 (これがリストの削除が一般的にO(n)である理由です)
for (int i = list.Count - 1; i >= 0; --i)
list.RemoveAt(i);
または、これを行うことができます:
List<int> listA;
List<int> listB;
...
List<int> resultingList = listA.Except(listB);
大規模なリストを扱うとき、これは多くの場合より高速です。削除の速度と、削除するディクショナリ内の適切なアイテムの検索は、ディクショナリの作成を補います。ただし、元のリストには一意の値を指定する必要があります。完了したら順序が保証されるとは思いません。
List<long> hundredThousandItemsInOrignalList;
List<long> fiftyThousandItemsToRemove;
// populate lists...
Dictionary<long, long> originalItems = hundredThousandItemsInOrignalList.ToDictionary(i => i);
foreach (long i in fiftyThousandItemsToRemove)
{
originalItems.Remove(i);
}
List<long> newList = originalItems.Select(i => i.Key).ToList();
OK
static void Main(string[] args)
{
Stopwatch watch = new Stopwatch();
watch.Start();
List<Int32> test = GetList(500000);
watch.Stop(); Console.WriteLine(watch.Elapsed.ToString());
watch.Reset(); watch.Start();
test.RemoveAll( t=> t % 5 == 0);
List<String> test2 = test.ConvertAll(delegate(int i) { return i.ToString(); });
watch.Stop(); Console.WriteLine(watch.Elapsed.ToString());
Console.WriteLine((500000 - test.Count).ToString());
Console.ReadLine();
}
static private List<Int32> GetList(int size)
{
List<Int32> test = new List<Int32>();
for (int i = 0; i < 500000; i++)
test.Add(i);
return test;
}
これは2回だけループし、100,000個のアイテムを削除します
このコードの私の出力:
00:00:00.0099495
00:00:00.1945987
1000000
HashSetを試すように更新されました
static void Main(string[] args)
{
Stopwatch watch = new Stopwatch();
do
{
// Test with list
watch.Reset(); watch.Start();
List<Int32> test = GetList(500000);
watch.Stop(); Console.WriteLine(watch.Elapsed.ToString());
watch.Reset(); watch.Start();
List<String> myList = RemoveTest(test);
watch.Stop(); Console.WriteLine(watch.Elapsed.ToString());
Console.WriteLine((500000 - test.Count).ToString());
Console.WriteLine();
// Test with HashSet
watch.Reset(); watch.Start();
HashSet<String> test2 = GetStringList(500000);
watch.Stop(); Console.WriteLine(watch.Elapsed.ToString());
watch.Reset(); watch.Start();
HashSet<String> myList2 = RemoveTest(test2);
watch.Stop(); Console.WriteLine(watch.Elapsed.ToString());
Console.WriteLine((500000 - test.Count).ToString());
Console.WriteLine();
} while (Console.ReadKey().Key != ConsoleKey.Escape);
}
static private List<Int32> GetList(int size)
{
List<Int32> test = new List<Int32>();
for (int i = 0; i < 500000; i++)
test.Add(i);
return test;
}
static private HashSet<String> GetStringList(int size)
{
HashSet<String> test = new HashSet<String>();
for (int i = 0; i < 500000; i++)
test.Add(i.ToString());
return test;
}
static private List<String> RemoveTest(List<Int32> list)
{
list.RemoveAll(t => t % 5 == 0);
return list.ConvertAll(delegate(int i) { return i.ToString(); });
}
static private HashSet<String> RemoveTest(HashSet<String> list)
{
list.RemoveWhere(t => Convert.ToInt32(t) % 5 == 0);
return list;
}
これは私に与えます:
00:00:00.0131586
00:00:00.1454723
100000
00:00:00.3459420
00:00:00.2122574
100000
Nが実際に大きくなるまで、リストはLinkedListsよりも高速です。これは、LinkedListsを使用した場合、Listsよりもキャッシュミスが頻繁に発生するためです。メモリ検索は非常に高価です。リストは配列として実装されるため、必要なデータが隣り合って保存されていることがわかっているため、CPUは大量のデータを一度にロードできます。ただし、リンクリストはCPUに次に必要なデータのヒントを与えません。これにより、CPUはより多くのメモリ検索を実行します。ところで。用語メモリでは、RAMを意味します。
詳細については、以下をご覧ください: https://jackmott.github.io/programming/2016/08/20/when-bigo-foolsya.html
他の回答(および質問自体)では、組み込みの.NET Frameworkクラスを使用して、この "スラッグ"(スローネスバグ)を処理するさまざまな方法を提供しています。
ただし、サードパーティのライブラリに切り替えたい場合は、データ構造を変更し、リストの種類を除いてコードを変更しないでおくだけで、パフォーマンスを向上させることができます。
Loyc Coreライブラリには、List<T>
と同じように機能するが、アイテムをより速く削除できる2つのタイプが含まれています。