web-dev-qa-db-ja.com

リストからアイテムをすばやく削除する方法

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は間違ったデータ構造ですか?

今、私は新しいリストを作成し、新しいリストに良いアイテムを追加することに傾いていますが、もっと良い方法があるはずです。

68
user807566

リストは、削除に関しては効率的なデータ構造ではありません。削除は隣接するエントリの参照の更新を必要とするだけなので、二重リンクリスト(LinkedList)を使用することをお勧めします。

35
Steve Morgan

新しいリストを作成してよければ、項目を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>などの代替データ構造を見たい場合があります。データ構造から必要な機能に本当に依存します。

17
Jon Skeet

HashSetLinkedList、またはDictionaryの方がはるかに良いと思います。

13
Daniel A. White

順序が重要でない場合は、単純な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(n)でなければなりません。
  • リンクリストは、特に長い寿命を持つ大規模なコレクションの場合、トラバースの速度が遅くなります。
11
Yosef O

リストの最後からいつでもアイテムを削除できます。リストの削除は、デクリメントカウントのみであるため、最後の要素で実行される場合はO(1)です。関連する次の要素のシフトはありません。 (これがリストの削除が一般的にO(n)である理由です)

for (int i = list.Count - 1; i >= 0; --i)
  list.RemoveAt(i);
4
arviman

または、これを行うことができます:

List<int> listA;
List<int> listB;

...

List<int> resultingList = listA.Except(listB);
3
NeilPearson

大規模なリストを扱うとき、これは多くの場合より高速です。削除の速度と、削除するディクショナリ内の適切なアイテムの検索は、ディクショナリの作成を補います。ただし、元のリストには一意の値を指定する必要があります。完了したら順序が保証されるとは思いません。

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();
3
NeilPearson

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
3
Alex L

Nが実際に大きくなるまで、リストはLinkedListsよりも高速です。これは、LinkedListsを使用した場合、Listsよりもキャッシュミスが頻繁に発生するためです。メモリ検索は非常に高価です。リストは配列として実装されるため、必要なデータが隣り合って保存されていることがわかっているため、CPUは大量のデータを一度にロードできます。ただし、リンクリストはCPUに次に必要なデータのヒントを与えません。これにより、CPUはより多くのメモリ検索を実行します。ところで。用語メモリでは、RAMを意味します。

詳細については、以下をご覧ください: https://jackmott.github.io/programming/2016/08/20/when-bigo-foolsya.html

2

他の回答(および質問自体)では、組み込みの.NET Frameworkクラスを使用して、この "スラッグ"(スローネスバグ)を処理するさまざまな方法を提供しています。

ただし、サードパーティのライブラリに切り替えたい場合は、データ構造を変更し、リストの種類を除いてコードを変更しないでおくだけで、パフォーマンスを向上させることができます。

Loyc Coreライブラリには、List<T>と同じように機能するが、アイテムをより速く削除できる2つのタイプが含まれています。

  • DList<T> は、ランダムな場所からアイテムを削除するときにList<T>を2倍高速化する単純なデータ構造です。
  • AList<T> は、リストが非常に長い場合(ただしリストが短い場合は遅くなる場合があります)にList<T>を大幅に高速化する洗練されたデータ構造です。
1
Qwertie